Revision history for DBIx::Class::Async v1.1.0 2026-09-08 [SECURITY] - All dynamic class loading locations were secured to prevent code injection (CWE-94). The schema_class, result_class and similar specifications used to go through the eval "require $class" method which means that the code got executed in Perl and not only loaded as a module at 8 locations in the code. Any string that could be recognised as a class name combined with any number of ';' separated Perl instructions could cause arbitrary code execution while being loaded. - One of the paths of the 'result_class' resultset attribute in ResultSet::new_result was looked at in a proof of concept together with its unique accidental protection because of its protection clause, unless $target_class->can('can'), which gets true for any string that has ever existed (all classes inherit UNIVERSAL::can), therefore making the problematic line executing any code. The other call points are protected with different protection clauses (for example, ->can('connect')) and were confirmed to be affected. - The following new function has been added: DBIx::Class::Async::_safe_require_class. It checks the validity of the name of the class and then loads it using function require($file), providing a similar mechanism as Class::C3::Componentised::ensure_class_loaded. All calls to eval "require $class" and the double-interpolated eval "package main; ... " VERSION fallback function have now been changed to use this method. The previously ambiguous ->can('can') check has also been replaced by an actual check that the class was loaded before. [TEST SUITE] - A new test file t/157-safe-class-loading.t has been created, and a regression check has been added to t/123-result-class.t file to make sure that any malicious schema_class/result_class strings are rejected and no code is being run when real classes are being processed. v1.0.9 2026-09-08 [SECURITY] - Fixed a SQL injection (CWE-89) in txn_do's variable-chaining feature. _resolve_placeholders/_interpolate_string substituted register values (e.g. "$stepname.id", populated from a prior step's row primary key) directly into arbitrary strings via unescaped text replacement, including the 'sql' field of a { action => 'raw', sql => ..., bind => [...] } step, which is executed as literal SQL text via $dbh->do(). If a table's primary key value can be influenced by the caller (e.g. a client-supplied slug/username used as a string primary key, not an uncommon pattern, and not something the auto-increment happy path guards against), that value flowed unescaped into the SQL text, bypassing WHERE-clause scoping and any other part of the statement. This is a real, demonstrated injection, not merely a theoretical concern: it matched the library's own previously-shipped SYNOPSIS example and its own t/139-txn-do.t test, which exercised exactly this substitution as intended, documented behaviour. - _resolve_placeholders no longer touches the 'sql' key of a step under any circumstance. Chained values reaching a raw step now MUST go through the 'bind' arrayref with a '?' placeholder in 'sql', substitution into 'bind' array elements was already safe (it becomes a genuine DBI bind parameter) and is unchanged. Documentation in DBIx::Class::Async::Storage::DBI updated to show the safe pattern and warn against the unsafe one. [TEST SUITE] - Rewrote t/139-txn-do.t's "Raw SQL String Interpolation" test, which previously asserted the vulnerable behaviour as correct. It now verifies the safe bind-parameter pattern works, and that a literal "$name.id" left in raw SQL text is no longer silently substituted (it now surfaces as a query error and the whole transaction rolls back atomically, rather than injecting). v1.0.8 2026-09-08 [SECURITY] - An authorisation bypass was corrected using a cache key controlled by the user (CWE-639). This instance was detected by CPANsec. There were two different defects that together allowed one user's query to be fulfilled by other user's (or another database's) stored results. * The first of these issues is _generate_cache_key (ResultSet.pm) which reduced the cache key to primary key fields every time primary key was present in search condition ignoring all other conditions. Because of this, a search like search({ id => $id, owner => $user }) was saved as { id => $id }, so that in the future query with the same id regardless of who the owner is, could return the result from this cache. Actually the condition was applied correctly to the database but the cache key merged different queries in the cache. The cache key is generated now using the full search condition. * The CHI cache was created using global => 1 by _build_default_cache (Async.pm). As a result, all the instances of CHI::Driver::Memory in the given process are present in the same store. Because the cache key does not include connection identity, two Schemas that are connected to different databases have the possibility to read/write the entries of the cache of each other. The cache is isolated per connection now: global => 0, but, because each connection owns its own cache object, there is no need to add any connection identity to the key. * Because of this, it is impossible to guarantee that the cache key for Row::update's invalidation-by-primary-key would match the (now condition-aware) cache entry that has to be invalidated. [TEST SUITE] - Added regression tests to t/148-caching.t reproducing both the predicate-collapse and the cross-connection cache leakage, and confirming the fix. v1.0.7 2026-09-08 [SECURITY] - The issue with ResultSet::update() not working properly when invalidating the query cache has been resolved. While update() was able to create a key for invalidating the cache, it was not able to call clear_cache afterwards. Because cache_ttl was on, any read action conducted after the update would not return any new values. This meant that any changes made to the database would not be reflected in the read statements as it kept returning information that had already been updated long ago. In order to fix the issue with update(), it was made that update() should always call clear_cache just the same way as create() and delete() do. - Added a regression test (t/148-caching.t) reproducing the stale read and verifying update() now invalidates the cache. v1.0.6 2026-09-05 [TEST SUITE] - Fixed t/135-single.t to cover the issue #12 reported by @eserte. - Added unit tests: t/changes.t and t/version.t v1.0.5 2026-07-13 [POD CHANGES] - Used App:genstopwords v0.0.1 to generate .stopwords - Tidied up Makefile.PL script. - Cosmetic Changes to pod document. v1.0.4 2026-05-31 [BUG FIXES] - Fixed a SEGV (signal 11) during Perl global destruction when using two or more workers. The root cause was an incorrect teardown order in DBIx::Class::Async::disconnect(): calling $loop->remove() after $instance->stop() causes IO::Async to set an internal {stopping} flag that silently prevents remove() from deregistering the notifier. The orphaned Stream, Handle, and Process child notifiers remained in the loop's internal registry, and the loop's XS destructor accessed freed memory during global destruction. Fixed by calling $loop->remove() before $instance->stop(), then awaiting the returned Future so workers complete their shutdown sequence before cleanup proceeds. (Reported during Debian packaging; triggered by make -j32 test.) - Fixed DBIx::Class::Async::Schema::disconnect() which was performing its own broken worker cleanup instead of delegating to DBIx::Class::Async::disconnect(). The original code called stop() on the worker hashref ({ instance => .., healthy => .. }) rather than on the IO::Async::Function object it contained, meaning workers were never actually stopped. It then wiped %{$self->{_async_db}} = (), destroying all state including the loop reference before any cleanup could happen. Schema::disconnect() now delegates entirely to DBIx::Class::Async::disconnect() for correct, ordered teardown. - Fixed clone() creating a full independent worker pool (new forked processes) for every call, including in contexts where no isolation was needed. With many clones sharing the same IO::Async::Loop, the concurrent teardown of dozens of IO::Async::Function instances caused the loop's XS destructor to SEGV during global destruction. clone() now shares the parent's worker pool by default. An independent pool is only created when workers => $count is explicitly passed. - Fixed the async_loop option not being recognised in create_async_db(). Schema::connect() passes the loop via async_loop => but create_async_db() checked only for loop =>, causing _owns_loop to be incorrectly set to true for all user-supplied loops. create_async_db() now consistently uses async_loop => as the canonical parameter name throughout. - Added DESTROY() to DBIx::Class::Async::Schema. Schema objects that go out of scope during the RUN phase now automatically call disconnect() to stop their worker processes. DESTROY() is a no-op during global destruction (${^GLOBAL_PHASE} eq 'DESTRUCT') where IO::Async's XS internals may already be partially freed, and is also a no-op for shared clones which don't own their worker pool. [BEHAVIOUR CHANGE] - clone() now shares the parent's worker pool by default instead of spawning a new pool of processes. This is a safe change for the common case (clones connect to the same database and don't require process isolation), and eliminates a class of SEGV that occurred when many clones were created and destroyed within the same event loop. To obtain an independent worker pool (the previous behaviour), pass workers => $count explicitly: # Before (always independent - now only when workers given): my $clone = $schema->clone; # After (shared pool, auto-cleanup, safe default): my $clone = $schema->clone; # Independent pool (e.g. dedicated reporting workers): my $reporting = $schema->clone( workers => 8 ); # ... use $reporting ... $reporting->disconnect; # caller must disconnect Independent clones (created with workers =>) own their worker pool. Callers are responsible for calling disconnect() on them before they go out of scope. Shared clones (no workers =>) are cleaned up automatically. [DOCUMENTATION] - Updated POD for clone() in DBIx::Class::Async::Schema to document the shared vs. independent pool distinction and the disconnect() requirement for independent clones. - Updated POD for create_async_db() in DBIx::Class::Async to document all options in a structured list, including the async_loop alias for loop and the ownership semantics (who stops the loop on disconnect). - Updated POD for disconnect() in DBIx::Class::Async to document the required remove()-before-stop() ordering and the reason it matters for XS safety during global destruction. [TESTS] - Updated t/001-schema.t and t/027-schema-class.t to reflect the new clone() behaviour: clones now share _async_db with the parent rather than holding an independent instance. - t/103-clone.t continues to pass without modification: clone() called with workers => $count still creates an independent pool with the requested worker count and a distinct _async_db. v1.0.3 2026-05-06 [IMPROVEMENTS] - Fix non-deterministic failures in t/020-slice.t by forcing 'name' sort order, ensuring consistent results across asynchronous worker insertions. - Improve reliability of caching tests (t/148-caching.t and t/149-scalar-ref-char-pk.t) by using high-precision fractional seconds with SQLite's strftime. - Explicitly disable Resultset caching in t/148-caching.t using 'cache_ttl => 0' to verify cache-bypass logic regardless of execution speed. - Fix incorrect HashRefInflator accessors in test suites. v1.0.2 2026-05-06 [IMPROVEMENTS] - Tidied up unit test for Oracle and MySQL to use deploy(). v1.0.1 2026-05-06 [IMPROVEMENTS] - Added MySQL database support and verified the following features work correctly against MySQL 8.0.45 (Ubuntu 24.04): CRUD operations, search/count/all, belongs_to and has_many relationships, transactions via txn_do(), and concurrent async queries across multiple workers. - Added xt/002-mysql.t as an author integration test for MySQL. Requires DBD::mysql. Connection details are configurable via environment variables: DBIC_ASYNC_MYSQL_DSN, DBIC_ASYNC_MYSQL_USER, DBIC_ASYNC_MYSQL_PASS. - Updated POD documenting MySQL compatibility and instructions for running the MySQL test suite. v1.0.0 2026-05-01 [IMPROVEMENTS] - PAUSE didn't like the v-string move: Status: Decreasing version number So, bumped the version to v1.0.0. v0.66.0 2026-05-01 [IMPROVEMENTS] - Switched to v-string versioning (semantic versioning going forward). - Added Oracle database support and verified the following features work correctly against Oracle AI Database 23ai Free (via Docker): CRUD operations, search/count/all, belongs_to and has_many relationships, transactions via txn_do(), and concurrent async queries across multiple workers. - Added xt/001-oracle.t as an author integration test for Oracle. Requires DBD::Oracle and Math::Base36 >= 0.07. Connection details are configurable via environment variables: DBIC_ASYNC_ORACLE_DSN, DBIC_ASYNC_ORACLE_USER, DBIC_ASYNC_ORACLE_PASS. - Added DATABASE SUPPORT section to main POD documenting Oracle compatibility and instructions for running the Oracle test suite. 0.65 2026-03-15 [IMPROVEMENTS] - Added security document SECURITY.md as recommended in the issue #11. 0.64 2026-03-02 [BUG FIXES] - Fix InflateColumn::DateTime inflation on async worker path (PR#138) Workers correctly inflate datetime columns but deserialise results back to the parent as raw strings. The parent holds no active DB connection so ->storage returns nothing; formatter detection now reads the DSN once at connect time and stores it on _async_db as _datetime_formatter. - _inflate_row re-inflates datetime columns in the parent using this formatter, and sets it on the resulting DateTime object so that stringification produces database-native output rather than DateTime's bare ISO8601 format. The fix is gated behind isa('DBIx::Class::InflateColumn::DateTime') and is a no-op for schemas that do not load that component. [IMPROVEMENTS] - Updated pod spelling test script to use Test::Spelling::Stopwords. 0.63 2026-02-27 [BUG FIXES] - delete() on a ResultSet with group_by and/or having would cause DBIx::Class to generate a DELETE subquery with GROUP BY silently stripped, producing invalid SQL that most databases reject. Fixed by detecting group_by/having, building a clean copy of the attrs with those keys removed, and re-fetching matching PKs via a plain WHERE search before issuing delete_all(). - Fixed delete() attribute routing: the previous guard triggered the two-query delete_all() path whenever the ResultSet had any attribute at all, including harmless ones such as result_class, order_by, and prefetch, causing unnecessary extra queries. The check is now restricted to the six attributes that genuinely make a direct DELETE unsafe: rows, offset, and page (LIMIT/OFFSET), join (multi-table DELETE), and group_by/having (see above). ResultSets carrying only ordering or prefetch attributes now issue a direct single-query DELETE. [ENHANCEMENTS] - SelectNormaliser is now applied automatically inside search() after attribute merging and before the new ResultSet is constructed. Any { -ident => $col, -as => $alias } items in the select list are rewritten to bare column strings at this point, so every downstream operation flowing through search() -- all, count, count_total, update, and so on -- receives clean attributes without requiring individual normalisation calls at each call site. [TESTS] - Added t/154-grouped-delete.t covering delete() on ResultSets with group_by, having, rows (LIMIT), and join, plus a regression guard for plain ungrouped delete. Tests use a live SQLite database via the standard TestSchema fixture. - Added t/155-select-normaliser.t covering _is_ident_hashref classification, all normalise() input forms (bare strings, -ident with and without -as, function hashrefs, literal SQL refs, mixed lists, scalar and hashref non-array inputs), as/select length mismatches, normalise_attrs() key preservation and no-mutation guarantee, and a structural proof that the normalised output contains no -ident hashrefs. No live database required. 0.62 2026-02-25 [NEW FEATURES] - Added as_subselect_rs() method for wrapping queries as subselects. Correctly preserves column lists from the original ResultSet. When you select a subset of columns, the subselect will only include those columns, not all columns from the table. This enables complex query patterns with derived tables, limited subqueries, and multi-stage aggregations. - Added support for empty SELECT lists (select => []). Generates database-appropriate SQL: "SELECT FROM table" for PostgreSQL, "SELECT 1 FROM table" for other databases. This addresses compatibility with PostgreSQL's column-less SELECT syntax and resolves the limitation inherited from DBIx::Class core. - Added DBIx::Class::Async::Exception, a structured exception class hierarchy consistent with the DBIx::Class::Exception interface. Exceptions stringify to their message, support boolean overload, and provide throw/rethrow. - Added DBIx::Class::Async::Exception::Factory to translate raw DBIx::Class error strings into typed exception objects automatically, including improved diagnostics (relationship name passed as undef column value). Loading Factory is sufficient for all internal use -- it loads the base class and all subclasses as part of its own initialisation. - Each exception subclass lives in its own file, individually loadable via use/require and properly indexed on MetaCPAN: - DBIx::Class::Async::Exception::RelationshipAsColumn - DBIx::Class::Async::Exception::NotInStorage - DBIx::Class::Async::Exception::MissingColumn - DBIx::Class::Async::Exception::NoSuchRelationship - DBIx::Class::Async::Exception::AmbiguousColumn - Added DBIx::Class::Async::SelectNormaliser, a pre-processing module that allows { -ident => $col, -as => $alias } clauses in the ResultSet select attribute. DBIx::Class passes select attrs directly to SQL::Abstract's _select_field path, which does not recognise -ident as a special sigil and instead renders it as a function call (-IDENT($col)), producing invalid SQL. SelectNormaliser rewrites -ident hashrefs to bare column strings before they reach SQL::Abstract, requiring no upstream changes. Function hashrefs ({ count => 'me.id', -as => 'total' }), bare strings, and literal SQL refs (\[]) are left untouched. The as[] array is populated from inline -as values where not already specified by the caller, with caller-supplied as[] entries taking priority. Called automatically from ResultSet::search(); search_rs() benefits transitively as it delegates to search(). All downstream operations that build on a searched ResultSet (all, count, update, delete, as_query, etc.) see clean attrs without any change to call sites. [ENHANCEMENTS] - Enhanced as_query() to detect and handle empty column selection, generating optimal SQL for each database backend. - _call_worker now derives result_class from the payload's source_name at the top of the method, making it available to both the pre-flight validation and all three post-worker error paths without any change to the call signature. Pre-flight validation in _call_worker catches (undef relationship key in create/update hashref) before the request reaches the worker pool, returning a typed Future->fail immediately. [TESTS] - Added comprehensive tests for empty select list SQL generation, condition handling, and execution in t/152-sql-consistency.t. - Added t/exception.t covering throw/rethrow/overload behaviour for the base class and all subclasses, Factory pattern matching, and passthrough of unrecognised errors. Tests use mock schema/source objects and require no live database connection. [DOCUMENTATION] - Documented empty SELECT list support with usage examples and database-specific behavior in search() method POD. 0.61 2026-02-24 [NEW FEATURES] - Added update_query() method to DBIx::Class::Async::ResultSet for previewing UPDATE SQL without execution. Useful for debugging, audit logging, and testing. (GH#XXX) - Added delete_query() method to DBIx::Class::Async::ResultSet for previewing DELETE SQL without execution. Enables safe validation of destructive operations before execution. - Added insert_query() method to DBIx::Class::Async::Row for previewing INSERT SQL without execution. Allows inspection of insert operations for testing and validation. 0.60 2026-02-24 [ENHANCEMENTS] - Added debugobj() and debugfh() methods to DBIx::Class::Async::Storage::DBI for full API compatibility with DBIx::Class::Storage::DBI. These methods enable SQL debugging and logging with custom debug objects or filehandles. - Enhanced worker processes to respect debug flag from parent process. When storage->debug(1) is called, SQL statements are now logged to STDERR in worker processes, enabling comprehensive query debugging in async mode. - Improved SQL generation consistency in DBIx::Class::Async::ResultSet. Column ordering and attribute merging are now fully deterministic across multiple invocations of the same query, improving query cache effectiveness and debugging reliability. - Added attribute deduplication in ResultSet->search() chaining. Accumulating attributes (join, prefetch, columns, select, as, order_by, group_by, having) are now automatically deduplicated using stable serialization, preventing duplicate JOINs and maintaining consistent SQL structure. [TESTS] - Added t/152-sql-consistency.t to verify deterministic SQL generation across multiple query invocations, including tests for column order stability, attribute merging, and deduplication behaviour. - Added t/spelling.t to perform British spell check for AUTHOR_TESTING. [DOCUMENTATION] - Added comprehensive DEBUGGING section to DBIx::Class::Async::Storage::DBI documenting debug(), debugobj(), and debugfh() methods with usage examples. - Enhanced debug() method documentation to clarify integration with debugobj and worker process behavior. - Added detailed POD for SQL generation consistency guarantees and their benefits for query caching, performance analysis, and testing. [INTERNAL] - Refactored _merge_attrs() in ResultSet.pm with clearer logic and improved deduplication using Data::Dumper for nested structure comparison. - Extracted _merge_cond(), _is_empty_cond(), and _dedup_key() helper methods for better code organization and reusability. - Modified _call_worker() in Async.pm to pass debug flag to worker processes, enabling consistent debug behavior across process boundaries. 0.59 2026-02-21 [Bug Fixes] - Fixed issue silent drop of relationship 'where' conditions. Relationships defined with a 'where' containing raw SQL scalar refs (e.g. datetime functions) were silently ignored when traversing via a row accessor or prefetch, returning all related rows instead of the filtered subset. Fixed in _ensure_accessors(), _fetch_relationship_async(), and related_resultset() in DBIx::Class::Async::Row. Relationships with raw SQL in their 'where' are now excluded from the '_related' cache, ensuring dynamic expressions are evaluated fresh on every call. [TESTS] - t/151-has-many-datetime-filter.t: to cover the bug fix. - t/150-multi-column-relationship.t: new test covering multi-column relationships spanning two tables, demonstrating both the DBIC coderef relationship approach (join/prefetch) and the async-safe helper method pattern for cases where join conditions depend on columns from related tables rather than self. 0.58 2026-02-20 [BUG FIX] - ResultSet::create() now detects scalar-ref values targeting primary key columns and croaks immediately with a descriptive error message. Previously, scalar refs used to embed inline SQL expressions (e.g. \'UUID()') were silently stringified during IPC serialisation to the worker process, causing DBIC to fall back to last_insert_id() on a non-autoincrement char column and return a wrong value ('1' on SQLite, undef on MySQL). Callers should generate PK values in Perl before calling create(), e.g. Data::UUID->new->create_str. [TEST SUITE] - t/149-scalar-ref-char-pk.t: new test covering the scalar-ref char PK guard and confirming that Perl-side UUID generation works correctly as the fix. - t/lib/TestSchema/Result/Event.pm: new test fixture. - t/003-basic.t, t/004-simple-get.t: updated source count from 3 to 4 to reflect the addition of the Event fixture to TestSchema. 0.57 2026-02-10 - [FEATURE] Changed caching to be disabled by default (TTL set to 0). - [FEATURE] Added automatic detection of non-deterministic SQL functions (NOW, RAND, etc.) to automatically bypass cache for safety. - [FIX] Improved cache invalidation on update/delete operations to use specific primary key keys, preventing stale data scenarios. - [FIX] count() queries are no longer cached by default due to high risk of stale data, ensuring accurate row counts. 0.56 2026-02-08 [TEST SUITE] - Fixed t/030-row-copy.t to deal in system with -Duselongdouble as reported in the issue #4 by @eserte. - Fixed t/145-mojo-integration.t to skip test unless Mojo::IOLoop and IO::Async::Loop::Mojois are found as reported in the issue #5 by @eserte. 0.55 2026-02-06 [NEW FEATURES] - Added run_parallel() method to DBIx::Class::Async::Schema. This method accepts a list of code references, executes them concurrently using the asynchronous schema connection, and returns a Future that resolves when all tasks are complete. - Added await_all() method to DBIx::Class::Async::Schema. This method takes one or more Future objects and synchronously blocks until all of them have completed, returning the list of results. [DOCUMENTATION] - Added comprehensive POD for run_parallel() and await_all() in Schema.pm, including usage examples for maximizing query performance. [TEST SUITE] - Added t/146-await-all.t to verify synchronous waiting and error propagation for await_all(). - Added t/147-run-parallel.t to verify concurrent execution, aggregation of results, and error propagation for run_parallel(). 0.54 2026-02-04 [TEST SUITE] - Fixed t/145-mojo-integration.t to prevent compile-time failures on environments where Mojo::IOLoop is not installed. [DOCUMENTATION] - Formalised Event Loop Agnosticism: Explicitly documented the ability to inject external IO::Async::Loop instances. 0.53 2026-02-04 [TEST SUITE] - Added t/144-loop-engine.t: Validates "Smart Default" auto-initialisation and verified "Injection Pattern" for custom IO::Async::Loop instances. - Added t/145-mojo-integration.t: Established formal support for Mojo::IOLoop environments, proving non-blocking co-existence between DBIC workers and Mojo timers. [DOCUMENTATION] - Updated DBI.pm/Storage.pm: Clearly defined the "Master-Worker" architecture and the separation of the Parent process from the DBI Handle (Ghost DBH pattern). - Enhanced CAVEATS: Documented why traditional txn_begin/commit blocks are replaced by the atomic instruction-set logic of txn_do. 0.52 2026-02-04 - Proposed patch for issue #4 (-Duselongdouble), thanks @eserte. - Proposed patch for issue #9 (seg fault), thanks @eserte. - Proposed patch for issue #7 (missing prereq). thanks @szabgab. - Merged pull request #8 (add more versions of Perl to CI), thanks @szabgab. 0.51 2026-02-03 - Bumped version as PAUSE was unhappy with previous tar ball. 0.50 2026-02-03 [MAJOR ARCHITECTURAL OVERHAUL] - Complete rewrite of the Storage and Persistence layer to use a decoupled "Bridge & Worker" architecture. - Implementation of DBIx::Class::Async::Row lifecycle management: * Introduced "Dirty" column tracking to minimise SQL UPDATE payloads. * Added shadow-key optimisation for high-speed attribute access. * Robust AUTOLOAD mechanism for transparent ResultSet/Row interaction. - Enhanced Race Condition Recovery: * find_or_create() now handles unique constraint collisions automatically via a catch-and-retry strategy. - Improved Memory & Process Management: * Weakened schema references in Storage to prevent worker leaks. * Refined worker pool lifecycle (connect/disconnect) for cleaner shutdowns in event-loop environments. - Streaming Support: * Introduced DBIx::Class::Async::Storage::DBI::Cursor for non-blocking, memory-efficient iteration over large result sets. - Persistence Integrity: * Added strict Primary Key validation in find(), update(), and delete() to prevent ambiguous database operations. - Documentation: * Full POD refresh for Row, ResultSet, and Storage classes reflecting the new async design patterns. 0.49 2026-01-22 - [IMPROVEMENT] Fixed ResultSet->slice to correctly return a ResultSet in scalar context. - [IMPROVEMENT] Fixed ResultSet->count to respect slices (LIMIT/OFFSET) by using subqueries in the async worker. - [IMPROVEMENT] Ensure result_class persists across chained search() calls. - [IMPROVEMENT] Fixed constructor (new) to correctly store and inherit result_class, rows, and pager attributes. - [IMPROVEMENT] Fixed argument passing in Async.pm to ensure attributes ($attrs) reach the background worker for count operations. - [TEST] Fixed t/43-result-class.t to verify result_class persistence. - [TEST] Fixed t/26-slice.t to test comprehensive slicing and chaining behaviour against a real SQLite database. - [TEST] Added t/57-count-performance.t to benchmark and verify the overhead of asynchronous subquery counting. 0.48 2026-01-22 - [ENHANCE] Hardened _merge_result_data to support multiple database return patterns (HASH, ARRAY, and SCALAR). - [FIX] Added support for positional Primary Key mapping from ARRAY refs, improving compatibility with specialised DBI drivers. - [FIX] Improved Composite Primary Key safety during row creation, ensuring multi-column keys are correctly populated from database returns (e.g., PostgreSQL RETURNING clauses). - [TEST] Added comprehensive test suite for result data merging logic (t/56-merge-result-data.t). 0.47 2026-01-22 - [REFACTOR] Overhauled Row state management to use authoritative 'in_storage' flags instead of guessing based on Primary Key presence. - [FIX] Resolved "Cannot update row: not in storage" errors in complex workflows by propagating storage state through recursive relationship inflation. - [REFACTOR] Centralised row inflation logic into Async.pm bridge to ensure consistent object creation across all ResultSet fetch paths. - [FIX] Updated find_or_new to correctly distinguish between persistent and transient objects. - [IMPROVE] Hardened Row and ResultSet against Mock objects in test suites by adding defensive metadata checks (can('columns')). - [OPTIMISE] Improved search performance by only generating cache keys when caching is explicitly enabled. - [FIX] Ensure single-column primary keys are correctly merged during create() operations across all bridge drivers. 0.46 2026-01-22 - [DOCS] Major documentation overhaul across the entire distribution. - [DOCS] Modernised SYNOPSIS for all core packages to utilise Future-based chaining (->then/->catch) instead of legacy blocking patterns: * DBIx::Class::Async * DBIx::Class::Async::Schema * DBIx::Class::Async::ResultSet * DBIx::Class::Async::Row * DBIx::Class::Async::Storage * DBIx::Class::Async::Storage::DBI * DBIx::Class::Async::Storage::DBI::Cursor * DBIx::Class::Async::Pager * DBIx::Class::Async::ResultComponent - [DOCS] Added practical examples for high-concurrency operations, including race-condition safe find_or_create and update_or_create. - [DOCS] Documented the "Smart Discovery" logic for unique constraints. - [DOCS] Improved technical clarity regarding synchronous metadata vs. asynchronous data retrieval in Pager and Row objects. - [DOCS] Demonstrated standard async patterns for recursive cursor iteration. 0.45 2026-01-22 - [REFACTOR] Extracted unique constraint discovery into internal helper _extract_unique_lookup to unify logic across ResultSet operations. - [FEATURE] Refactored find_or_new to use Smart Discovery for lookups, improving search accuracy. - [FIX] Enhanced find_or_new to correctly strip "me." prefixes from ResultSet conditions when instantiating new result objects. - [FIX] Cleaned up find_or_create logic by utilising the new lookup helper and streamlining the race-condition recovery path. - [DOCS] Added POD documentation for internal _extract_unique_lookup method. - [FIX] Refactored update_or_create to utilise _extract_unique_lookup and implement atomic race-condition recovery. - [FIX] Corrected update_or_new behavior to properly return a volatile new_result object instead of persisting via create() on find failure. - [IMPROVEMENT] Standardised prefix stripping (me/self/foreign) in update_or_new to prevent invalid column crashes on filtered ResultSets. - [REFACTOR] Streamlined identity discovery across all upsert-style methods to ensure consistent unique key prioritisation. 0.44 2026-01-22 - [FEATURE] Refactor find_or_create to be race-condition safe in asynchronous environments. - [IMPROVEMENT] Implement "Smart Discovery" for unique constraints; automatically identifies lookup columns if 'key' is not provided. - [FIX] Enhance Async.pm error propagation to correctly stringify DBIx::Class::Exception objects for the parent process. - [FIX] Ensure find_or_create handles the "Gap" between initial find and create by performing an atomic recovery find on conflict. - [FIX] Add missing $attrs propagation to find() in Async.pm to support complex recovery searches. 0.43 2026-01-22 - Minor test cleanup. 0.42 2026-01-22 - [MAJOR] Added DBIx::Class::Async::ResultComponent to provide non-blocking row-level update() and delete() methods. - [OPTIMISATION] Re-engineered ResultSet update() and delete() to use high-performance bulk SQL ("WHERE PK IN (...)") instead of individual N-queries. - [FEATURE] Added support for bulk update_bulk() and delete_all() respecting LIMIT and OFFSET attributes via ID-mapping. - [FIX] Silenced "Query returned more than one row" deprecation warnings by utilising search()->first() with explicit row limits in worker operations. - [FIX] Hardened the Bridge update() method to support hybrid return types: returns Row objects for single updates and row-counts for bulk updates. - [STABILITY] Improved IO::Async cleanup during Global Destruction to prevent "Notifier does not exist" warnings. - [INTERNAL] Added _check_response() and _merge_result_data() helpers to standardise error handling and row inflation. - [TESTS] Added 8 new test files (t/48 through t/55) covering bulk operations, row-level components, and internal safety guards. 0.41 2026-01-19 - Added support for inflate column. 0.40 2026-01-19 - Added test to cover the use of CHI with DBIx::Class::Async. 0.39 2026-01-18 - Added sub is_ordered() to DBIx::Class::Async::ResultSet. 0.38 2026-01-18 - Added new package DBIx::Class::Async::ResultSet::Pager. - Added the following subs to DBIx::Class::Async::ResultSet. - count_total - is_paged - page - pager - search_with_pager 0.37 2026-01-18 - Documented the removal of support for txn_scope_guard() in DBIx::Class::Async::Schema. 0.36 2026-01-18 - Added sub txn_batch() to DBIx::Class::Async::Schema. - Updated DBIx::Class::Async::Schema::deploy() to return Future. 0.35 2026-01-18 - Updated pod for txn_batch() and added unit test. 0.34 2026-01-18 - Added sub search_with_prefetch() to DBIx::Class::Async. - Added sub result_class() to DBIx::Class::Async::ResultSet. 0.33 2026-01-17 - Added the following methods to DBIx::Class::Async::ResultSet - get_cache - set_cache - clear_cache 0.32 2026-01-17 - Added the following methods to DBIx::Class::Async::ResultSet - count_literal - count_rs - search_literal 0.31 2026-01-16 - Added deploy() to DBIx::Class::Async and DBIx::Class::Async::Schema. 0.30 2026-01-16 - Added the following methods to DBIx::Class::Async::Schema - schema_version - unregister_source 0.29 2026-01-16 - Added the following methods to DBIx::Class::Async::Row - is_column_dirty - update_or_insert - insert_or_update (alias for update_or_insert) 0.28 2026-01-13 - Added sub copy() to DBIx::Class::Async::Row. - Added sub register_[class|source] to DBIx::Class::Async::Schema. 0.27 2026-01-12 - Tidied up pod in general. 0.26 2026-01-12 - Re-organise Cursor package in sync with DBIx::Class. Before: DBIx::Class::Async::Cursor After: DBIx::Class::Async::Storage::DBI::Cursor - Added new package DBIx::Class::Async::Storage::DBI. 0.25 2026-01-12 - Added sub DBIx::Class::Async::Row::id(). 0.24 2026-01-11 - Added the following methods to DBIx::Class::Async::Row - is_column_changed - make_columm_dirty - get_dirty_columns - set_column - set_columns 0.23 2026-01-11 - Improved DBIx::Class::Async::Schema::clone(). - Added dedicated unit test to cover the clone(). 0.22 2026-01-11 - Added sub class() and populate() to DBIx::Class::Async::Schema. 0.21 2026-01-09 - Updated Makefile.PL to pull version automatically. - Tidied up pod document. 0.20 2026-01-08 - Updated Makefile.PL w.r.t test requisite thanks Gabor Szabo. - Added CI workflow thanks Gabor Szabo. - Tidied up pod document. 0.19 2026-01-08 - Added sub search_related[_rs] to DBIx::Class::Async::ResultSet. 0.18 2026-01-08 - Added sub delete_all() to DBIx::Class::Async::ResultSet. 0.17 2026-01-08 - Added sub update_or_[new|create]() to DBIx::Class::Async::ResultSet. 0.16 2026-01-08 - Added sub find_or_[new|create]() to DBIx::Class::Async::ResultSet. 0.15 2026-01-08 - Added sub populate() and populate_bulk() to DBIx::Class::Async::ResultSet. 0.14 2026-01-08 - Added sub slice() to DBIx::Class::Async::ResultSet. 0.13 2026-01-07 - Added sub related_resultset() to DBIx::Class::Async::ResultSet. 0.12 2026-01-06 - Updated pod about N+1 issue. 0.11 2026-01-06 - Search with prefix now supported by DBIx::Class::Async::Schema. 0.10 2026-01-06 - POD cleanup in general. 0.09 2026-01-05 - Added support for Cursor. 0.08 2026-01-05 - Added sub create_related() to DBIx::Class::Async::Row. 0.07 2026-01-05 - Fixed sub prefetch() in DBIx::Class::Async::ResultSet. 0.06 2026-01-05 - Fixed sub next() in DBIx::Class::Async::ResultSet. 0.05 2026-01-04 - Removed redundant documented attributes. 0.04 2026-01-04 - Updated pod document. 0.03 2026-01-04 - Tidied up unit tests. 0.02 2026-01-04 - Added initial draft to support following async operations: - DBIx::Class::Async::Row - DBIx::Class::Async::ResultSet - DBIx::Class::Async::Schema - DBIx::Class::Async::Storage - Also added template for the following: - DBIx::Class::Async::TxnGuard 0.01 2026-01-01 - Module Created.