## Name
Database::Abstraction - Read-only Database Abstraction Layer (ORM)
## Version
Version 0.50
## Description
`Database::Abstraction` is a read-only ORM for Perl that gives a uniform
interface over CSV, PSV, TSV, JSON, XML, SQLite, DBM::Deep, BerkeleyDB, and
Excel (XLS/XLSX) files - local, remote (via SSH), or fetched from a URL -
without writing any SQL.
Effectively it allows you to access a database table, of many different
database formats, as an object.
Key features:
- **No SQL required.** Use plain Perl method calls for simple lookups and
scans; switch storage formats without changing application code.
- **Rich query criteria.** Pass plain values, SQL wildcards, `undef` (IS NULL),
comparison operators (`>` `<` `>=` `<=` `!=`), pattern
operators (`-like`, `-not_like`), set operators (`-in`, `-not_in`,
`-between`), and logical groupings (`-or`, `-and`).
- **Automatic joins.** Add a `join` parameter to any select method to
combine tables with INNER, LEFT, RIGHT, FULL, or CROSS joins.
- **Chained query builder.** The `query()` method returns a
[Database::Abstraction::Query](https://metacpan.org/pod/Database%3A%3AAbstraction%3A%3AQuery) object for fluent, composable queries:
`$db->query->where(...)->order_by(...)->limit(...)->all()`.
- **Aggregation.** The `aggregate()` method and the fluent
`->group_by()->measure()->having()->all()` chain push `GROUP BY` to
SQL backends and fold rows in Perl on slurp-only backends.
- **Schema introspection.** `columns()` lists column names; `schema()`
returns full type/nullability metadata, using native driver introspection
(`PRAGMA table_info` for SQLite, `column_info` for others).
- **DSN portability.** Pass a `dsn` (plus optional `username`/`password`)
to connect to any DBI-supported database (SQLite, PostgreSQL, MySQL, ...)
instead of pointing at a local file.
- **Performance.** Small files are slurped into a RAM hash for sub-millisecond
lookups. All DBI statement handles are cached with `prepare_cached()`.
A CHI-compatible cache layer is also supported.
## Synopsis
```perl
# 1. Create a thin subclass for your table (e.g. Database/Foo.pm)
package Database::Foo;
use parent 'Database::Abstraction';
# 2. Open the database - file is auto-detected from the class name
# (looks for foo.sql / foo.sqlite / foo.sqlite3 / foo.psv / foo.tsv / foo.csv / foo.xlsx / foo.xml / foo.json / foo.db)
my $db = Database::Foo->new(directory => '/path/to/data');
# 3. Simple lookups -----------------------------------------------
# Fetch one row
my $row = $db->fetchrow_hashref(entry => 'key1');
# Fetch all rows matching a criterion
my $rows = $db->selectall_arrayref(status => 'active');
# Column shortcut via AUTOLOAD
my $name = $db->name(entry => 'key1');
# 4. Rich criteria ------------------------------------------------
# Comparison operators
my $high = $db->selectall_arrayref(score => { '>' => 90 });
# Set membership
my $selected = $db->selectall_arrayref(
name => { -in => ['Alice', 'Bob'] }
);
# Range
my $mid = $db->selectall_arrayref(
score => { -between => [60, 80] }
);
# OR grouping
my $either = $db->selectall_arrayref(
-or => [
{ status => 'active' },
{ score => { '>' => 95 } },
]
);
# 5. Joins --------------------------------------------------------
my $joined = $db->selectall_arrayref(
join => { table => 'dept', on => 'foo.dept_id = dept.id', type => 'LEFT' }
);
# 6. Chained query builder ----------------------------------------
my $results = $db->query
->where(status => 'active')
->where(score => { '>=' => 80 })
->order_by('score DESC')
->limit(10)
->all();
my $first = $db->query->where(name => 'Alice')->first();
my $count = $db->query->where(status => 'active')->count();
# 7. Connect via DSN (PostgreSQL, MySQL, SQLite, ...) ---------------
my $db2 = Database::Foo->new(
dsn => 'dbi:Pg:dbname=mydb;host=db.example.com',
username => 'myuser',
password => 's3cret',
);
# 8. Schema introspection -----------------------------------------
my $cols = $db->columns(); # ['entry', 'name', 'score', ...]
my $schema = $db->schema(); # { name => { type=>'TEXT', nullable=>1, ... }, ... }
# 9. Aggregation (GROUP BY) ----------------------------------------
# Total revenue and order count per region
my $summary = $db->aggregate(
by => ['region'],
measures => [
{ col => 'amount', fn => 'sum', as => 'revenue' },
{ fn => 'count', as => 'orders' },
],
);
# Grand total across all rows
my $totals = $db->aggregate(
measures => [{ col => 'amount', fn => 'sum', as => 'total' }],
)->[0];
# Fluent style with HAVING and sort
my $top = $db->query
->group_by('region')
->measure('amount', 'sum', 'revenue')
->measure(undef, 'count', 'orders')
->having(revenue => { '>' => 1000 })
->order_by('revenue DESC')
->limit(5)
->all();
```
## Quick Start Example
If `/var/dat/foo.csv` contains:
```
"customer_id","name"
"plugh","John"
"xyzzy","Jane"
```
Create a driver in `.../Database/foo.pm`:
```perl
package Database::foo;
use parent 'Database::Abstraction';
# Regular CSV: no entry column, comma-separated
sub new {
my ($class, %args) = @_;
return $class->SUPER::new(no_entry => 1, sep_char => ',', %args);
}
```
Then query it:
```perl
my $foo = Database::foo->new(directory => '/var/dat');
# Prints "John"
print 'Customer: ', $foo->name(customer_id => 'plugh'), "\n";
# Returns { customer_id => 'xyzzy', name => 'Jane' }
my $row = $foo->fetchrow_hashref(customer_id => 'xyzzy');
```
## File Formats
The module probes the `directory` for files in this priority order:
- 1. `SQLite`
File ending `.sql`, `.sqlite`, or `.sqlite3`.
Requires [DBD::SQLite](https://metacpan.org/pod/DBD%3A%3ASQLite).
- 2. `Deep`
DBM::Deep file ending `.dbm` or `.deep`. The entire file is slurped
into a plain Perl hash on open; all in-memory fast-paths apply.
Requires [DBM::Deep](https://metacpan.org/pod/DBM%3A%3ADeep) (loaded lazily).
- 3. `PSV`
Pipe-separated file, ending `.psv`.
- 4. `TSV`
Tab-separated file, ending `.tsv`.
- 5. `CSV`
Comma (or custom) separated file, ending `.csv` or `.db`; can be
gzipped (`.csv.gz` or `.db.gz`).
**Note:** the default separator is `!` not `,` for historical
reasons - pass `sep_char => ','` for standard CSVs.
Requires [Text::xSV::Slurp](https://metacpan.org/pod/Text%3A%3AxSV%3A%3ASlurp) for the slurp fast-path (loaded lazily).
- 6. `Excel` (`.xls`) and `XLSX` (`.xlsx`)
Two separate Excel backends - one per file format:
- **.xls** - old binary format, opened via [DBD::Excel](https://metacpan.org/pod/DBD%3A%3AExcel) (which uses
[Spreadsheet::ParseExcel](https://metacpan.org/pod/Spreadsheet%3A%3AParseExcel) internally). All queries go through DBI/SQL;
no in-memory slurp path. `max_slurp_size` has no effect.
- **.xlsx** - modern OOXML format, parsed directly via
[Spreadsheet::ParseXLSX](https://metacpan.org/pod/Spreadsheet%3A%3AParseXLSX) and slurped into an in-memory hash (keyed mode)
or array (`no_entry` mode). No DBI handle is created; all queries use
the in-memory fast-path. Complex criteria (operator hashes, `-or`/`-and`)
will fall through to the SQL path and croak - use simple scalar criteria.
For both formats, each worksheet is a separate logical table; the active
worksheet is determined by the class-derived table name (or the `table`
constructor parameter). Both modules are loaded lazily.
- 7. `XML`
File ending `.xml`.
Requires [XML::Simple](https://metacpan.org/pod/XML%3A%3ASimple) for the slurp fast-path (loaded lazily).
- 8. `JSON`
File ending `.json`, slurped into memory via [JSON::MaybeXS](https://metacpan.org/pod/JSON%3A%3AMaybeXS) (loaded
lazily).
The file may contain either a JSON array of row objects:
```
[
{ "entry": "key1", "col": "val1" },
{ "entry": "key2", "col": "val2" }
]
```
or a JSON object whose keys are the primary-key values:
```
{
"key1": { "col": "val1" },
"key2": { "col": "val2" }
}
```
In the object form, each key is injected into its row hash under the `id`
column name (default `entry`), so all normal lookups work identically to
the array form.
A zero-byte or whitespace-only file is treated as empty - all query methods
return 0 / `undef` / `[]` without throwing.
Requires [JSON::MaybeXS](https://metacpan.org/pod/JSON%3A%3AMaybeXS) (loaded lazily).
- 9. `BerkeleyDB`
Binary key-value file ending `.db`.
- 10. `HTML`
HTML page fetched via a `url`. Pass `url =` 'https://...'> instead of
`directory`; the module fetches the page with [LWP::UserAgent::Cached](https://metacpan.org/pod/LWP%3A%3AUserAgent%3A%3ACached), parses all
`
` elements with [HTML::TableExtract](https://metacpan.org/pod/HTML%3A%3ATableExtract), and slurps the first (or
`html_table_index`-selected) table into memory. The first row of the table
is treated as column headers. Both modules are loaded lazily and are not
required for other backends.
Pass `dsn` to bypass file detection entirely and connect via any DBI driver.
Pass `url` to fetch and slurp data from a remote source without a local
directory. When the URL returns `Content-Type: application/json` or the URL
path ends in `.json`, the response is parsed as JSON (see item 8 above).
Otherwise the response is parsed as an HTML page (item 10).
Example - fetching CPAN Testers results:
```perl
package Database::cpantesters;
use parent 'Database::Abstraction';
my $db = Database::cpantesters->new(
url => 'https://www.cpantesters.org/show/Database-Abstraction.json',
no_entry => 1,
);
my $passes = $db->selectall_arrayref(grade => 'PASS');
```
## Query Criteria
All select methods (`selectall_arrayref`, `selectall_array`,
`fetchrow_hashref`, `count`) accept the same criteria syntax.
### Plain Value
```perl
status => 'active' # status = 'active'
name => undef # name IS NULL
```
Values containing `%` or `_` are matched with `LIKE`:
```perl
name => 'A%' # name LIKE 'A%'
```
### Comparison Operator Hashref
```perl
score => { '>' => 90 } # score > 90
score => { '<' => 50 } # score < 50
score => { '>=' => 80 } # score >= 80
score => { '<=' => 100 } # score <= 100
score => { '!=' => 0 } # score != 0
```
Multiple operators on one column are ANDed:
```perl
score => { '>' => 60, '<' => 90 } # 60 < score < 90
```
### Pattern Matching
```perl
name => { -like => 'A%' } # name LIKE 'A%'
name => { -not_like => 'Z%' } # name NOT LIKE 'Z%'
```
### Set Membership
```perl
name => { -in => ['Alice', 'Bob'] } # name IN (...)
name => { -not_in => ['Alice', 'Bob'] } # name NOT IN (...)
```
### Range
```perl
score => { -between => [60, 90] } # score BETWEEN 60 AND 90
```
### Logical Groupings
`-or` and `-and` take an arrayref of condition hashrefs:
```perl
-or => [
{ status => 'active' },
{ score => { '>' => 95 } },
]
-and => [
{ status => 'active' },
{ score => { '>=' => 80 } },
]
```
### Joins
Any select method accepts a `join` key with a hashref (or arrayref of
hashrefs) describing the join:
```perl
join => {
table => 'dept',
on => 'employees.dept_id = dept.id',
type => 'LEFT', # INNER (default) | LEFT | RIGHT | FULL | CROSS
}
# Multiple joins
join => [
{ table => 'dept', on => 'e.dept_id = dept.id' },
{ table => 'country', on => 'e.country_id = country.id' },
]
```
## Subroutines/Methods
### Init
Set class-level defaults shared by all instances.
```perl
Database::Abstraction::init(directory => '../data');
```
Accepts the same parameters as ["new"](#new). Returns a reference to the
current defaults hash, so you can read them back:
```perl
my $defaults = Database::Abstraction::init();
print $defaults->{'directory'}, "\n";
```
### Import
The module can be initialised by the `use` directive.
```perl
use Database::Abstraction 'directory' => '/etc/data';
```
or
```perl
use Database::Abstraction { 'directory' => '/etc/data' };
```
### New
Create an object pointing to a read-only database.
Accepts arguments as a hash, a hashref, or - as a shortcut - a single bare
string which is taken to be `directory`.
#### Connection Parameters
- `directory`
Directory containing the data files. The module probes this directory for
files named after the subclass (see ["FILE FORMATS"](#file-formats)). Required unless
`dsn` is given.
- `dsn`
A DBI data-source string (e.g. `dbi:SQLite:dbname=/path/to/db` or
`dbi:Pg:dbname=mydb;host=db.example.com`). When present, file detection
is skipped entirely and the DSN is used directly. The SQL dialect is
inferred from the DSN prefix (`sqlite`, `postgres`, `mysql`).
- `username`
Database username. Used only with `dsn`; ignored for file-based backends.
- `password`
Database password. Used only with `dsn`; ignored for file-based backends.
- `dbname`
Override the filename stem searched in `directory` (default: the table
name derived from the class name).
- `table`
Override the table (or worksheet) name used in SQL queries for this object.
Default is the class-name-derived table name (e.g. `Database::Foo` =>
`foo`). Particularly useful for Excel workbooks where a single `.xlsx`
file contains multiple worksheets: pass `table => 'Summary'` to query
the `Summary` worksheet without creating a dedicated subclass. Also works
with SQLite/DSN connections to select a table other than the class-derived
default. The filename stem (`dbname`) continues to fall back to the class
name, so the correct file is opened regardless of this override. The value
is validated against `$SAFE_QUALIFIED` at construction time.
- `filename`
Override the full filename (relative to `directory`). Takes precedence
over `dbname`.
- `host`
Remote hostname (or `user@host`) from which to fetch the data file(s) via
SSH/SCP. When present, each candidate filename is fetched with
[File::Slurp::Remote](https://metacpan.org/pod/File%3A%3ASlurp%3A%3ARemote) into a local temporary directory; the existing
extension-based file-type detection then runs against that directory.
`directory` is treated as the remote path (no local canonicalization is
applied). Using `filename` together with `host` avoids probing multiple
extensions and is therefore more efficient. [File::Slurp::Remote](https://metacpan.org/pod/File%3A%3ASlurp%3A%3ARemote) must be
installed; it is loaded lazily (only when `host` is given).
- `url`
A URL (`http://` or `https://`) pointing to an HTML page that contains one
or more `` elements. When present, `directory` is not required.
The first row of the selected table is used as column headers.
Requires [LWP::UserAgent::Cached](https://metacpan.org/pod/LWP%3A%3AUserAgent%3A%3ACached) and [HTML::TableExtract](https://metacpan.org/pod/HTML%3A%3ATableExtract) (both loaded lazily).
- `database`
A Redis connection URL in the form `redis://[password@]host[:port][/db_index]`.
When present, `directory`, `dsn`, and `url` are not required. Rows are
expected to be stored as Redis Hashes at keys of the form
`tablename:entry_value`; fields of each Hash become column values. The
module slurps all matching keys via `KEYS tablename:*` + `HGETALL` into the
same in-memory structure used by the CSV and DBM::Deep backends, so all
existing fast-paths (`selectall_arrayref`, `selectall_array`,
`fetchrow_hashref`, `count`, `columns`, `schema`, AUTOLOAD) work without
modification. `no_entry` mode is supported.
[Redis::Fast](https://metacpan.org/pod/Redis%3A%3AFast) is tried first (XS, faster); if unavailable, [Redis](https://metacpan.org/pod/Redis)
(pure-Perl) is used as a fallback. Both are loaded lazily - neither is
required when using file-based or DSN-based backends. The URL scheme is
validated at construction time (`redis://` only; all other schemes croak).
Use ["select"](#select) to switch the active Redis database on an already-open
connection.
#### Behaviour Parameters
- `no_entry`
Set to `1` when the table has no key column (standard CSVs, for example).
Default is `0` (keyed on `entry`).
- `id`
Name of the key column. Default is `entry`.
- `sep_char`
Field separator for CSV/PSV files.
Default is `!` - pass `sep_char => ','`
for standard comma-separated files.
- `max_slurp_size`
Files smaller than this (in bytes) are loaded entirely into memory for fast
lookups. Default is 16 KB. Set to `0` to force SQL mode for all sizes.
- `no_fixate`
Set to `1` to return mutable arrays. Default is `0` (arrays are made
read-only via [Data::Reuse](https://metacpan.org/pod/Data%3A%3AReuse)).
- `auto_load`
Set to `0` to disable the AUTOLOAD column shortcut. Default is `1`
(enabled).
- `html_table_index`
Zero-based index of the HTML `` to extract when the `url`
backend is used. Default is `0` (the first table on the page).
- `infer_types`
Set to `1` to enable heuristic type inference in `schema()` for
slurp-backed sources (CSV, JSON, XLSX, XML, HTML URL, DBM::Deep).
Default is `0` (off; all columns reported as `TEXT` for backward
compatibility).
When enabled, `schema()` scans the first 100 rows and promotes column
types: all-integer values => `INTEGER`; all-floating-point values =>
`REAL`; all ISO-8601 timestamps => `TIMESTAMP`; all ISO-8601 dates =>
`DATE`; otherwise `TEXT`. `NULL` (`undef` or empty string) values
are skipped during the scan.
- `base_criteria`
A hashref of column-value pairs that is ANDed into **every** SELECT
automatically. Callers do not need to repeat these criteria on each
call. Common use cases:
- Row-level security: `base_criteria => { tenant_id => $tid }`
- Soft-delete filtering: `base_criteria => { deleted_at => undef }`
- Status gates: `base_criteria => { active => 1 }`
The hashref is shallow-copied at construction time so later mutations to
the original do not affect the object. Column names are validated
against the same identifier rules as the `id` parameter. `-or` and
`-and` groupings are allowed as keys.
**SQL path**: the criteria are merged into the `WHERE` clause before
the caller-supplied criteria (caller values win on key collision).
**Slurp/in-memory path**: the criteria are applied as an additional
filter. The `fetchrow_hashref` entry-key fast path checks
`base_criteria` against the returned row before returning it.
**Query builder**: `base_criteria` are merged into the `WHERE` clause
generated by `_build_sql`, so `$db->query->where(...)->all()`
also respects them.
#### Caching and Logging
- `cache`
A [CHI](https://metacpan.org/pod/CHI)-compatible cache object. When set, query results are stored and
retrieved from the cache.
- `cache_duration` / `expires_in`
TTL for cached results. Default is `'1 hour'`. `expires_in` is a
synonym for compatibility with [CHI](https://metacpan.org/pod/CHI).
- `logger`
An object that understands `warn()` and `trace()` (e.g.
[Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl), [Log::Any](https://metacpan.org/pod/Log%3A%3AAny)), a code reference, or a filename.
- `config_file`
Path to a YAML, XML, or INI configuration file whose keys are merged into
the constructor arguments. Loaded via [Object::Configure](https://metacpan.org/pod/Object%3A%3AConfigure).
#### Notes
- If no arguments are set, class-level defaults set via `init()` or `use`
are used.
- Slurp mode assumes the key column (`entry`) is unique. If it is not,
searches will be incomplete - disable slurp mode by setting
`max_slurp_size => 0`.
- Passing an existing object as `$class` clones it, merging any new
arguments.
### Set\_Logger
Sets the class, code reference, or file that will be used for logging.
### Select
Select a Redis database by number (0-15). Clears the in-memory data cache so
the next query re-slurps from the newly selected database. Returns `$self`
for chaining. Croaks if the object is not backed by a Redis connection.
```perl
$db->select(3); # switch to Redis DB 3
my $count = $db->count; # queries the new DB
```
### Selectall\_Arrayref
Returns a reference to an array of hash references for every row that
matches the given criteria, or `undef` when there are no matches.
```perl
my $rows = $db->selectall_arrayref(); # all rows
my $rows = $db->selectall_arrayref(status => 'active'); # exact match
my $rows = $db->selectall_arrayref(score => { '>' => 8 }); # operator
```
The full criteria syntax is described in ["QUERY CRITERIA"](#query-criteria).
Pass a `join` key to combine with another table:
```perl
my $rows = $db->selectall_arrayref(
dept_name => 'Engineering',
join => { table => 'dept', on => 'e.dept_id = dept.id' },
);
```
Pass `limit => N` and/or `offset => M` for pagination:
```perl
my $page = $db->selectall_arrayref(status => 'active', limit => 10, offset => 20);
```
Both values must be non-negative integers; invalid values are ignored with a
`carp` warning. When `offset` is given without `limit` the SQL backend
uses `LIMIT -1` on SQLite (meaning "no upper bound") so the `OFFSET` clause
is legal.
Pass `sort_by => 'col'` (ascending) or `sort_by => ['col', 'DESC']`
to request a specific sort column and direction instead of the default primary-key
ordering:
```perl
my $rows = $db->selectall_arrayref(sort_by => 'name');
my $rows = $db->selectall_arrayref(sort_by => ['score', 'DESC']);
```
The column name is validated against the same identifier rules as all other
column parameters. An unsafe name or an unrecognised direction (anything other
than `ASC` or `DESC`, case-insensitive) is ignored with a `carp` warning and
the default sort order is used instead. Sorting is applied before
`limit`/`offset` pagination.
Results are returned in the cache (if configured) and the returned array
reference is made read-only unless `no_fixate` was set.
**Note:** this always returns all matching rows. Use ["selectall\_array"](#selectall_array)
in scalar context, or `$db->query->limit(1)->all()`, to fetch just one row.
#### Pseudocode
```
1. Parse criteria; extract and build any JOIN clause.
2. If data is slurped AND no joins AND criteria are simple:
a. No criteria -> return all rows as arrayref.
b. entry-only lookup -> return [$data{entry}].
c. Otherwise -> scan rows in-memory with _match_criterion.
In all slurp cases: sort by sort_by column (if given), then apply offset/limit.
3. Otherwise build SQL: SELECT * FROM table [JOIN] [WHERE]
ORDER BY sort_by [else id] [LIMIT] [OFFSET].
4. Check cache; return cached arrayref on HIT.
5. prepare_cached + execute; fetch all rows.
6. Store result in cache; fixate the array; return arrayref.
```
#### Api Specification
##### Arguments
```perl
%criteria # optional key-value pairs or operator hashrefs
join => HashRef | ArrayRef # optional join spec(s)
sort_by => Str | ArrayRef # optional: 'col' or ['col','DESC']
limit => Int # optional: max rows
offset => Int # optional: rows to skip
no_fixate => Bool # optional: skip read-only locking
table => Str # optional: override table name
```
##### Returns
```
ArrayRef[HashRef] | undef
```
An arrayref of row hashrefs on success. Returns `undef` when no
rows match. Each hashref is read-only unless `no_fixate` was set.
### Selectall\_Hashref
Deprecated alias for ["selectall\_arrayref"](#selectall_arrayref). Use `selectall_arrayref` in
new code.
### Each\_Row
```perl
$db->each_row(\&callback);
$db->each_row(\&callback, status => 'active');
$db->each_row(\&callback, sort_by => 'name', limit => 100, offset => 20);
```
Iterates over matching rows one at a time, calling `\&callback` once per
row with the row hashref as the sole argument. Uses **constant memory** on
the SQL path: rows are fetched from the database one at a time via
`fetchrow_hashref` without materialising the full result array. On the
slurp/in-memory path data is already in RAM, so memory use is equivalent to
["selectall\_arrayref"](#selectall_arrayref).
Accepts the same criteria, `join`, `sort_by`, `limit`, and `offset`
parameters as ["selectall\_arrayref"](#selectall_arrayref).
Returns the number of rows passed to `\&callback`.
Exceptions raised inside `\&callback` abort iteration and propagate to
the caller; the DBI statement handle is left in a valid state (`finish()`
is called on exception).
**Note:** rows on the SQL path are not fixated (made read-only) because they
are discarded after each callback invocation. Slurp-path rows are already
fixated from the initial load.
#### Api Specification
##### Arguments
```perl
CodeRef # required: callback receiving one row hashref per call
%criteria # optional: same criteria syntax as selectall_arrayref
join => HashRef | ArrayRef # optional join spec(s)
sort_by => Str | ArrayRef # optional sort column
limit => Int # optional: max rows to visit
offset => Int # optional: rows to skip
```
##### Returns
```
Int (>= 0)
```
The number of rows visited (i.e. the number of times the callback
was called). Any exception thrown inside the callback is re-thrown
after the DBI statement handle is properly finished.
### Selectall\_Array
Similar to ["selectall\_arrayref"](#selectall_arrayref) but returns a list of hash references
rather than a reference to an array.
```perl
my @rows = $db->selectall_array(status => 'active');
```
In **scalar context** it applies `LIMIT 1` and returns just the first
matching hash reference - making it more efficient than `selectall_arrayref`
when you only need one row. In **list context** all matching rows are returned.
Accepts the same criteria, `join`, `limit`, `offset`, and `sort_by`
parameters as ["selectall\_arrayref"](#selectall_arrayref). When `limit` is given in scalar context
it overrides the implicit `LIMIT 1`.
### Selectall\_Hash
Deprecated alias for ["selectall\_array"](#selectall_array). Use `selectall_array` in new
code.
### Count
Returns the number of rows matching the given criteria.
```perl
my $total = $db->count();
my $active = $db->count(status => 'active');
my $high = $db->count(score => { '>' => 90 });
```
Accepts the full criteria syntax described in ["QUERY CRITERIA"](#query-criteria).
#### Api Specification
##### Arguments
```perl
%criteria # optional: same criteria syntax as selectall_arrayref
join => HashRef | ArrayRef # optional join spec(s)
table => Str # optional: override table name
```
##### Returns
```
Int (>= 0)
```
The number of rows that match the given criteria. Returns `0`
when no rows match -- never `undef`.
### Fetchrow\_Hashref
Returns a hash reference for the first row matching the given criteria,
or `undef` when there is no match. Always applies `LIMIT 1`.
```perl
my $row = $db->fetchrow_hashref(entry => 'key1');
my $row = $db->fetchrow_hashref(score => { '>=' => 10 });
```
When `no_entry` is **not** set you may pass a single bare value and it is
used as the `entry` key:
```perl
my $row = $db->fetchrow_hashref('key1'); # same as entry => 'key1'
```
Accepts the full criteria syntax described in ["QUERY CRITERIA"](#query-criteria), including
the `join` parameter:
```perl
my $row = $db->fetchrow_hashref(
name => 'Alice',
join => { table => 'dept', on => 'e.dept_id = dept.id' },
);
```
Pass `table => $other_table` to query a table other than the one
derived from the class name.
#### Api Specification
##### Arguments
```perl
entry => Str # primary-key value to look up (positional first
# arg when no_entry is false)
%criteria # optional additional column filters
join => HashRef | ArrayRef # optional join spec(s)
no_fixate => Bool # optional: skip read-only locking
table => Str # optional: override table name
```
##### Returns
```
HashRef | undef
```
A single row hashref when exactly one matching row is found. Returns
`undef` when the key is not found or no row matches the criteria.
The hashref is read-only unless `no_fixate` was set.
**Note:** `fetchrow_hashref` does **not** croak when multiple rows
match the given criteria; it returns whichever row the backend
delivers first (usually the first row in file/insertion order).
### Execute
Execute a raw SQL query on the underlying database.
```perl
# Scalar context: returns the first row as a hashref
my $row = $db->execute(query => 'SELECT * FROM foo WHERE id = 1');
# List context: returns all rows as a list of hashrefs
my @rows = $db->execute(query => 'SELECT * FROM foo WHERE score > ?',
args => [80]);
```
The `FROM ` clause is appended automatically if omitted.
On CSV tables without `no_entry` it may help to add
`WHERE entry IS NOT NULL AND entry NOT LIKE '#%'` to filter comment rows.
If the data have been slurped into memory this method still hits the actual
database file directly.
`args` is an arrayref of bind values (see ["execute" in DBI](https://metacpan.org/pod/DBI#execute)).
### Updated
Returns the Unix timestamp of the last database update.
For file-based backends (CSV, XML, SQLite via `directory`), this is the
mtime of the backing file, set at `new()` time.
For SQLite DSN connections (`dbi:SQLite:dbname=...`), the file path is
extracted from the DSN and `stat()`-ed live on every call, so callers
get a current mtime suitable for cache-invalidation even when the database
was opened via a DSN rather than a `directory`.
For all other DSN-based connections (PostgreSQL, MySQL, etc.) and for
URL-based backends, returns the Unix timestamp of the most recent
`new()` call (connection time).
### Columns
Returns an array reference of column names for the current table.
```perl
my $cols = $db->columns(); # e.g. ['entry', 'name', 'score', 'status']
```
Column names are always returned in alphabetical (lexicographic) order,
regardless of backend. This makes the result stable and portable when the
same logical table is backed by different engines (CSV => SQLite, etc.).
The source of column names varies by backend:
- **Slurp mode** - keys of the first row in memory.
- **SQLite / other DBI** - a zero-row `SELECT *` exposes the driver's
`NAME` attribute.
- **BerkeleyDB** - always returns `['entry', 'value']`.
The result is cached inside the object after the first call.
#### Api Specification
##### Arguments
None beyond the implicit invocant. The result is cached in the
object after the first call.
##### Returns
```
ArrayRef[Str]
```
A sorted (lexicographic, ascending) arrayref of column names. The
sort order is identical across all backends, so switching from CSV
to SQLite does not change the returned order.
### Schema
Returns a hash reference describing the schema of the current table.
Each key is a column name; each value is a hash reference with these keys:
- `type` - data type string (e.g. `TEXT`, `INTEGER`, `REAL`)
- `nullable` - `1` if the column may be NULL, `0` if NOT NULL
- `default` - default value string, or `undef`
- `pk` - `1` if this column is (part of) the primary key, `0` otherwise
```perl
my $schema = $db->schema();
for my $col (sort keys %{$schema}) {
my $info = $schema->{$col};
printf "%s %s %s\n",
$col,
$info->{type},
$info->{nullable} ? 'NULL' : 'NOT NULL';
}
```
The schema is determined by the backend:
- **SQLite** - `PRAGMA table_info(table)`
- **Other DBI drivers** - `$dbh->column_info(...)`
- **Slurp mode** - inferred from the first row; all columns typed as
`TEXT` by default. When `infer_types => 1` was passed to the
constructor, up to 100 rows are scanned per column and the type is
promoted: all-integer values => `INTEGER`; all-float values =>
`REAL`; all ISO-8601 timestamps (`YYYY-MM-DDThh:mm`) => `TIMESTAMP`;
all ISO-8601 dates (`YYYY-MM-DD`) => `DATE`; otherwise `TEXT`.
`undef` and empty-string values are treated as SQL `NULL` and skipped
during the scan.
- **BerkeleyDB** - always returns `entry` (pk) and `value`
The result is cached inside the object after the first call.
#### Api Specification
##### Arguments
```perl
infer_types => Bool # enable heuristic type inference for slurp backends
# (may also be set at construction time)
```
##### Returns
```perl
HashRef[Str => HashRef]
```
A hashref keyed by column name. Each value is a hashref with:
```perl
type => Str # 'TEXT', 'INTEGER', 'REAL', 'TIMESTAMP', 'DATE'
nullable => Bool # 1 if the column may be NULL, 0 if NOT NULL
default => Str | undef # default value, or undef
pk => Bool # 1 if (part of) the primary key
```
For slurp-mode backends all columns default to type `TEXT` unless
`infer_types` is set. The result is cached in the object after the
first call.
### Dbi\_Source
Returns a hashref `{ dbh => $dbh, table => $name }` when the backend
is a live SQLite connection, or `undef` for every other backend (slurp-mode
CSV, JSON, XLSX, HTML URL, DBM::Deep, BerkeleyDB, PostgreSQL, MySQL, ...).
The hashref is consumed by `Database::Join` to perform a zero-copy
`ATTACH DATABASE` so rows never pass through Perl.
Nested `Database::Join` objects that are themselves SQLite-backed expose themselves
as attachable sources to parent joins through the same interface.
Subclasses may override this method to expose non-SQLite DBI connections
if their join layer supports them.
#### Api Specification
##### Arguments
None beyond the implicit invocant.
##### Returns
A hashref `{ dbh => DBI::db, table => Str }` on a SQLite-backed
instance, or `undef` on all other backends.
### Aggregate
Groups matching rows and computes aggregate measures over each group.
Pushes `GROUP BY` SQL to the backend when a DBI handle is available;
otherwise folds rows in Perl (BerkeleyDB, DBM::Deep, XLSX, JSON, HTML, Redis).
```perl
my $rows = $db->aggregate(
by => ['region', 'month'],
measures => [
{ col => 'amount', fn => 'sum', as => 'revenue' },
{ fn => 'count', as => 'orders' },
],
status => 'active',
);
# Grand total (no 'by')
my $row = $db->aggregate(
measures => [{ col => 'score', fn => 'avg', as => 'mean_score' }],
)->[0];
```
Reserved parameter keys (extracted before the rest is treated as WHERE criteria):
`by`, `measures`, `having`, `sort_by`, `limit`, `offset`, `table`, `join`.
Always returns an **arrayref of hashrefs** (never `undef`); returns `[]` when
no group qualifies. Row keys are the `by` column names (in declaration order)
followed by the measure aliases.
#### Measure Specification
Each measure hashref:
```perl
{ col => 'amount', fn => 'sum', as => 'revenue' }
{ fn => 'count', as => 'orders' } # COUNT(*)
{ col => 'amount', fn => 'count', as => 'paid' } # COUNT(col)
{ col => 'customer', fn => 'count_distinct', as => 'customers' }
```
`fn` must be one of `sum`, `avg`, `min`, `max`, `count`, `count_distinct`
(case-insensitive). `col` is required for every function except plain
`count`. `as` (the result key) must be a valid SQL identifier and must not
duplicate another alias or a `by` column name.
#### Having
Pass `having => { alias_or_by_col => criterion }` to filter groups
after aggregation. Accepts the full criteria syntax.
```perl
having => { revenue => { '>' => 1000 } }
```
#### Pseudocode
```
1. _open_table; croak if JOIN requested on no-DBI backend.
2. _validate_aggregate_spec: extract/validate by/measures/having/
sort_by/limit/offset; remainder of params is WHERE criteria.
3. Merge base_criteria.
4. SQL path (DBI handle): _build_aggregate_sql; prepare_cached+execute;
normalise empty-set values; cache; fixate; return.
5. Perl fold: selectall_arrayref(criteria); _fold_aggregate;
HAVING via _match_criteria; sort; offset; limit; fixate; return.
```
#### Backend Notes
On slurp-only backends all matching rows are materialised before folding.
Use a SQL backend (SQLite DSN, PostgreSQL, MySQL) for large datasets.
#### Api Specification
##### Arguments
```perl
by => ArrayRef[Str] # optional: GROUP BY column names
measures => ArrayRef[HashRef] # required: one or more measure specs
having => HashRef # optional: post-group filter criteria
sort_by => Str | ArrayRef # optional: sort column (Str) or [col,dir]
limit => Int # optional: max rows to return
offset => Int # optional: rows to skip before returning
join => HashRef | ArrayRef # optional: join spec(s) -- SQL path only
no_fixate => Bool # optional: skip read-only locking
%criteria # optional: WHERE filter before grouping
```
Each element of `measures` is a hashref with keys:
```perl
col => Str # column to aggregate; omit only for plain count()
fn => Str # one of: sum avg min max count count_distinct
as => Str # result key name (required; valid SQL identifier)
```
##### Returns
```
ArrayRef[HashRef]
```
An arrayref of hashrefs. Each hashref has one key per `by` column
and one key per measure `as` alias. Returns `[]` (empty arrayref)
when no groups match -- never `undef`.
Empty-set normalisation: `sum`/`count`/`count_distinct` become
`0`; `avg`/`min`/`max` become `undef` when no rows are present.
### Query
Returns a new [Database::Abstraction::Query](https://metacpan.org/pod/Database%3A%3AAbstraction%3A%3AQuery) builder object bound to this
database instance, for fluent method-chaining queries.
```perl
# All active rows with high scores, newest first, max 10
my $rows = $db->query
->where(status => 'active')
->where(score => { '>' => 80 })
->order_by('score DESC')
->limit(10)
->all();
# Single row
my $row = $db->query->where(name => 'Alice')->first();
# Just a count
my $n = $db->query->where(status => 'active')->count();
```
See [Database::Abstraction::Query](https://metacpan.org/pod/Database%3A%3AAbstraction%3A%3AQuery) for the full API.
### AUTOLOAD - Column Shortcut
Calling an unknown method whose name matches a column name performs a column
lookup. The method name is the column you want; the arguments are criteria.
```perl
# Scalar context: return the first match
my $name = $db->name(entry => 'key1');
# List context: return all matching values
my @names = $db->name();
# Shortcut when the table has an 'entry' key column
my $name = $db->name('key1'); # same as name(entry => 'key1')
# Unique/distinct values
my @statuses = $db->status(distinct => 1);
```
**In list context** the full column is returned (all rows), ordered by the
column value. **In scalar context** only the first match is returned
(`LIMIT 1`).
Results come from the slurp cache when available.
Throws an error if the column does not exist (slurp mode) or if AUTOLOAD
has been disabled with `auto_load => 0`.
#### Pseudocode
```perl
1. Extract column name from $AUTOLOAD; guard on DESTROY.
2. Croak if auto_load => 0.
3. Validate $column against /^[a-zA-Z_][a-zA-Z0-9_]*$/.
4. If data is slurped:
a. List context, no params -> map column over all rows (exists guard).
b. entry-only param -> direct hash lookup (exists guard).
c. No params, scalar -> first value in hash.
d. no_entry set -> scan array for matching key/value pair.
e. Other params -> scan keyed hash for matching column.
5. If not slurped, build SQL:
- List: SELECT column FROM table [WHERE ...] ORDER BY column
- Scalar: SELECT DISTINCT column FROM table [WHERE ...] LIMIT 1
6. Check cache; return on HIT.
7. prepare_cached + execute; fetch result.
8. Store in cache; fixate; return.
```
## Author
Nigel Horne, ``
## Support
This module is provided as-is without any warranty.
Please report any bugs or feature requests to `bug-database-abstraction at rt.cpan.org`,
or through the web interface at
[http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Database-Abstraction](http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Database-Abstraction).
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.
## Messages
The table below lists every error that the module can croak or carp, what
triggers it, and how to resolve it.
- `_Class_: abstract class`
Direct instantiation of `Database::Abstraction` was attempted.
Create a subclass and instantiate that instead.
- `_Class_: where are the files?`
Neither `directory` nor `dsn` was supplied to `new()`.
- `_Class_: _/path_ is not a directory`
The `directory` argument exists on disk but is not a directory.
- `_Class_: cannot connect: _$DBI::errstr_`
DBI failed to connect to the given `dsn`. Check credentials and host.
- `Can't find a file called '_name_' for the table _T_ in _dir_`
None of the probe extensions (`.sql`, `.sqlite`, `.sqlite3`, `.psv`, `.tsv`, `.csv`, `.xlsx`, `.db`, `.xml`)
matched in `directory`.
- `_Class_: prepare failed: _$errstr_`
`prepare_cached()` returned false. Usually a syntax error in an internally
built query; file a bug if you see this from a normal API call.
- `_build_where_conditions: unsafe column name '_name_'`
A criteria key contained characters outside `[A-Za-z0-9_.]`.
This is a SQL-injection guard. Use only valid SQL identifier characters.
- `join: missing "table"` / `join: missing "on" condition`
A join spec hashref is incomplete. Both `table` and `on` are required.
- `Invalid JOIN type: _TYPE_`
`type` in a join spec was not one of `INNER LEFT RIGHT FULL CROSS`.
- `_Class_: Unknown column _col_` / `_Class_: AUTOLOAD disabled`
An AUTOLOAD call was made for a column that does not exist, or AUTOLOAD
was disabled with `auto_load => 0`.
- `Usage: set_logger(logger => $logger)`
`set_logger()` was called without a `logger` argument.
- `Usage: execute(query => $query)`
`execute()` was called without a `query` argument.
- `XML slurp: _..._ is not yet supported`
The XML file structure is too complex for slurp mode.
Use `max_slurp_size => 0` to force the DBI/XMLSimple SQL path.
- `_Class_: _method_ is meaningless on a NoSQL database`
A relational method (`selectall_arrayref`, `count`, `execute`, etc.)
was called on a BerkeleyDB backend, which only supports key-value lookup
via `fetchrow_hashref`.
- `_Class_: aggregate: 'measures' must be a non-empty arrayref`
`aggregate()` was called without a `measures` arrayref, or with an empty one.
- `_Class_: aggregate: measure must be a hashref, got _ref_`
An element of the `measures` arrayref is not a hashref.
- `_Class_: aggregate: unknown measure function '_fn_'`
`fn` must be one of `sum`, `avg`, `min`, `max`, `count`, `count_distinct`.
- `_Class_: aggregate: measure column is required for function '_fn_'`
`col` was omitted for a function that requires it. Only plain `count` (COUNT(\*)) accepts a missing `col`.
- `_Class_: aggregate: unsafe measure column '_col_'`
The `col` value is not a valid SQL identifier.
- `_Class_: aggregate: measure 'as' is required`
The `as` alias was missing from a measure hashref.
- `_Class_: aggregate: unsafe measure alias '_as_'`
The `as` value is not a valid SQL identifier.
- `_Class_: aggregate: duplicate measure alias '_as_'`
Two measure hashrefs share the same `as` alias.
- `_Class_: aggregate: measure alias '_as_' collides with a group-by column`
A measure alias duplicates a name in the `by` list.
- `_Class_: aggregate: 'by' must be an arrayref of column names`
The `by` parameter was not an arrayref.
- `_Class_: aggregate: unsafe group-by column '_col_'`
A `by` element is not a valid SQL identifier.
- `_Class_: aggregate: having key '_key_' is not a group-by column or measure alias`
A `having` key does not match any `by` column name or measure `as` alias.
- `_Class_: aggregate: sort_by names '_name_', which is not a group-by column or measure alias`
The `sort_by` column is not one of the `by` columns or measure aliases.
- `_Class_: aggregate: cannot aggregate a JOIN on the _backend_ backend`
`join` was passed to `aggregate()` on a slurp-only backend that has no DBI handle.
- `Database::Abstraction::Query: count() is not available once measure() has been called`
`count()` was called on a query builder that has measures set. Use `all()` and check `scalar @{$rows}`.
## Common Pitfalls
The following mistakes cause unexpected behaviour in common usage.
Read this section before filing a bug report.
- **Default CSV separator is `!`, not `,`**
For historical reasons the module defaults to bang (`!`) as the
field separator for CSV files. Standard RFC 4180 comma-separated
files require you to pass `sep_char => ','` in your subclass
constructor.
```perl
sub new {
my ($class, %args) = @_;
return $class->SUPER::new(sep_char => ',', %args);
}
```
- **Primary key column is named `entry`, not `key`**
The default primary-key column name is `entry` because `key` is
a reserved word in SQL. If your data uses a different column name
(e.g. `id`, `customer_id`), pass `id => 'customer_id'` to
`new()`.
- **Duplicate primary keys silently overwrite earlier rows**
In slurp mode the entire data file is loaded into a Perl hash keyed
by the `entry` column. If two rows share the same key value the
second row replaces the first. To keep duplicates either disable
slurp with `max_slurp_size => 0` or switch to a `no_entry`
schema.
- **Operator-hash criteria do not work on no-DBI backends**
No-DBI backends (XLSX, DBM::Deep, JSON, HTML URL, Redis) have no SQL
engine. Criteria that use operator hashes (e.g.
`score => { '>=' => 80 }`) trigger the SQL path and will croak
with a "meaningless on a NoSQL database" error. Use simple scalar
criteria (`score => 80`) on these backends.
- **Read-only results: do not modify returned hashrefs**
Unless you pass `no_fixate => 1`, every hashref returned by
`selectall_arrayref`, `fetchrow_hashref`, and similar methods is
made read-only by `Data::Reuse`. Attempting to assign a value to
a key in the returned hashref will throw an exception. If you need
to modify a row, either copy it first (`my %row = %{$orig_row}`) or
construct the object with `no_fixate => 1`.
- **`where()` calls in the query builder use AND semantics**
Each call to `$q->where(...)` adds an additional required
condition. Two separate `where()` calls do NOT produce OR
semantics. To express OR, pass `-or => [...]` inside a single
`where()` call:
```perl
# Wrong: this is AND, not OR
$q->where(status => 'active')->where(status => 'pending');
# Correct: OR grouping inside one where() call
$q->where(-or => [{ status => 'active' }, { status => 'pending' }]);
```
- **`aggregate()` `sort_by` must name a grouped or aliased column**
The `sort_by` parameter for `aggregate()` only accepts column names
from the `by` list or measure `as` aliases. You cannot sort
aggregate results by a raw data column that is not part of the group.
This restriction applies equally to the `->order_by()` call on a
query builder that has `measure()` methods set.
- **`sort_by` is not an argument to the query builder's `all()`**
When using the fluent query builder to build an aggregate query, set
the sort column via `->order_by('col')` before calling
`->all()`. Passing `->all(sort_by => 'col')` has no
effect because the `all()` terminal method ignores extra arguments.
- **`Params::Get` positional mapping with `no_entry = 0`**
When `no_entry` is false (the default), the first positional argument
to `selectall_arrayref` and `fetchrow_hashref` is silently mapped to
the `entry` key. Passing an operator-hash criteria in that position
(e.g. `selectall_arrayref(status =` { '!=' => 'x' })>) causes
`status` to be treated as the entry value rather than a column name,
which will produce wrong results. Always pass a hashref of criteria
as a single reference, or use `no_entry =` 1> in your subclass.
## Encoding
- **Column names** must match the pattern `[a-zA-Z_][a-zA-Z0-9_.]*`.
Only ASCII letters, digits, underscores, and dots (for qualified names
such as `table.column`) are accepted. Non-ASCII Unicode characters
in column names will cause a croak with an "unsafe column name" error.
- **Column values** (data stored in your rows) are passed through
unchanged. Full UTF-8 strings, emoji, and any valid Perl string are
safe as values in criteria and are stored and returned as-is. The
exact encoding depends on your backend:
- **SQLite / DBI backends** -- DBD::SQLite stores strings as
UTF-8 internally. Enable `sqlite_unicode => 1` in your DSN
or connect attributes if you want Perl to receive decoded Unicode
scalars rather than raw UTF-8 bytes.
- **CSV / PSV / TSV** -- Text::CSV and DBD::CSV pass bytes
through unchanged. If your file is UTF-8-encoded, strings are
returned as UTF-8 byte strings. You may need to decode them with
`Encode::decode('UTF-8', $val)` in your application.
- **JSON** -- JSON::MaybeXS decodes JSON strings to Perl Unicode
scalars automatically. UTF-8 and emoji in JSON values are fully
supported.
- **XML** -- XML::Simple decodes the file with the XML encoding
declaration. UTF-8 encoded XML (the default) produces Perl Unicode
scalars.
- **XLSX** -- Spreadsheet::ParseXLSX decodes strings to Perl
Unicode scalars. Emoji in cell values are fully supported.
- **HTML URL** -- LWP::UserAgent fetches bytes; HTML::TableExtract
returns them as-is. Decode with Encode if the page is UTF-8.
- **File paths** are handled by `File::Spec::catfile` and work
on any platform. On Linux and macOS, UTF-8 directory and file names
are fully supported.
## Known Limitations
- **Read-only.** No INSERT, UPDATE, or DELETE is provided. `execute()`
runs raw read-only SQL.
- **Default CSV separator is `!`**, not `,`, for historical reasons.
Pass `sep_char => ','` for standard RFC 4180 files.
- **Primary-key column is named `entry`**, not `key`, because `key`
is a SQL reserved word. Override with the `id` parameter.
- **XML slurp is limited.** Only simple flat XML structures are supported
in slurp mode. Multi-key or deeply nested documents will croak.
Force SQL mode with `max_slurp_size => 0` if slurp fails.
- **Unique key assumption in slurp mode.** Duplicate values in the key
column silently overwrite earlier rows. Disable slurp with
`max_slurp_size => 0` if duplicates are expected.
- **BerkeleyDB does not support joins or the chained query builder.**
- **Column names must be valid SQL identifiers** (letters, digits,
underscores, and a single dot for `table.column` join notation).
Other characters will cause a croak.
- **count() cache is opportunistic.** Count results are served from cache
only when a prior `selectall_arrayref()` or `count()` call with the
same criteria has already populated it.
## See Also
- [Database::Abstraction::Query](https://metacpan.org/pod/Database%3A%3AAbstraction%3A%3AQuery) - chained query builder
- [Configure an Object at Runtime](https://metacpan.org/pod/Object%3A%3AConfigure)
- [JSON::MaybeXS](https://metacpan.org/pod/JSON%3A%3AMaybeXS) - JSON backend (optional; install for `.json` support)
- [Test Dashboard](https://nigelhorne.github.io/Database-Abstraction/coverage/)
## Formal Specification
The following section uses Z-notation to describe the key state
transitions and invariants for the module. Z is a formal
specification language based on set theory and predicate logic.
Unicode is used here as permitted by the documentation skill.
### Database Object State
```
State == [
data : TABLE_NAME --> (ENTRY --> ROW) | seq ROW | undef,
dbh : TABLE_NAME --> DBI_HANDLE | undef,
type : Str | undef,
base_crit : CRITERIA,
_columns : seq Str | undef,
_schema : NAME --> COL_INFO | undef,
_destroyed : Bool
]
Initial state S0 == [
data? = undef,
dbh? = undef,
type? = undef,
base_crit? = {},
_columns? = undef,
_schema? = undef,
_destroyed? = false
]
```
### \_Open\_Table
```
_open_table ___________________________________________
DeltaState
class? : CLASS
dir? : DIRPATH | undef
────────────────────────────────────────────────────
pre: _destroyed = false
pre: dbh(table) = undef AND data(table) = undef
post: (EXISTS f : FILE_FORMATS WITH f.probe(dir) = true
IMPLIES (f.slurp? IMPLIES data'(table) != undef)
AND (f.dbi? IMPLIES dbh'(table) != undef))
```
### Selectall\_Arrayref
```
selectall_arrayref __________________________________
XiState (read-only; state unchanged except cache)
criteria? : CRITERIA
────────────────────────────────────────────────────
pre: _destroyed = false
result == (data != undef AND simple(criteria)
IMPLIES { r : data | match(r, criteria) }
ELSE dbh != undef
IMPLIES SQL_SELECT(dbh, criteria))
post: result : seq ROW UNION {undef}
```
### Aggregate
```
aggregate ____________________________________________
XiState
by? : seq COLNAME
measures? : seq MEASURE_SPEC
criteria? : CRITERIA
────────────────────────────────────────────────────
pre: measures != empty
pre: FORALL m : measures WITH m.as NOT IN by
pre: sort_by != undef IMPLIES sort_by IN ran(by) UNION aliases(measures)
result == (dbh != undef
IMPLIES SQL_GROUP_BY(dbh, by, measures, criteria)
ELSE PERL_FOLD(selectall_arrayref(criteria), by, measures))
post: result : seq ROW (keys = by UNION aliases(measures))
post: FORALL r : result WITH
r.count != undef IMPLIES r.count >= 0
AND r.sum = undef IMPLIES no rows in that group
```
### Destroy
```
DESTROY ______________________________________________
DeltaState
────────────────────────────────────────────────────
post: _destroyed' = true
post: dbh' = undef (DBI handles disconnected)
post: data' = undef
post: _columns' = undef
post: _schema' = undef
```
## State Diagram
The following ASCII diagram shows every valid state the object can
occupy and the transitions between them. Transitions are labelled
with the method call or event that triggers them. Actions are shown
in parentheses.
```
.-----------------------------------------------------------.
| S1: CONSTRUCTED |
| dbh=undef, data=undef, type=undef |
'---.-------.-------.-------.-------.-----.---.-------------'
| | | | | | |
| first query call triggers _open_table()
| | | | | | |
v v v v v v v
.------. .------. .------. .------. .--. .--. .--.
| S2 | | S3 | | S4 | | S5 | |S6| |S7| |S8|
| DBI | | CSV | | XML | | JSON | |Xls| |Bk| |Dp|
'---^--' '--^---' '--^---' '--^---' '-^-' '-^-' '-^-'
| | | | | | | |
| | '-------'-------' | | |
| | slurp path | | |
| '-----------+---------------' | |
| | | |
| (data populated, | |
| _columns/_schema undef) | |
| | |
| (dbh populated, | |
| data undef) | |
| | |
+-------+--------+-----------+----------+ |
| | | |
selectall fetchrow aggregate fetchrow
count _hashref query->all _hashref
each_row AUTOLOAD AUTOLOAD AUTOLOAD
| | | |
v v v v
.-----------------------------------------------------------.
| (result returned to caller) |
| state is unchanged; only cache may be updated |
'-----------------------------------------------------------'
|
DESTROY (or object goes out of scope)
|
v
.-----------------------------------------------------------.
| S_DEAD: DESTROYED |
| _destroyed=true; all handles disconnected; |
| temp files unlinked; _remote_tmpdir removed |
'-----------------------------------------------------------'
State labels:
S1 CONSTRUCTED - new() returned; no I/O done yet (lazy open)
S2 DBI - live DBI handle (SQLite file, DSN, DBD::CSV)
S3 CSV_SLURP - small CSV/PSV/TSV/gzip-CSV slurped into RAM hash
S4 XML_SLURP - XML file slurped into RAM hash
S5 JSON_SLURP - JSON file slurped into RAM hash
S6 XLSX_SLURP - XLSX file slurped (type='XLSX', no DBI handle)
S7 BERKELEYDB - BerkeleyDB key-value file open (berkeley flag set)
S8 DEEP_SLURP - DBM::Deep file slurped into plain RAM hash
Blocked transitions (croak before state changes):
S1 -> any if constructor arguments are invalid
(unsafe id/table/host/url)
S* -> S* aggregate() with join on S3/S4/S5/S6/S7/S8
(no DBI handle)
S7 -> S* join() or select()-projection on BerkeleyDB
S8 -> S* join() on DBM::Deep
```
## License and Copyright
Copyright 2015-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.