AmberDB
view release on metacpan or search on metacpan
lib/AmberDB/Tools.pm view on Meta::CPAN
my $adb = $self->{_adb} or return;
my %diff = ();
return unless $tableid;
scalar @records or return \%diff;
my $table_path = $adb->table_path($tableid);
my $table_info = $adb->table_info($tableid);
exists( $table_info->{search_block} ) or return \%diff;
foreach my $line (@records) {
my @fields = @$line;
foreach my $src ( @{ $table_info->{search_block} } ) {
$src =~ /^[0-9]+$/ or next;
my %words = $adb->get_words( $fields[$src], "write" );
foreach my $word ( keys %words ) {
$diff{recs}->{$src}->{$word}->{ $fields[0] } = 1;
}
}
}
foreach my $src ( @{ $table_info->{search_block} } ) {
$src =~ /^\d+$/ or next;
my $src_path = "${table_path}_$src.src";
next unless -e $src_path;
$adb->table_read($src_path) or next;
$adb->recs_scan(
$src_path,
sub {
my ( $word, $records ) = @_;
foreach my $rid ( $adb->db_decode($records) ) {
if ( exists( $diff{recs}->{$src}->{$word}->{$rid} ) ) {
delete( $diff{recs}->{$src}->{$word}->{$rid} );
}
else {
$diff{inds}->{$src}->{$word}->{$rid} = 1;
}
}
}
);
$adb->table_close($src_path);
}
return \%diff;
}
# my $ok = $tools->tie2csv("dbase_table");
# print $self->{say} if($ok);
# ---------------------------------------------------------------------
sub tie2csv {
my ( $self, $tableid ) = @_;
my $adb = $self->{_adb} or return;
$tableid or return;
my $table_path = $adb->table_path($tableid);
my $i = 1;
if ( -e "$table_path.csv" ) {
my $day_id = $adb->{date}->{day_id} || 'backup';
rename( "$table_path.csv", "$table_path-$day_id.csv" );
}
# cevirme islemlerini yap
my $tie_path = "${table_path}.$adb->{db_ext}";
return 1 unless -e $tie_path;
$adb->table_read($tie_path) or return 1;
my %data;
$adb->recs_scan(
$tie_path,
sub {
my ( $k, $v ) = @_;
$data{$k} = $v;
}
);
$adb->table_close($tie_path);
my @uids = $adb->db_sortid( $tableid, keys %data );
open my $fh, ">", "$table_path.csv" or do {
cluck "[DB_TOOL] Could not open $table_path.csv: $!\n";
return;
};
foreach my $uid (@uids) {
my @fields = $adb->db_decode( $data{$uid} );
my $record = $adb->db_encode( $uid, @fields );
print $fh "$record\n";
$self->{say} .= "$i. $uid ID record converted.\n\n";
$i++;
}
close $fh;
$adb->table_close($tie_path);
return 1;
}
# $tools->{ISO2UTF} = 1;
# my $ok = $tools->csv2tie("file");
# ---------------------------------------------------------------------
sub csv2tie {
my ( $self, $tableid ) = @_;
my $adb = $self->{_adb} or return;
$tableid or return;
# file paths
my $table_path = $adb->table_path($tableid);
my $file_path = "${table_path}.$adb->{db_ext}";
my $csv_path = "$table_path.csv";
return unless -e $csv_path;
# backup with timestamp if exists
if ( -e "${table_path}.$adb->{db_ext}" ) {
my $sec_id = $adb->{date}->{second_id} || time();
rename( "${table_path}.$adb->{db_ext}",
"${table_path}-$sec_id.$adb->{db_ext}" );
unlink("${table_path}.$adb->{db_ext}");
}
# perform conversion operations
my $i = 1;
my @records;
open my $FH, "<", $csv_path or do {
cluck "[DB_TOOL] Could not open $csv_path: $!\n";
return;
};
$adb->table_write($file_path) or do {
close $FH;
cluck "[DB_TOOL] Could not open $file_path for writing.\n";
return;
};
while ( my $record = <$FH> ) {
chomp($record);
$record =~ s/\r$//;
my (@fields) = $adb->db_decode($record);
$adb->recs_put( $file_path, [@fields] );
# collect into list for indexing
push @records, [@fields];
$self->{say} .= "$i. Record ID $fields[0] converted.\n\n";
$i++;
}
close $FH;
$adb->table_close($file_path);
$self->set_index( $tableid, @records );
return 1;
}
# my @tables = $tools->dir_tables();
# ------------------------------------------------
sub dir_tables {
my ( $self, $dir ) = @_;
my $adb = $self->{_adb} or return;
$dir or return;
my $target_dir = File::Spec->catdir( $adb->path('dbase_dir') || ".", $dir );
my $ext = $adb->{db_ext} || "db";
my @names = $adb->dir_files( $target_dir, "*.$ext", full_path => 0 );
my %all_tables = map { /^(.+)\.\Q$ext\E$/i ? ( $1 => 1 ) : () } @names;
return sort { $a cmp $b } keys %all_tables;
}
# my @tables = $tools->vacuum($tableid, 1);
# ------------------------------------------------
sub vacuum {
lib/AmberDB/Tools.pm view on Meta::CPAN
return;
};
foreach my $record (@records) {
my $new_record = $adb->db_encode(@$record);
print $FH "$new_record\n";
$count->{csv1}++;
}
close $FH;
# Reverse operation
rename( $tie_path, $tie_back );
open $FH, "<", $csv_path or do {
cluck "[DB_TOOL] Could not open $csv_path for reading: $!\n";
return;
};
$adb->table_write($tie_path);
while ( my $record = <$FH> ) {
chomp($record);
$record =~ s/\r$//;
my @fields = $adb->db_decode($record);
my $ok = $adb->recs_put( $tie_path, [@fields] );
$ok and $count->{tie2}++;
}
close $FH;
$adb->table_close($tie_path);
$adb->table_attr( $tableid, $table_info );
$self->set_index( $tableid, @records ) if $reindex;
$self->{say} .= "Vacuum completed. Record counts: Tie old: $count->{tie1}, CSV: $count->{csv1}, Tie new: $count->{tie2}\n";
return 1;
}
# my $tables_hash = $tools->all_tables(); # scalar context -> grouped hashref
# my @table_list = $tools->all_tables(); # list context -> flat array of table IDs
# ------------------------------------------------
sub all_tables {
my ($self) = @_;
my $adb = $self->{_adb} or return;
my @all_tables;
my %all_tables;
my $dbase_dir = $adb->path('dbase_dir') || ".";
my $year_dir = $adb->path('year_dir') || "";
my $schema_dir = $adb->path('schema_dir') || "";
# 1. Simple Mode: Single flat directory scan for files matching configured db_ext
if ( $adb->config('simple') ) {
my $ext = $adb->{db_ext} || "db";
my @files = $adb->dir_files( $dbase_dir, "*.$ext", full_path => 0 );
@all_tables = map { /^([a-z0-9_]+)\.\Q$ext\E$/i ? $1 : () } @files;
}
# 2. Standard Structured Mode: Multi-directory scan (tables/ and year directories) for .db files
else {
my $tbl_dir = File::Spec->catdir( $dbase_dir, 'tables' );
push @all_tables, $adb->dir_files( $tbl_dir, "*.db", full_path => 0 );
my %seen_dirs = ( "tables" => 1, "schema" => 1, "backup" => 1 );
if ($year_dir) {
my $yd_path = File::Spec->catdir( $dbase_dir, $year_dir );
push @all_tables, $adb->dir_files( $yd_path, "*.db", full_path => 0 );
$seen_dirs{$year_dir} = 1;
}
# Auto-discover any 4-digit year directories under $dbase_dir (e.g. 2024, 2025, 2026)
if ( -d $dbase_dir ) {
my @year_dirs = $adb->dir_files( $dbase_dir, qr/^\d{4}$/, full_path => 0, files_only => 0 );
foreach my $yd (@year_dirs) {
next if $seen_dirs{$yd} || !-d File::Spec->catdir( $dbase_dir, $yd );
my $yd_path = File::Spec->catdir( $dbase_dir, $yd );
push @all_tables, $adb->dir_files( $yd_path, "*.db", full_path => 0 );
}
}
@all_tables = map { /^([a-z0-9_]+)\.db$/i ? $1 : () } @all_tables;
}
foreach my $record (@all_tables) {
next unless $record;
next if $record =~ /^_/; # skip internal / temp files
if ( my ( $dbs, $tbl ) = ( $record =~ /^([a-z0-9]+)_(.+)$/i ) ) {
$all_tables{$dbs}->{$record} = 1;
if ( $schema_dir && !-e "$schema_dir/$dbs.dbase" ) {
$all_tables{__NO_DBASE__}->{$dbs} = 1;
}
if ( $schema_dir && !-e "$schema_dir/$record.table" ) {
$all_tables{__NO_TABLE__}->{$record} = 1;
}
}
else {
# Single-token table name without underscore
$all_tables{_main}->{$record} = 1;
if ( $schema_dir && !-e "$schema_dir/$record.table" ) {
$all_tables{__NO_TABLE__}->{$record} = 1;
}
}
}
if (wantarray) {
my @flat_list;
foreach my $dbs ( sort keys %all_tables ) {
next if $dbs =~ /^__/;
push @flat_list, sort keys %{ $all_tables{$dbs} };
}
return @flat_list;
}
return \%all_tables;
}
# my $ok = $tools->table_exist("tableid");
# ------------------------------------------------
sub table_exist {
my ( $self, $table ) = @_;
my $adb = $self->{_adb} or return 0;
lib/AmberDB/Tools.pm view on Meta::CPAN
# my $db_obje = $tools->db_simple($datadir);
# ------------------------------------------------
sub db_simple {
my ( $self, $dir, $ext ) = @_;
$ext //= "db";
require AmberDB;
my $db_obje = AmberDB->new(
ext => { db => $ext },
path => { dbase_dir => $dir },
cfg => {
simple => 1,
}
);
return $db_obje;
}
# Rebuilds and converts binary indexes for all tables in database directory.
# my $status = $tools->convert_tables();
# ------------------------------------------------
sub convert_tables {
my ($self) = @_;
my $adb = $self->{_adb} or return;
my $db_dir = $adb->path('dbase_dir');
return unless $db_dir && -d $db_dir;
my @dirs = ($db_dir);
push @dirs, File::Spec->catdir( $db_dir, 'tables' ) if -d File::Spec->catdir( $db_dir, 'tables' );
my @db_files;
foreach my $d (@dirs) {
if ( opendir my $dh, $d ) {
push @db_files, map { File::Spec->catfile( $d, $_ ) }
grep { /\.\Q$adb->{db_ext}\E$/ }
readdir($dh);
closedir $dh;
}
}
my %converted;
foreach my $file (@db_files) {
my ($tableid) = $file =~ /([^\\\/]+)\.\w+$/;
next unless $tableid;
next if $tableid =~ /^\_/;
$self->{say} .= "Processing table $tableid...\n";
my $ok = $self->set_index($tableid);
if ($ok) {
$converted{$tableid} = 1;
$self->{say} .= " -> Rebuilt packed binary indexes (.inx, .fld, .src, .srt) for $tableid\n";
}
}
return \%converted;
}
# Creates a compressed, portable .amberdb archive containing table schemas,
# native data files (.db, .del, .aut, .cnt), and integrity manifest.
# my $archive = $tools->dump( [file => 'backup.amberdb'], [tables => ['t1', 't2']] );
# ---------------------------------------------------------------------
sub dump {
my ( $self, %opts ) = @_;
my $adb = $self->{_adb} or return;
require Archive::Tar;
require Digest::SHA;
require JSON::PP;
require File::Spec;
require File::Path;
# 1. Determine target tables
my @tables;
if ( $opts{tables} && ref( $opts{tables} ) eq 'ARRAY' ) {
@tables = @{ $opts{tables} };
}
elsif ( $opts{table} ) {
@tables = ( $opts{table} );
}
else {
@tables = $self->all_tables();
}
return unless @tables;
# 2. Flush and close open table handles for read consistency
$adb->close_all();
# 3. Determine output file path
my $year = ( $adb->{date} && $adb->{date}->{year} ) ? $adb->{date}->{year} : (localtime)[5] + 1900;
my $month = ( $adb->{date} && $adb->{date}->{month} ) ? $adb->{date}->{month} : sprintf( "%02d", (localtime)[4] + 1 );
my $day = ( $adb->{date} && $adb->{date}->{day} ) ? $adb->{date}->{day} : sprintf( "%02d", (localtime)[3] );
my $date_iso = "$year-$month-$day";
my $time_id = ( $adb->{date} && $adb->{date}->{second_id} ) ? $adb->{date}->{second_id} : time();
my $backup_base = $adb->path('backup_dir')
|| ( $adb->path('dbase_dir') ? $adb->path('dbase_dir') . "/backup" : "backup" );
my $year_dir = "$backup_base/$year";
unless ( -d $year_dir ) {
File::Path::make_path($year_dir);
}
my $outfile = $opts{file} || "$year_dir/amberdb_${date_iso}_${time_id}.amberdb";
# Ensure parent directory for $outfile exists
if ( my ($outdir) = $outfile =~ m{^(.*)[/\\]} ) {
unless ( -d $outdir ) {
File::Path::make_path($outdir);
}
}
my $tar = Archive::Tar->new();
my $manifest = {
format => "AmberDB Archive",
format_version => 1,
amberdb_version => $AmberDB::VERSION || $VERSION,
created_at => "$date_iso " . sprintf( "%02d:%02d:%02d", (localtime)[2], (localtime)[1], (localtime)[0] ),
dbase_dir => $adb->path('dbase_dir'),
tables => {},
};
my $schema_dir = $adb->path('schema_dir')
|| ( $adb->path('dbase_dir') ? $adb->path('dbase_dir') . "/schema" : "schema" );
# Collect .dbase database group schemas
if ( -d $schema_dir ) {
opendir( my $sdh, $schema_dir );
my @dbase_files = grep { /\.dbase$/i } readdir($sdh);
closedir $sdh;
foreach my $df (@dbase_files) {
my ($dbs) = $df =~ /^([^.]+)\.dbase$/i;
next unless $dbs;
# If dumping specific tables, only include matching dbase prefix
if ( $opts{tables} || $opts{table} ) {
my $matched = 0;
foreach my $tid (@tables) {
if ( $tid =~ /^\Q$dbs\E_/ or $tid eq $dbs ) {
$matched = 1;
last;
}
}
next unless $matched;
}
my $dpath = "$schema_dir/$df";
if ( -e $dpath ) {
open my $dfh, "<:raw", $dpath or next;
local $/ = undef;
my $dcontent = <$dfh>;
close $dfh;
$tar->add_data( "schema/$df", $dcontent );
$manifest->{dbases}->{$dbs} = "schema/$df";
}
}
}
lib/AmberDB/Tools.pm view on Meta::CPAN
$arch_path = $1;
}
else {
$arch_path = "tables/$tid.$sfx";
}
$tar->add_data( $arch_path, $dcontent );
push @{ $table_manifest->{files} }, $arch_path;
my $sha256 = Digest::SHA::sha256_hex($dcontent);
$table_manifest->{sha256}->{$arch_path} = $sha256;
}
}
# D. Collect Authoritative String Dictionaries (_*.unq)
my @unq_files = glob "${tpath}_*.unq";
foreach my $fpath (@unq_files) {
next unless -e $fpath;
open my $dfh, "<:raw", $fpath or next;
local $/ = undef;
my $dcontent = <$dfh>;
close $dfh;
my $norm_fpath = $fpath;
$norm_fpath =~ s{\\}{/}g;
my $arch_path = $norm_fpath;
if ( $norm_fpath =~ m{^\Q$base_dir\E/(.+)$} ) {
$arch_path = $1;
}
else {
my ($fname) = $fpath =~ m{([^/\\]+)$};
$arch_path = "tables/$fname";
}
$tar->add_data( $arch_path, $dcontent );
push @{ $table_manifest->{files} }, $arch_path;
my $sha256 = Digest::SHA::sha256_hex($dcontent);
$table_manifest->{sha256}->{$arch_path} = $sha256;
}
$manifest->{tables}->{$tid} = $table_manifest;
}
# Add manifest.json to archive
my $json = JSON::PP->new->utf8->pretty->encode($manifest);
$tar->add_data( "manifest.json", $json );
# Write tar.gz archive
unless ( $tar->write( $outfile, Archive::Tar::COMPRESS_GZIP() ) ) {
cluck "[DB_BACKUP] Failed to write archive $outfile: " . $tar->error() . "\n";
return;
}
$self->{say} .= "Archive successfully written to $outfile (" . ( -s $outfile ) . " bytes)\n";
return wantarray ? ( $outfile, $manifest ) : $outfile;
}
# Restores a .amberdb archive into target database, validates checksums,
# and deterministically rebuilds all binary indexes via set_index.
# my $res = $tools->restore( file => 'backup.amberdb', [force => 1], [reindex => 1] );
# ---------------------------------------------------------------------
sub restore {
my ( $self, %opts ) = @_;
my $adb = $self->{_adb} or return;
my $file = $opts{file} or do {
cluck "[DB_RESTORE] Missing required parameter 'file'.\n";
return;
};
return unless -e $file;
require Archive::Tar;
require Digest::SHA;
require JSON::PP;
require File::Spec;
require File::Path;
my $tar = Archive::Tar->new();
unless ( $tar->read($file) ) {
cluck "[DB_RESTORE] Cannot read archive $file: " . $tar->error() . "\n";
return;
}
# 1. Read and parse manifest.json
my $manifest_content = $tar->get_content("manifest.json");
unless ($manifest_content) {
cluck "[DB_RESTORE] Archive $file is missing manifest.json.\n";
return;
}
my $manifest = eval { JSON::PP->new->utf8->decode($manifest_content) };
if ( $@ || ref($manifest) ne 'HASH' ) {
cluck "[DB_RESTORE] Corrupted manifest.json in $file: $@\n";
return;
}
# 2. Check if target DB is empty or force is set
my $schema_dir = $adb->path('schema_dir')
|| ( $adb->path('dbase_dir') ? $adb->path('dbase_dir') . "/schema" : "schema" );
my $table_dir = $adb->path('table_dir')
|| ( $adb->path('dbase_dir') ? $adb->path('dbase_dir') . "/tables" : "tables" );
my $force = $opts{force} || $opts{overwrite};
unless ($force) {
# Check if existing schema or data files exist.
# NOTE: Avoid calling table_path() here because it triggers
# table_info() -> dbase_info() which caches empty hashes for
# schemas that don't exist yet on the target, poisoning the
# cache for the rest of the restore operation.
my $has_existing = 0;
my $db_ext = $adb->{db_ext} || "db";
foreach my $tid ( keys %{ $manifest->{tables} || {} } ) {
if ( -e "$table_dir/$tid.$db_ext" || -e "$schema_dir/$tid.table" ) {
$has_existing = 1;
last;
}
}
if ($has_existing) {
cluck "[DB_RESTORE] Target database is not empty. Use 'force => 1' to overwrite existing tables.\n";
return;
lib/AmberDB/Tools.pm view on Meta::CPAN
if ( $expected_sha ne $actual_sha ) {
cluck "[DB_RESTORE] Checksum mismatch for $arch_path! Expected $expected_sha, got $actual_sha.\n";
return;
}
}
# Native archive path: e.g. tables/products.db or 2026/sales.db
my $target_file = "$base_dir/$arch_path";
if ( my ($tdir) = $target_file =~ m{^(.*)[/\\]} ) {
File::Path::make_path($tdir) unless -d $tdir;
}
open my $dfh, ">:raw", $target_file or do {
cluck "[DB_RESTORE] Cannot write data file $target_file: $!\n";
return;
};
print $dfh $dcontent;
close $dfh;
}
push @restored_tables, $tid;
}
# 5. Rebuild all indexes if reindex is requested (default: 1)
my $reindex = defined $opts{reindex} ? $opts{reindex} : 1;
if ($reindex) {
foreach my $tid (@restored_tables) {
$self->set_index($tid);
}
}
$self->{say} .= "Restored " . scalar(@restored_tables) . " tables from $file.\n";
return {
ok => 1,
file => $file,
tables => \@restored_tables,
manifest => $manifest,
reindexed => $reindex ? 1 : 0,
};
}
1;
__END__
=head1 NAME
AmberDB::Tools - Database maintenance, CLI reindexing, and bulk conversion toolset
=head1 SYNOPSIS
use AmberDB;
use AmberDB::Tools;
my $adb = AmberDB->new(path => { dbase_dir => "/path/to/dbstore" });
my $tools = AmberDB::Tools->new($adb);
# 1. Create portable .amberdb database backup archive
my $archive = $tools->dump();
# 2. Restore database archive with integrity validation and reindexing
my $result = $tools->restore(file => "backup.amberdb", force => 1);
# 3. Rebuild all indexes for a single table (.inx, .src, .fld, .fac, .srt)
$tools->set_index("catalog_product");
# 4. Rebuild only specific index components
$tools->set_search("catalog_product"); # Rebuild full-text search index
$tools->set_fields("catalog_product"); # Rebuild field exact match index
$tools->set_filters("catalog_product"); # Rebuild facet forward filter index
$tools->set_sort("catalog_product"); # Rebuild binary sort index
# 5. Batch reindex / convert all tables in database directory
my $report = $tools->convert_tables();
=head1 DESCRIPTION
C<AmberDB::Tools> provides maintenance, native disaster recovery archiving (C<dump>/C<restore>), and batch utility functions for rebuilding indexes, populating full-text search inverted files, compiling forward facet filter dictionaries, generating b...
=head1 CONSTRUCTOR
=head2 new($adb, [%options])
Creates an C<AmberDB::Tools> instance associated with an active C<AmberDB> object handle.
my $tools = AmberDB::Tools->new($adb);
=head1 METHODS
=head2 dump([%options])
Creates a compressed, portable C<.amberdb> archive file (gzipped tar archive) containing table and database schemas (C<schema/*.table>, C<schema/*.dbase>), native database data files (C<tables/*.db>, C<tables/*.del>, C<tables/*.aut>, C<tables/*.cnt>)...
Options:
=over 4
=item * C<file>: Custom output file path (defaults to C<backup/YYYY/amberdb_YYYY-MM-DD_time.amberdb>).
=item * C<tables>: Array reference of table IDs to include (defaults to all tables in database).
=item * C<table>: Single table ID to export as a focused snapshot.
=back
my $archive = $tools->dump();
my $archive = $tools->dump(tables => ["catalog_product", "orders_cart"]);
=head2 restore(%options)
Restores a C<.amberdb> archive into the target database. Validates archive integrity via SHA-256 checksums in C<manifest.json>, extracts schemas and data files, and deterministically reconstructs all binary indexes (C<.inx>, C<.src>, C<.fld>, C<.fac>...
Options:
=over 4
=item * C<file>: Path to C<.amberdb> archive file (required).
=item * C<force>: Boolean (default 0). Must be set to 1 to overwrite existing tables in a non-empty database directory.
=item * C<reindex>: Boolean (default 1). Automatically executes C<set_index> for all restored tables.
=item * C<tables>: Array reference of specific table IDs to extract from the archive.
=back
my $res = $tools->restore(file => "backup.amberdb", force => 1);
=head2 set_index($table_id, [@records])
Rebuilds all secondary and primary indexes for C<$table_id> based on its schema definition:
=over 4
=item * Primary key index (C<.inx>) via C<set_readall>
=item * Full-text search inverted indexes (C<_${blk}.src>) via C<set_search>
=item * Inverted field match indexes (C<_${blk}.fld>) via C<set_fields>
=item * Columnar facet filter forward indexes (C<_${blk}.fac>) via C<set_filters>
=item * Monotonic binary pre-sorted record indexes (C<_${blk}.srt>) via C<set_sort>
=back
If C<@records> is omitted, reads all records from the base table automatically.
$tools->set_index("catalog_product");
=head2 set_readall($table_id, [@ids])
Rebuilds the primary C<.inx> index file, populating C<keys> (compact binary packed list of IDs), C<count>, and C<lastid>.
$tools->set_readall("catalog_product");
=head2 set_search($table_id, [@records])
Scans records, tokenizes text according to schema C<search_block>, and builds inverted keyword index files (C<_${blk}.src>).
$tools->set_search("catalog_product");
=head2 set_fields($table_id, [@records])
Builds inverted exact match index files (C<_${blk}.fld>) for fields specified in schema C<match_block>.
$tools->set_fields("catalog_product");
=head2 set_filters($table_id, [@records])
Builds columnar facet forward index files (C<_${blk}.fac>) and bidirectional string dictionaries (C<_${blk}.unq>) for blocks configured in schema C<facet_block>.
$tools->set_filters("catalog_product");
=head2 set_sort($table_id, [@records])
Builds monotonic binary pre-sorted index files (C<_${blk}.srt>) according to schema C<sort_block>.
$tools->set_sort("catalog_product");
=head2 convert_tables()
Scans the entire database directory, identifies all physical tables, and sequentially runs C<set_index> to rebuild and migrate packed binary indexes across the entire system. Returns a status hash reference.
my $status = $tools->convert_tables();
=head1 AUTHOR
Maruf Cetin <marufcetin@gmail.com>
=head1 LICENSE AND COPYRIGHT
Copyright (C) 2018-2026 Maruf Cetin.
( run in 0.541 second using v1.01-cache-2.11-cpan-4ef0a570458 )