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 )