Revision history for DBIx-Fast
0.21 2026-10-06
- Connection registry: configure(name => {...}) + connect(name), one
instance per name per process (rebuilt after fork)
- stream(): query() without loading the result set into memory
(MariaDB/MySQL use_result; 200k rows: +45 MB -> +0 MB)
- cached(): tag invalidation shared across processes with an external
backend (tag versions in the backend); DBIx::Fast::Cache::Redis
backend over Redis::Fast. With an external backend, invalidate()
now returns 1 instead of a per-process key count
- after_write => sub {...}: hook called after every CRUD write (audit)
- DBIx::Fast::Test::Mock: unit tests without a server (canned answers,
real SQL of the chosen dialect, recorded statements)
- on_slow_query => { ms, cb }: report slow statements
- metrics => 1, metrics(), metrics_text(): statement counters, duration
histogram and errors by class in Prometheus format
- configure_file(): registry connections from a JSON file
- Writes 32-42% faster than 0.20 (insert/update/delete/upsert) and
all() 16-19% faster on SQLite; 5-11% on MariaDB: no per-write
closures, fewer method calls, validated-identifier memo, rows built
from the driver's C fetchall
- Pg: insert/insert_ignore return the id with INSERT ... RETURNING (one
round trip; last_insert_id cost another: insert ~2x faster on Pg)
- val()/hash()/all() 31/24/9% faster than the first 0.21 build on SQLite:
fast path for cached statements, and Connector::dbh's common case in
a method touching 3 fields instead of 11 (Object::Pad binds every
field a method mentions): 428 -> 282 ns per query
- Bounded caches (statements, CRUD SQL, identifiers) shed a quarter of
their entries when full instead of emptying (no burst of misses)
- json_columns: write column sets memoized per table; reads decode only
the configured columns the result actually has
- DBD::SQLite, Test::More, Test::Exception are test-only prerequisites
(TEST_REQUIRES) - no longer installed for a runtime install
0.20 2026-10-06
- Reads ~10-15% faster: own statement cache instead of DBI's (Perl)
prepare_cached, and no per-call closure when no tracker is active
- query() iterator: no warning when it outlives a closed connection
- IN (?) with an arrayref expands to one placeholder per value
- where(): WHERE clause from filters (undef skipped; IN, CONTAINS...)
- page(): rows + total + pages, ORDER BY from a whitelist
- like(): literal LIKE patterns (ESCAPE '!'); in_chunks() for long lists
- count()/exists(): IN, NOT IN, CONTAINS, PREFIX, SUFFIX operators
- upsert/upsert_many: update modes 'new', '+', { '+' => N }, \'SQL'
- insert_ignore(); find_or_create() (race-safe via the UNIQUE index)
- insert_many/upsert_many: chunk => N; insert_many: ignore => 1
- timestamps => {...}: automatic created/updated columns per table
- json_columns => {...}: encode on write, decode on read
- Fix: rows after a write uses execute()'s count (DBD::Pg ON CONFLICT)
0.19 2026-09-28
- Typed exceptions (DBIx::Fast::X::Duplicate, ::Deadlock, ...); same text
- Every module has $VERSION; META provides (kwalitee all green)
- Changes in CPAN::Changes format; README regenerated from the POD
- examples/errors-and-reads.pl: 0.18 error handling and read helpers
- Every module: =encoding utf8, SYNOPSIS and SEE ALSO (enforced by a test)
- META homepage and license URL; CONTRIBUTING.md
- examples/benchmark-json.pl: Cpanel::JSON::XS vs JSON::PP
- Fork safety: a forked child no longer drops the parent's MariaDB
connection (DBD::MariaDB 1.24 disconnect_all bug, upstream PR #175)
- Optional POSIX::AtFork: fork check without getpid() (reads ~10% faster)
- Fix: MariaDB/MySQL analyze_indexes() never ran on a real server (reserved
alias `reads`, ONLY_FULL_GROUP_BY, missing sql_text column)
0.18 2026-09-28
- CRUD: stricter WHERE/data validation (SQL::Abstract raw-SQL operators)
- CRUD: statement cache for insert/update/delete/upsert (~10x faster)
- upsert/insert_many/upsert_many: reject unbindable references
- txn(): works with AutoCommit => 0
- DSN: keep user without password; percent-decoding; reject ';' in parts
- load_extension(): only accepts module names
- MariaDB profiler: EXPLAIN runs with max_statement_time
- Pg schema: table_info/get_indexes are schema-aware
- Docs: insert_many last_id is driver-dependent
- Errors record the failing sql and binds (redacted with errors_redact)
- on_error callback; error_mark/errors_since to scope errors
- Exception text without timestamp (groupable in logs)
- strict_reads: failed reads throw even with RaiseError => 0
- RaiseError => 0: a read whose prepare fails returns empty (no crash)
- rows: affected rows of the last write
- exists(), kv(), all_by() helpers
- on_connect_sql: session SQL re-applied on every reconnect
0.17 2026-08-20
- DSN parsing is more accepting (strictly additive - no DSN that
worked before changes): the native "dbi:..." prefix is
case-insensitive; "dbi:SQLite::memory:" and other DSNs whose
driver-specific part contains colons are no longer rejected;
and a credential-less URI ("mariadb://localhost/db", socket /
peer auth) is accepted. Verified byte-identical output for every
previously-valid DSN.
- The vestigial "quote" constructor option no longer runs a dead
$dbh->quote() no-op on connect; it is still accepted (and stored
in args) for backward compatibility but has no effect - identifier
quoting is automatic and driver-aware.
- Security: the CRUD operator whitelist now covers non-hash WHERE
specs. update()/delete() accept SQL::Abstract's arrayref
(OR-of-conditions) and scalarref (literal SQL) WHERE shapes;
_safe_columns bailed out silently on anything that was not a
hash, letting an injected operator or literal SQL through when
the WHERE was built as an arrayref/scalarref from untrusted
input. Now validated recursively; scalarref WHERE is rejected.
Regression test in t/security.t.
- Fix: read methods (all/flat/hash/val/array/count) use the 3-arg
prepare_cached form, so a live query() iterator on the same SQL
is no longer finished out from under the caller (silent
truncation, a 0.17 regression).
- Fix: execute() with no bind argument clears any $p left from a
previous query instead of binding it to the new statement.
- Fix: qualified table names ("schema.table") work in count(),
upsert(), insert_many(), upsert_many() and Schema introspection
(were quoted as one identifier via _quote_id; now per-segment
via _quote_ident_path, matching insert/update/delete).
- Fix: Schema tables cache is a hashref when the database has no
tables (tn => 1 on an empty DB gave a misleading crash).
- Fix: cache => 0 (or '') no longer switches caching on; the guard
is truthiness, not defined-ness.
- Fix: a transaction clears its savepoints on successful commit
too, so a later rollback_to() cannot fire on a stale name.
- Fix: t/security.t guards its LRU::Cache require (was bare, would
fail on smokers without the optional backend); Result.pm POD
link to query() points at DBIx::Fast::SQL; META repository url
is a valid https URI (an SSH spec was silently dropped).
[ New ]
- DBIx::Fast::Connector: fork-safe lazy connections, throttled
ping, automatic reconnect (replaces DBIx::Connector).
- query() row-by-row iterator via DBIx::Fast::Result.
- Query cache (DBIx::Fast::Cache / Cached): TTL, tag invalidation,
LRU or CHI backends, CRUD auto-invalidation.
- upsert() / insert_many() / upsert_many(), driver-aware.
- txn(isolation => ...), whitelist-validated and restored on exit.
- DBIx::Fast::Profile::Pg: native PostgreSQL diagnostics.
- DBIx::Fast::Profile::mysql: native MySQL diagnostics (inherits
the MariaDB profile; overrides EXPLAIN, removed-in-8.0 EXTENDED).
- DBIx::Fast::Output: unified profiler output - text (default),
JSON lines or callback; profile_output / $obj->output(...).
- now_utc(); examples/profiler-pg.pl.
[ Security ]
- SQL injection hardening: operator whitelists for all CRUD
WHERE/SET specs; identifiers validated AND quoted everywhere.
- Credentials never echoed: connect/DSN errors redacted;
errors_redact also scrubs backticks.
- Fork safety: AutoInactiveDestroy + pid-guarded disconnect.
- Cache keys include method + connection identity.
- Deadlock retry gated on driver error codes, not message text;
isolation-restore failure closes the connection (fail closed).
- EXPLAIN allowlists (MariaDB/Pg) resist literal-quoting tricks,
file access and DoS functions; terminal escapes stripped.
[ Fixes ]
- Transaction retry/rollback state, savepoint sequencing, memory
leak (weakened back-refs), val() first-column contract.
- DSN parsing: host/port, ':' in passwords, postgres:// schemes,
driver-correct utf8 flags; Pg isolation and last_insert_id.
- Cache: bounded tag index, TTL pushed down to CHI, honest stats.
- Profiler text output degrades gracefully on bare installs:
Cpanel::JSON::XS / Term::ANSIColor are recommends, so print_query
falls back to core JSON::PP and colorless output when absent.
- Profile::MariaDB information_schema queries carry explicit
lowercase aliases (MySQL 8 uppercases unaliased columns) and
get_processes no longer uses the MariaDB-only time_ms column.
- Exceptions: Exception() honors errors_redact, attaches last_error
only when recent, POD documents the real always-croak contract;
a failed transaction records its error once, not three times.
[ Performance ]
- fetchall_arrayref C-loop fetches, prepare_cached everywhere,
lazy SQL::Abstract, cheaper profiler/cache internals.
[ Packaging ]
- Query/CRUD methods live in the DBIx::Fast::SQL role, composed
into DBIx::Fast - identical API, the main module is half the
size and each file documents its own methods.
- Test suite coherence pass: the committed fixture t/db/test.db is
read-only (writing suites use tempdirs; regenerated from
t/db/schema.sql, 455KB of stale rows dropped), duplicate suites
merged (02-schema/dbix-fast/driver MariaDB.t), live tests skip
cleanly when the server is down, tables are per-file prefixed
and cleaned up, and the whole suite passes under prove -j.
TableName() format-validates unconditionally (insert/update had
accepted names count/upsert rejected).
- Optional deps as recommends; whole distro on Object::Pad; full
POD with coverage tests; 500+ tests on SQLite/MariaDB/Pg/MySQL
(CI service containers); LICENSE, SECURITY.md, README.md.
0.161 2026-03-21
- Connection mode: ping → fixup (eliminates ping overhead per query)
0.16 2026-03-20
- Migration from Moo to Object::Pad (requires Perl v5.38+)
- New: tracker() - query profiling across all methods (all/hash/val/flat/array/exec/insert/update/delete)
- New: Profile::Base, Profile::MariaDB, Profile::SQLite (driver-specific diagnostics)
- New: Transaction module with nested support, savepoints, deadlock retry
- Removed: up2() (unused), Query.pm (unused), execute_prepare2 (duplicate), make_sen old (buggy)
- Renamed: make_senN → make_sen (correct implementation)
- Performance: statement caching (prepare_cached), inlined query methods, _make_where with join
- Bug fixes: Schema typos, HandleError return 0, Time::HiRes import, `rows` keyword MariaDB 11+
- Bug fixes: Profile::MariaDB init (execute→exec), session stats columns, getrusage optional
- Tests: 147 SQLite + 34 MariaDB + 14 profiler
- Updated CI: Perl 5.38/5.40, bookworm, Object::Pad
- Full POD documentation on all 8 modules
- Added examples/profiler-mariadb.pl
0.15 2024-09-06
- args : SQLite
0.14 2024-09-03
- POD
0.13 2024-09-01
- t/exception.t
- Clean
0.12 2024-09-01
- Test
- DSN / URI
- _check_dsn / _make_dsn_dbi
0.11 2024-09-01
- SQL::Abstract
0.10 2024-09-01
- _make_dsn
- _check_dsn
- driver && dbd
- t/functions.t
0.09 2020-01-01
0.08 2016-01-26
- Force UTF8
0.07 2016-01-24
0.06 2015-03-23
0.05 2015-03-18T03:30:38+01:00
- More test
- Method : array
0.04 2015-03-18T00:59:19+01:00
- Test
0.03 2015-03-17T00:50:31+01:00
- New methods : value & count
- Dereference hash reference for using 'keys'
0.02 2015-03-17T00:20:48+01:00
- extra_args -> time NOW()
0.01 Unknown Release Date
Keyboard Shortcuts
Global
s
Focus search bar
?
Bring up this help dialog
GitHub
gp
Go to pull requests
gi
Go to GitHub issues (only if GitHub is preferred repository)