AmberDB

 view release on metacpan or  search on metacpan

bin/convert_dbstore.pl  view on Meta::CPAN


# Discover tables across all database directories
my @table_list = $tools->all_tables();

my %side_files = ( del => 0, aut => 0, cnt => 0, str => 0 );
foreach my $tid (@table_list) {
    my $tpath = $adb->table_path($tid);
    $side_files{del}++ if -e "$tpath.del";
    $side_files{aut}++ if -e "$tpath.aut";
    $side_files{cnt}++ if -e "$tpath.cnt";
    my @strs = glob "${tpath}_*.str";
    $side_files{str} += scalar(@strs);
}

print "Found " . scalar(@table_list) . " main tables (.db) to re-index.\n";
print "Detected side files: " . $side_files{del} . " .del (archived deleted), "
    . $side_files{aut} . " .aut (audit logs), "
    . $side_files{cnt} . " .cnt (view counters), "
    . $side_files{str} . " .str (string dictionaries).\n";
print "-----------------------------------------------------------------\n";

lib/AmberDB.pm  view on Meta::CPAN


    my $k = $self->utf_encode("$key");

    my $ret = $db->del($k);
    warn "[DB_TIE] $table_path can't del key $k.\n" if $ret > 0;

    return $ret == 0 ? 1 : 0;
}

# Writes add|edit|del operation to daily CSV backup audit stream (backup/YYYY/YYYY-MM-DD.csv).
# Exits silently if no_backup is set (globally or in table schema).
# my $ok = $adb->recs_back("add|edit|del", $tableid, @records);
# ------------------------------------------------
sub recs_back {

    my ( $self, $action, $tableid, @records ) = @_;

    ( $action and $tableid and scalar @records ) or return;

    # Global config check: disables backup for all tables
    return if $self->config('no_backup');

lib/AmberDB/Base.pm  view on Meta::CPAN

    my %pairs = ( @args == 1 && ref( $args[0] ) eq 'HASH' ) ? %{ $args[0] } : @args;

    for my $key ( keys %pairs ) {
        $self->{_path}->{$key} = $pairs{$key};
    }
    $self->_invalidate_table_paths();

    return $self;
}

# Invalidate cached table paths if global path-affecting configurations change
# ------------------------------------------------
sub _invalidate_table_paths {

    my ($self) = @_;

    if ( $self->{_table} && ref( $self->{_table} ) eq 'HASH' ) {
        for my $tbl ( keys %{ $self->{_table} } ) {
            delete $self->{_table}->{$tbl}->{_path}
              if ref( $self->{_table}->{$tbl} ) eq 'HASH';
        }

lib/AmberDB/Locale.pm  view on Meta::CPAN

    return 0 unless defined $cond && length $cond;

    my $n = abs( $count // 0 );

    # Whitelist strictly only digits, 'n', whitespace, arithmetic and logical operators
    return 0 unless $cond =~ /^[n0-9+\-*\/%&|!=<>()\s]+$/;

    # Replace 'n' variable token with actual numeric value
    ( my $expr = $cond ) =~ s/\bn\b/$n/g;

    # Safely evaluate numeric expression isolated from global DIE handlers
    my $res = eval {
        local $SIG{__DIE__} = sub {};
        eval $expr; ## no critic
    };
    return $res ? 1 : 0;
}

# -------------------------------------------------------
# Evaluate CLDR plural rule and select template.
#

lib/AmberDB/Tools.pm  view on Meta::CPAN

# my @tables = $tools->dir_tables();
# ------------------------------------------------
sub dir_tables {

    my ( $self, $dir ) = @_;
    my $adb = $self->{_adb} or return;

    $dir or return;
    my $dbase_dir = $adb->path('dbase_dir') || ".";
    my @all_tables =
      ( glob "$dbase_dir/$dir/*.$adb->{db_ext}" );

    my %all_tables =
      map { /([^\/]+)\.$adb->{db_ext}$/; $1 => 1 } @all_tables;

    return sort { $a cmp $b } keys %all_tables;
}

# my @tables = $tools->vacuum($tableid, 1);
# ------------------------------------------------
sub vacuum {

lib/AmberDB/Tools.pm  view on Meta::CPAN

    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";
        push @all_tables, ( glob "$dbase_dir/*.$ext" );
        @all_tables = map { /([a-z0-9_]+)\.\Q$ext\E$/i; $1 } @all_tables;
    }
    # 2. Standard Structured Mode: Multi-directory scan (tables/ and year directories) for .db files
    else {
        push @all_tables, ( glob "$dbase_dir/tables/*.db" );

        my %seen_dirs = ( "tables" => 1, "schema" => 1, "backup" => 1 );
        if ($year_dir) {
            push @all_tables, ( glob "$dbase_dir/$year_dir/*.db" );
            $seen_dirs{$year_dir} = 1;
        }

        # Auto-discover any 4-digit year directories under $dbase_dir (e.g. 2024, 2025, 2026)
        if ( -d $dbase_dir ) {
            opendir( my $dh, $dbase_dir );
            my @year_candidates = grep { /^\d{4}$/ && -d "$dbase_dir/$_" && !$seen_dirs{$_} } readdir($dh);
            closedir $dh;

            foreach my $yd (@year_candidates) {
                push @all_tables, ( glob "$dbase_dir/$yd/*.db" );
            }
        }

        @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

lib/AmberDB/Tools.pm  view on Meta::CPAN

sub replace_tablename {

    my ( $self, $find, $replace ) = @_;
    my $adb = $self->{_adb} or return;

    my @tables;
    my $dbase_dir = $adb->path('dbase_dir') || ".";
    my $year_dir  = $adb->path('year_dir') || "";

    if ( $adb->config('simple') ) {
        @tables = glob "$dbase_dir/$find.*";
        push @tables, ( glob "$dbase_dir/${find}_*" );
    }
    else {
        @tables = glob "$dbase_dir/tables/$find.*";
        push @tables, ( glob "$dbase_dir/tables/${find}_*" );
        if ( $adb->config('use_section') ) {
            my @sections = glob "$dbase_dir/section_*";
            foreach my $sec_file (@sections) {
                push @tables, ( glob "$sec_file/$find.*" );
                push @tables, ( glob "$sec_file/${find}_*" );
            }
        }

        if ( $adb->config('use_year') && $year_dir ) {
            @tables = glob "$dbase_dir/$year_dir/$find.*";
            push @tables, ( glob "$dbase_dir/$year_dir/${find}_*" );
            if ( $adb->config('use_section') ) {
                my @sections = glob "$dbase_dir/$year_dir/section_*";
                foreach my $sec_file (@sections) {
                    push @tables, ( glob "$sec_file/${find}.*" );
                    push @tables, ( glob "$sec_file/${find}_*" );
                }
            }
        }
    }

    foreach my $old_file (@tables) {
        my $new_file = "$old_file";
        $new_file =~ s/\/$find([\.\_])/$replace$1/;
        rename( $old_file, $new_file );
    }

lib/AmberDB/Tools.pm  view on Meta::CPAN

    my ( $self, $tableid ) = @_;
    my $adb = $self->{_adb} or return;

    $tableid or return;

    $adb->config('no_write') and return;

    my $table_path = $adb->table_path($tableid);
    return unless $table_path && -e "$table_path.$adb->{db_ext}";

    my @files  = glob "$table_path*";
    my @files1 = grep { /^\Q$table_path\E(?:\.[a-z0-9]+|_[0-9]+\.[a-z0-9]+)$/i } @files;

    foreach my $file (@files1) {
        unlink($file);
        $self->{say} .= "          * $file deleted.\n";
    }

    $self->{say} .= "    - Table $tableid deleted.\n";

    return 1;

lib/AmberDB/Tools.pm  view on Meta::CPAN


                $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 (_*.str)
        my @str_files = glob "${tpath}_*.str";
        foreach my $fpath (@str_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;

lib/AmberDB/Transact.pm  view on Meta::CPAN

# Uses non-blocking exclusive flock to safely claim ownership without race conditions.
# Rolls back and removes confirmed orphaned journals.
# Called automatically at the start of each new transaction.
# ------------------------------------------------
sub transact_recover {
    my ( $self ) = @_;

    my $txn_dir = ( $self->path('dbase_dir') || "." ) . "/txn";
    return unless -d $txn_dir;

    my @orphans = glob "$txn_dir/txn_*.txn";
    return unless @orphans;

    foreach my $orphan ( sort @orphans ) {
        my ($pid) = $orphan =~ /\-(\d+)\.txn$/;
        next unless $pid;

        # Skip our own active transaction file
        next if $self->{_txn} && $self->{_txn}->{file} && $orphan eq $self->{_txn}->{file};

        # Open candidate orphan journal file

t/amberdb_simple_mode.t  view on Meta::CPAN


        my @read = $adb->read_id( 'sessions', $id );
        is( $read[0], $id, "read_id returned exact ID: $id" );
        is( $read[1], 'Active', "read_id field 1 matches for $id" );

        my $exists = $adb->exist_id( 'sessions', $id );
        is( $exists, 1, "exist_id confirms key exists: $id" );
    }

    # Verify that ONLY sessions.db was created, NO index files (.inx, .fld, .src, .srt)
    my @created_files = glob("$db_dir/*");
    my @index_files   = grep { /\.(inx|fld|src|srt|fac|rwt)$/ } @created_files;
    is_deeply( \@index_files, [], 'No index files (.inx, .fld, .src, .srt, .fac, .rwt) generated' );
};

# ============================================================
# 2. CRUD Operations with Arbitrary IDs
# ============================================================
subtest '2. CRUD Lifecycle & Bulk Operations in Simple Mode' => sub {
    my $db_dir = "$tmp_dir/simple_db2";
    mkdir($db_dir);



( run in 2.552 seconds using v1.01-cache-2.11-cpan-0fb53d1c279 )