AmberDB

 view release on metacpan or  search on metacpan

lib/AmberDB.pm  view on Meta::CPAN

    $self->clear_cache($tableid);

    if ($use_ramdisk) {
        $self->ramdisk_delete($tableid);
    }
    else {
        my $file_path = $self->table_path( $tableid, 1 );
        if ( -e $file_path ) {
            $self->table_close($file_path);
            unlink $file_path;
        }
    }
    return 1;
}

# Automatically invalidates/truncates child volatile RAM-disk tables
# when their parent_table receives writes or deletes.
# ------------------------------------------------
sub _invalidate_child_ramdisk_tables {
    my ( $self, $parent_table ) = @_;
    return unless $parent_table;

    my %children;

    # 1. Check loaded schemas in memory
    if ( $self->{_table} && ref( $self->{_table} ) eq 'HASH' ) {
        for my $tbl ( keys %{ $self->{_table} } ) {
            my $info = $self->{_table}{$tbl};
            if ( $info && ( $info->{parent_table} // '' ) eq $parent_table && ( $info->{use_ramdisk} // 0 ) == 3 ) {
                $children{$tbl} = 1;
            }
        }
    }

    # 2. Check schema directory for any *.table with parent_table
    my $schema_dir = $self->path('schema_dir');
    if ( $schema_dir && -d $schema_dir ) {
        if ( opendir( my $dh, $schema_dir ) ) {
            while ( my $file = readdir($dh) ) {
                next unless $file =~ /^([a-zA-Z0-9_]+)\.table$/;
                my $tbl = $1;
                next if $tbl eq $parent_table;
                next if exists $children{$tbl};
                my $info = $self->table_info($tbl);
                if ( $info && ( $info->{parent_table} // '' ) eq $parent_table && ( $info->{use_ramdisk} // 0 ) == 3 ) {
                    $children{$tbl} = 1;
                }
            }
            closedir($dh);
        }
    }

    for my $child_table ( keys %children ) {
        $self->table_truncate($child_table);
    }
    return scalar keys %children;
}


# Sanitizes and validates a record ID according to table mode.
# If simple mode (global simple or table use_simple): allows safe arbitrary keys (max 255 bytes, no control chars).
# If standard mode: strictly enforces positive integer numeric ID.
# my $clean_id = $adb->id_check($tableid, $rid);
# ------------------------------------------------
sub id_check {
    my ( $self, $tableid, $rid ) = @_;

    return unless defined $rid && $rid ne '';
    return if ref $rid;

    my $table_info = $tableid ? $self->table_info($tableid) : {};
    my $is_simple  = $self->config('simple') || ( $table_info && $table_info->{use_simple} );

    # 1. Simple Mode: Safe Key Sanitization (no binary control chars, max 255 bytes)
    if ($is_simple) {
        return if $rid =~ /[\x00-\x1f\x7f]/;
        $rid = $self->trim_space($rid);
        return unless defined $rid && length($rid) > 0;
        return if length($rid) > 255;
        return $rid;
    }

    # 2. Standard Mode: Strict positive integer numeric ID
    $rid =~ s/\D//g;
    return ( length($rid) > 0 && $rid > 0 ) ? $rid : undef;
}

# Sets or gets table auto id.
# my $id = $adb->table_autoid($tableid, [$id]);
# ------------------------------------------------
sub table_autoid {

    my ( $self, $tableid, $aid ) = @_;

    $tableid or return;
    my $table_info = $self->table_info($tableid);
    my $is_simple  = $self->config('simple') || ( $table_info && $table_info->{use_simple} );

    if ( defined $aid && $aid ne '' && $aid ne '0' ) {
        $aid = $self->id_check( $tableid, $aid );
        return unless defined $aid && $aid ne '';

        # Numeric ID must be greater than current lastid unless simple mode
        if ( !$is_simple && $aid =~ /^\d+$/ ) {
            my $cached_auto = $self->get_cache( $tableid, 'last_autoid' );
            my $last = $cached_auto // ( $self->table_lastid($tableid) || 0 );

            if ( $aid <= $last ) {
                my $table_path = $self->table_path($tableid);
                my $file_path  = "$table_path.$self->{db_ext}";
                $self->transact_error( $file_path, "ID must be greater than last ID ($last): $aid" );
                return;
            }

            $self->set_cache( $tableid, 'last_autoid', $aid );
        }
    }
    else {
        my $last = $self->table_lastid($tableid) || 0;
        my $cached_auto = $self->get_cache( $tableid, 'last_autoid' );
        $last = $cached_auto if ( $cached_auto && $cached_auto > $last );

lib/AmberDB.pm  view on Meta::CPAN


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

    # Plural path:
    return 0 unless @keys_to_del;
    if ( !$self->{_db}->{$table_path} || !$self->{_dbm}->{$table_path} ) {
        $self->table_write($table_path)
              or do { cluck "[DB_TIE] $table_path can't open for index_del.\n"; return 0; };
    }
    my $db = $self->{_db}->{$table_path};
    return 0 unless $db;

    my $is_txn = $self->is_transact($tableid);
    my $del_count = 0;
    for my $k_item (@keys_to_del) {
        next unless defined $k_item && $k_item ne '';
        my $k = $self->utf_encode("$k_item");

        if ( $is_txn && !$self->{_txn}->{logged}->{"$table_path\x1e$k_item"}++ ) {
            my $old_raw;
            my $ret = $db->get( $k, $old_raw );
            if ( $ret == 0 && defined $old_raw ) {
                $self->_txn_log( 'index', $tableid, $table_path, $k_item, 'del', $old_raw );
            }
        }

        my $ret = $db->del($k);
        $del_count++ if $ret == 0;
    }

    # RAM-disk Tier 2 (mirror) or (Tier 4 in active transaction) dual-write
    if ( !$no_mirror && $tableid && $self->ramdisk_is_mounted() ) {
        my $table_info  = eval { $self->table_info($tableid) };
        my $use_ramdisk = $table_info ? $self->_normalize_ramdisk_tier( $table_info->{use_ramdisk} // $table_info->{use_cache} // 0 ) : 0;
        if ( $use_ramdisk == 1 || $use_ramdisk == 2 || ( $use_ramdisk == 4 && $is_txn ) ) {
            my $ram_dir   = $self->ramdisk_dir();
            my $is_in_ram = ( $ram_dir && index( $table_path, $ram_dir ) == 0 ) ? 1 : 0;
            my ($ext)     = $table_path =~ m{\.([^.]+)$};
            my $mirror_path;
            if ( $ext && $ext =~ /^(?:\Q$self->{db_ext}\E|inx|src|fld|fac|unq|slg|ajw|ajn)$/ ) {
                if ($is_in_ram) {
                    my $tbl_path = $self->table_path($tableid);
                    $mirror_path = "$tbl_path.$ext" if $tbl_path;
                }
                else {
                    my $r_path = $self->ramdisk_path($tableid);
                    $mirror_path = "$r_path.$ext" if $r_path;
                }
                if ( $mirror_path && $mirror_path ne $table_path ) {
                    $self->index_del( [ $mirror_path, $tableid, 1 ], \@keys_to_del );
                }
            }
        }
    }

    return $del_count;
}

# 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 1 if $self->config('no_backup');

    # Table schema check: no_backup => 1 in table schema or volatile Tier 3 RAM-disk
    my $table_info = $self->table_info($tableid);
    return 1 if $table_info->{no_backup} || ( $table_info->{use_ramdisk} // 0 ) == 3;

    my $user = $self->config('user') || 'system';

    $tableid =~ s/[:\/\\]/--/g;

    my $backup_base = $self->path('backup_dir')
      || ( $self->path('dbase_dir') ? $self->path('dbase_dir') . "/backup" : "backup" );
    my $year     = $self->year;
    my $month    = $self->month;
    my $day      = $self->day;
    my $date_iso = "$year-$month-$day";
    my $time_str = $self->date_str;

    my $backup_file;
    if ( $self->config('simple') ) {
        $backup_file = "$backup_base/$date_iso.csv";
    }
    else {
        my $year_dir = "$backup_base/$year";
        $self->make_path($year_dir);
        $backup_file = "$year_dir/$date_iso.csv";
    }

    open my $YAZ, ">>:encoding(UTF-8)", $backup_file
      or do {
        cluck "[DB_BACKUP] Cannot open backup file $backup_file: $!\n";
        return;
      };

    foreach my $record (@records) {
        ref($record) eq "ARRAY" or $record = [$record];
        my $rid     = $record->[0];
        my $bac_val = $self->db_encode( @{$record}[ 1 .. $#$record ] );
        print $YAZ "$time_str\t$user\t$action\t$tableid\t$rid\t$bac_val\n";
    }
    close $YAZ;

    return 1;
}

# my $view_pre = $adb->auth_view($tableid, $rid);
# Returns add/edit/del audit history on record as HTML <pre>.
# ------------------------------------------------
sub auth_view {

    my ( $self, $tableid, $rid ) = @_;

lib/AmberDB.pm  view on Meta::CPAN

  # 2. Insert with arbitrary key
  $adb->insert_id('sessions', 'user@example.com', 'Active', 'Chrome', time());

  # 3. Read record (O(1))
  my @sess = $adb->read_id('sessions', 'user@example.com');

  # 4. Search and filter without indexes
  my ($count, @active) = $adb->field_fetch('sessions', 1, 'Active', 0, 10);

  # 5. Volatile in-memory simple store
  my $ram_db = AmberDB->new(
      path => { dbase_dir => "/dev/shm/amberdb_tokens" },
      cfg  => { simple    => 1, no_backup => 1 },
  );
  $ram_db->insert_id('tokens', $token_id, $user_id, time());

=head1 RAM-DISK ACCELERATION

AmberDB features a built-in, transparent physical RAM-disk acceleration engine (Linux C<tmpfs>, macOS C<APFS RAM-Disk> via C<hdiutil>, or Windows C<ImDisk>). It enables high-traffic tables and hot indexes to operate at pure volatile memory speeds whi...

=head2 What is it?

RAM-disk acceleration routes file I/O for database tables directly to an operating system in-memory RAM-disk filesystem (Windows ImDisk C<R:/amberdb_$dbname>, Linux tmpfs C</dev/shm/amberdb_$dbname>, or macOS APFS C</Volumes/amberdb_$dbname>). Unlike...

=head2 How It Works

=over 4

=item * B<Native File Formats:> AmberDB stores all table data and index files on RAM-disk using their exact native file extensions (C<.db>, C<.inx>, C<.fld>, C<.src>, C<.fac>, C<.unq>, C<.slg>). No proprietary cache file format is used.

=item * B<Dual-Write Synchronization:> For accelerated tables, reads are served at microsecond RAM speeds directly from RAM-disk. Writes synchronously update both the persistent disk file and the RAM-disk file, ensuring complete data durability witho...

=item * B<ACID Transaction Safety:> Transactions (C<transact_start>, C<transact_end>, C<transact_rollback>) protect RAM-disk operations with disk-backed undo journals and strict two-phase locking (Strict 2PL).

=item * B<Automated Mount Detection & Strict Zero-Fallback:> The engine automatically verifies whether the OS RAM-disk filesystem is actively mounted. If unmounted, AmberDB enforces a strict Zero-Fallback policy: all RAM-disk paths (C<ramdisk_dir>, C...

=back

=head2 Acceleration Tiers (C<use_ramdisk>)

Tables can be assigned an acceleration tier via the C<use_ramdisk> schema flag or dynamically via C<table_attr()>:

=over 4

=item * B<Tier 1 (Hybrid Index-Only, C<use_ramdisk =E<gt> 1>):> Only secondary index files (C<.inx>, C<.fld>, C<.src>, C<.fac>, C<.unq>, C<.slg>) are placed in RAM-disk and synchronously dual-written to physical disk. Primary table data (C<.db>) rema...

=item * B<Tier 2 (Full Table Mirror, C<use_ramdisk =E<gt> 2>):> Both primary records (C<.db>) and all index files are mirrored on RAM-disk. Reads are served directly from RAM-disk with synchronous dual-writing to permanent storage.

=item * B<Tier 3 (Volatile In-Memory, C<use_ramdisk =E<gt> 3>):> Operates purely in RAM-disk as an unindexed simple store (C<use_simple =E<gt> 1>) with zero physical disk files. Ideal for ephemeral sessions, shopping carts, and temporary tokens. Supp...

=item * B<Tier 4 (Asynchronous Write-Behind, C<use_ramdisk =E<gt> 4>):> Primary records and indexes are updated immediately in RAM-disk for maximum throughput. Write operations append dirty sync events to C<journal/sync_ramdisk>, and a background dae...

=back

=head2 Transparent Management (C<use_ramdisk>)

AmberDB manages the RAM-disk layer entirely in the background. Developers do not need to call any low-level RAM-disk functions or manually orchestrate memory synchronization. Everything is controlled declaratively via the C<use_ramdisk> option:

=over 4

=item * B<Global Configuration:> Set globally at instantiation (C<AmberDB-E<gt>new(cfg =E<gt> { use_ramdisk =E<gt> 1 })>) or at runtime via C<$adb-E<gt>config(use_ramdisk =E<gt> 2)>.

=item * B<Per-Table Customization:> Set independently for each table in its C<.table> schema or via C<$adb-E<gt>table_attr($table, use_ramdisk =E<gt> $tier)>.

=item * B<Standard CRUD Workflow:> Continue using standard AmberDB methods (C<read_id>, C<search_table>, C<insert_id>, C<modify_id>). The engine automatically reads from memory at microsecond speeds and dual-writes to permanent storage.

=item * B<Windows & Linux Administration:> On Windows, use C<bin\setup_windows.bat> to mount or unmount the ImDisk RAM-disk. On Linux, C</dev/shm> is utilized automatically:

  - Mount RAM-disk (Windows): bin\setup_windows.bat start 512M R:
  - Status check (Windows):   bin\setup_windows.bat status
  - Background Sync Daemon:   perl bin/amberdb_daemon.pl start

=back

=head1 COMMAND-LINE TOOLS (CLI)

AmberDB provides standalone command-line utilities in its C<bin/> directory for management, queries, and background synchronization:

=over 4

=item * B<amberdb_cli.pl:> Management console and interactive query utility. Supports token-based session lifecycles, database dashboards, CRUD operations, dynamic schema mutations (C<table_attr>), CSV import/export, index rebuilding (C<reindex>), an...

=item * B<setup_windows.bat:> Windows ImDisk RAM-disk lifecycle manager (mount, unmount, permission configuration, status monitoring).

=item * B<amberdb_daemon.pl:> Background journal flusher and write-behind synchronization worker for Tier 4 acceleration.

=back

=head1 METHODS

=head2 new(%options)

Instantiates a new C<AmberDB> object.

    my $adb = AmberDB->new(
        cfg  => { language => "gb" },
        path => { dbase_dir => "./dbstore" },
    );

=head2 config([$key], [%options])

Gets or sets runtime configuration flags deterministically with automatic hook/side-effect dispatching (e.g. locale reloading, table path invalidation):

    # Single scalar getter
    my $lang = $adb->config('language');

    # Bulk getter (returns a safe shallow copy)
    my $cfg = $adb->config();

    # Key-value setter with method chaining
    $adb->config( language => 'gb', no_write => 1 );

    # Hashref setter
    $adb->config({ simple => 1, ramdisk_size => '1024M' });

=head2 insert_id($table_id, [$record_id], @record)

Inserts a new record into specified table. It automatically generates search, match, slug, and facet indexes if they are defined in the table schema. It supports transact operations. In normal records, there is no need to enter an ID value. It can be...

    # Auto-increment primary key ID (pass 0 or undef)
    my $new_id = $adb->insert_id("catalog_product", 0, "Widget Pro", "Electronics", 1250);



( run in 0.828 second using v1.01-cache-2.11-cpan-4716db65a44 )