CodeLogic.Data 5.0.121

This package has a SemVer 2.0.0 package version: 5.0.121+4885b82.
dotnet add package CodeLogic.Data --version 5.0.121
                    
NuGet\Install-Package CodeLogic.Data -Version 5.0.121
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeLogic.Data" Version="5.0.121" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeLogic.Data" Version="5.0.121" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.Data" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeLogic.Data --version 5.0.121
                    
#r "nuget: CodeLogic.Data, 5.0.121"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeLogic.Data@5.0.121
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeLogic.Data&version=5.0.121
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.Data&version=5.0.121
                    
Install as a Cake Tool

CodeLogic.Data

One database framework for MySQL, PostgreSQL, SQL Server and SQLite. It's the CodeLogic 5 replacement for CL.MySQL2, CL.MSSQL, CL.PostgreSQL and CL.SQLite: one core package plus one small package per database, with the same API and the same behaviour everywhere.

Package Database Driver
CodeLogic.Data core (no driver) —
CodeLogic.Data.MySql MySQL / MariaDB MySqlConnector
CodeLogic.Data.PostgreSql PostgreSQL Npgsql
CodeLogic.Data.SqlServer SQL Server / Azure SQL Microsoft.Data.SqlClient
CodeLogic.Data.Sqlite SQLite Microsoft.Data.Sqlite
dotnet add package CodeLogic.Data.PostgreSql   # brings CodeLogic.Data
await using var db = Database.Create("main", PostgreSqlDataProvider.Instance, new DataConnectionConfig
{
    Provider = "postgresql", Host = "localhost", Database = "app", Username = "app", Password = "secret",
}).ValueOrThrow();

await db.Schema.SyncAsync<Product>();
await db.Repository<Product>().InsertAsync(new Product { Sku = "A-1", Price = 9.95m });
var cheap = await db.Query<Product>().WithCache().Where(p => p.Price < 20m).OrderBy(p => p.Price).ToListAsync();

Features:

  • Queries: typed LINQ (strictly translated, never evaluated on the client), projections, joins over 2–5 tables, grouping with HAVING, cursor paging and streaming, and compiled queries.
  • Writes: repositories with upserts, counters and changed-column updates; set-based updates and deletes; transactions, savepoints, row locks, advisory locks (not on SQLite) and batches; raw SQL with an explicit or conservatively detected intent (SqlIntent).
  • Schema: table sync with safe levels and drift detection, migrations, seeding, admin, backup and retention.
  • Caching: query and entity caching with exact tag invalidation, warm queries and cross-node invalidation.
  • Replicas and resilience: read replicas with lag probes, sticky reads and failover; retries and per-node circuit breakers.
  • Entity behaviour: auditing, optimistic concurrency, multi-tenancy, global filters and interceptors.
  • Telemetry: OpenTelemetry spans and metrics, slow-query plans and N+1 detection.
  • Bulk copy: native on every provider.
  • Mapping: a source generator (shipped in this package) writes it at build time, with no reflection, and CL.Data is Native AOT compatible. Custom types (typed IDs, value objects) use value converters; see Entities.

Measured: within a few percent of raw ADO.NET and Dapper. Against CL.MySQL2 4.9 it uses 20–45% less memory, and cached reads take microseconds.

Documentation: docs/libs/data.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on CodeLogic.Data:

Package Downloads
CodeLogic.Data.MySql

MySQL / MariaDB provider for CodeLogic.Data (MySqlConnector).

CodeLogic.Data.PostgreSql

PostgreSQL provider for CodeLogic.Data (Npgsql).

CodeLogic.Data.Sqlite

SQLite provider for CodeLogic.Data (Microsoft.Data.Sqlite).

CodeLogic.Data.SqlServer

SQL Server / Azure SQL provider for CodeLogic.Data (Microsoft.Data.SqlClient).

CodeLogic.WebLogic.Data

Database stores for CodeLogic.WebLogic on CodeLogic.Data (MySQL, PostgreSQL, SQL Server or SQLite): users, sessions, API keys, recovery codes, one-time use, login throttle, rate limits, audit, widget settings, dashboards, passkeys and the DataProtection key ring — with schema sync and expiry sweeps.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.121 76 11/10/2026
5.0.120-preview 50 11/10/2026
5.0.119-preview 71 11/8/2026
5.0.118-preview 48 11/8/2026
5.0.117-preview 80 11/7/2026
5.0.116-preview 78 11/1/2026
5.0.115-preview 129 11/1/2026
5.0.114-preview 59 10/31/2026
5.0.113-preview 59 10/31/2026

# Changelog

## 5.0 (2026-11-10)

### Breaking

- **`ConnectionResetMode` replaces `MySqlConnectionReset`:** one enum (`WhenDirty`, `Always`, `Never`) for `mySql.connectionReset` and the new `postgreSql.connectionReset`. Configuration values are unchanged; C# code that names the old type must use the new one.

### Security

- **Bookkeeping reads use the primary:** schema sync state, migration and seed history, the database cache bus and every schema introspector (`ReadTableAsync`, `ListTablesAsync`) read the primary, whatever the read preference. Before, with replicas and the default `PreferReplica`, a lagging replica made startup sync try to create `cl_data_schema` a second time and let a second node re-apply migrations despite the advisory lock.
- **Raw SQL that writes runs as a write:** `SqlQueryAsync`, `SqlReadAsync`, `SqlScalarAsync` and `DataBatch.Sql` classify the statement conservatively (`SqlIntent.Auto`). Only a single plain `SELECT`, `WITH ... SELECT`, `VALUES` or `SHOW` is a read; anything else (`INSERT ... RETURNING`, data-modifying CTEs, `SELECT ... INTO`, `FOR UPDATE`, `EXEC`, several statements, ...) runs on the primary, is not retried once sent (no duplicate inserts after a transient error), and drops the connection's cached results. New overloads take an explicit `SqlIntent` (`Read`, `ReadPrimary`, `Write`, `SideEffect`); mark a `SELECT` that calls a function with side effects (`nextval`, `GET_LOCK`) as `Write` or `SideEffect`. **Behaviour change:** a raw non-`SELECT` statement through these APIs no longer goes to a replica and now clears the connection's cache.
- **Backup restore checks the manifest (breaking for unusual schemas):** `Backup.RestoreAsync` refuses (`data.validation`, before anything is dropped) a `manifest.json` whose store types, collations, defaults or table options are not plain values, and rows files outside the backup directory. Before, these were written into `CREATE TABLE` as they were, so anyone who could write the backup directory could run any SQL, and `file` could point anywhere. Store types must be a type name with an optional size and modifiers (`varchar(64)`, `timestamp(3) with time zone`, `bigint unsigned`, `enum('a','b')`); a default may only use literals, parentheses, casts and known functions (`CURRENT_TIMESTAMP`, `now()`, `newid()`, `gen_random_uuid()`, ...). Tables with user-defined types or computed defaults cannot be restored from a logical backup.
- **Cached results of array parameters:** the result-cache key writes array and list parameters element by element (and strings with their length). Before, an `int[]` parameter was keyed as `System.Int32[]`, so two cached queries over different arrays shared one result.
- **Large `InsertManyAsync` calls keep the command pipeline:** the switch to the native bulk path (`bulkThreshold`) is skipped when the database has command interceptors or middleware, so an auditing or policy interceptor sees every insert. Before, those inserts bypassed `OnExecutingAsync`/`OnExecutedAsync` silently.
- **Endpoints never print secrets:** `NodeEndpoint.ToString()` (and the record's generated member list) shows the name, role, host, port and user, never the password or the connection string. Before, logging an endpoint printed its password.
- **Slow-query plans never re-run statements:** `captureExplainOnSlowQuery` only explains a single plain read. Before, a slow `DataBatch` (or a multi-statement raw query) had `EXPLAIN` put in front of its first statement only, so the following statements ran a second time on the background connection.

### Fixed

- **Entity cache after a transaction with mixed writes:** a transaction that updated or deleted one row by key and also ran a set-based write on the same table (a set-based update or delete, an upsert, bulk copy) left the key lookups of the other rows cached after commit, so `GetByIdAsync` could return a row from before the set-based write until the entry expired. Whether a write names its rows is now decided per write, not for the transaction's writes together. Affects 5.0.119-preview with `entityCache` on.
- **Compiled queries are planned once per database, not once per view:** a transaction's `tx.Db`, `ReadFrom(...)` and `ForTenant(...)` are views of the database, and a compiled query planned itself again for every view (translation, SQL rendering and compiling its argument readers). Inside a transaction a compiled lookup allocated 24.4 KB and took about 0.2 ms longer than outside; it now allocates 10.7 KB, like a key lookup. Plans are kept per database and, within it, per tenant, user filters, read preference and (for locking reads) transaction.
- **Packages depend on a signed CodeLogic:** published nuspecs require CodeLogic `[5.0.35-preview, 6.0.0)` (the version the libraries are built against) instead of `[5.0.0-preview, 6.0.0)`. NuGet resolved the lowest match, 5.0.28-preview, whose `CodeLogic.dll` is not strong-name signed, so projects failed with `CS0012 'Result<>' is defined in an assembly that is not referenced` unless they referenced a newer CodeLogic themselves.
- **Advisory-lock release failures are no longer ignored:** when `RELEASE_LOCK` (or `pg_advisory_unlock`) fails or does not report the lock as released, the lock's connection is reset (MySQL) before it can be reused.
- **Provider table hints are applied:** `[PostgresTable(Unlogged, FillFactor, Tablespace)]` and `[SqlServerTable(FileGroup, DataCompression)]` now shape `CREATE TABLE` (new `DdlGenerator.CreateTableKeyword` hook). They apply when sync creates a table; existing tables are not altered. `[SqlServerColumn(Sparse)]` and `[MySqlColumn(Charset)]` are still not applied and are marked `[Obsolete]` (set `Collation`, which implies the MySQL character set). Before, all of them were declared and documented but ignored.
- **Compiled queries keep `WithCache()` and `ReadFrom()`:** a compiled chain's cache policy and read preference apply to every call. Before, both were dropped silently, so a compiled query was never cached and always followed the connection's read preference.
- **`SeedAppliedEvent` is published** for every seed a `SeedRunner` applies (and forwarded to the CodeLogic event bus by `DataLibrary`). Before, it was declared and documented but never raised.
- **Replicas configured by `host` keep the primary's database and credentials** when the primary uses `connectionString` (the documented pattern): the replica starts from the primary's connection string and puts its own host, port and credentials on top. Before, such a replica connected without a database, user or password.
- **Ordered and limited set-based deletes:** `Query<T>().Where(...).OrderBy(...).Take(n).DeleteAsync(hard: true)` deletes at most `n` rows in that order (MySQL `DELETE ... ORDER BY ... LIMIT`, SQL Server and the others through a key subquery). Before, `Take` and `OrderBy` were silently ignored and **every** matching row was deleted. `Skip`, and `Take` on a soft delete, now fail with `data.validation`.
- **Set-based updates with `Take`/`Skip`** fail with `data.validation` instead of updating every matching row.
- **Covering indexes on MySQL:** `[Index(Include = ...)]` columns are folded into the desired key where the database has no `INCLUDE` (MySQL), the same way the DDL creates the index. Before, every sync saw a missing index and planned to create it again.
- **MySQL rebuilds a table once per sync, not once per column:** a table's added, modified and dropped columns go into one `ALTER TABLE` (NULL backfills first). Before, every column change was its own statement, and MySQL copied large tables once per column. New `DdlGenerator.GenerateAll(changes)` hook; scripts and sync both use it.
- **Making a column NOT NULL** first replaces existing NULLs with the column's default (or an implicit one) in the same plan, so the `ALTER` no longer fails on tables that hold NULLs.
- **Tenant isolation on writes (security):**
 - **No tenant:** updating, deleting (soft or hard, by entity or by key) or adjusting a counter on a `[TenantKey]` entity without a tenant now fails with `data.tenant_missing`, exactly like reads. Before, such writes ran unscoped and could change another tenant's row by key.
 - **Upserts:** an upsert can no longer update another tenant's row that has the same conflict key. Every dialect guards the conflict update by the tenant column (PostgreSQL/SQLite `DO UPDATE ... WHERE`, MySQL `IF(...)` per assignment, SQL Server join condition), and a pre-check reports `data.conflict` before anything is written.
 - **Inserts, upserts and bulk copy (breaking):** these also need a tenant now. Before, an entity whose tenant column was already set could be inserted for any tenant through an unscoped database. Write for a specific tenant through `db.ForTenant(id)`.
 - **Retention:** a purge from a tenant view deletes only that tenant's expired rows. Without a tenant, the purge stays the system-wide job for every tenant's rows.
- **MySQL: saving an unchanged entity:** `UpdateAsync` of an entity whose values were already stored failed with `data.not_found` on MySQL. CL.Data turns on `UseAffectedRows`, which insert-ignore needs, and with it MySQL counts only rows that changed. An update reporting no rows is now confirmed with a `SELECT 1` using the same key, tenant and token conditions. This only happens on providers flagged `ProviderCapabilities.AffectedRowsCountChangesOnly`, and only in that case.
- **Column collation hints are per provider:** a column's collation now comes only from the hint of the provider being synced (`[MySqlColumn]`, `[PostgresColumn]`, `[SqlServerColumn]`, `[SqliteColumn]`). Before, the first hint of any provider was used, so an entity with both a MySQL and a SQL Server collation sent `COLLATE utf8mb4_bin` to SQL Server. Providers implement `DataProvider.GetColumnCollation`.
- **SQL Server: unique indexes over nullable columns:** they are created filtered (`WHERE col IS NOT NULL`), so many rows may leave the column empty, as on MySQL, PostgreSQL and SQLite. Before, SQL Server treated NULLs as equal and a second row without a value failed with `data.duplicate_key`. Existing indexes are matched by shape and left as they are; drop and re-sync to get the filtered form.
- **Dropped tables are recreated by the next sync:** `Admin.DropTableAsync` forgets the table's fingerprint in `cl_data_schema`. Before, a sync without `Force` reported the dropped table as unchanged and never recreated it.
- **Mixed identity upserts:** an `UpsertManyAsync` call mixing explicit and server-generated identities is split into two statements. Before, the identity column was dropped for every row and explicit identities were replaced.
- **Streaming:** `AsAsyncEnumerable` runs through the command pipeline: interceptors, telemetry, read routing (replicas, `ReadFrom`) and retries of the open. Before, streams always opened a primary connection directly.
- **Captured `SqlFn` calls:** a `SqlFn` call whose arguments are all captured values (for example `SqlFn.BucketUtc(at, 60)` in a projection) is translated to SQL. Before, it was evaluated on the client and failed.
- **Cache hits** no longer walk the statement to find the tables it reads; the tags are worked out only when a result is stored.
- **Mixed identity inserts:** when explicit and server-generated identities are interleaved in one `InsertManyAsync` call, generated identities are written back to the right entities. Before, a generated identity could overwrite an explicit entity's identity.

- Float-to-decimal materialization preserves the float's shortest round-trip representation instead
 of promoting it to double first (for example, `0.1f` now reads as `0.1m`).
- Converter registry version increments are atomic under concurrent registration/removal. Removing a
 nullable model type now removes the underlying registration, matching converter lookup.
- Query plan variants use volatile publication so concurrent readers see fully initialized snapshots.

### Changed

- **Rows are read by position where CL.Data wrote the SELECT list:** key lookups, whole-entity queries and compiled queries no longer ask the driver for column names. MySqlConnector decodes six strings per column when one name is read, about 1.4 KB per query for a six-column entity. A MySQL lookup by key allocates 7.1 KB instead of 8.5 KB (raw ADO.NET 5.1 KB, Dapper 6.9 KB) and, under 32 parallel workers, 6.1 KB instead of 7.6 KB (Dapper 6.8 KB). `Exclude(...)`, projections, raw SQL, replayed cache entries and statements an interceptor rewrote are matched by name, as before.
- **Cheaper cache hits:** the cache key is built in a reused buffer and hashed in place, and a hit needs neither the loader nor its closure. A cached key lookup allocates 0.62 KB instead of 1.63 KB (0.69 µs), a cached 100-row filter 13.5 KB instead of 16.2 KB, and the 95% lookup / 5% update mix 1.3 KB per operation instead of 2.4 KB.
- **Leaner writes:** inserts, updates and deletes without interceptors run without a delegate, a closure or extra async frames per write, and the write executor uses pooled state machines. An insert allocates 7.9 KB instead of 9.4 KB, an update 7.7 KB instead of 9.9 KB, a changed-columns update 6.6 KB instead of 8.1 KB (Dapper: 5.5 and 4.9 KB).
- **Less work per rendered statement:** quoted identifiers and table names are remembered per dialect, and an entity's SELECT list, table tags and default schema are built once instead of per call. A LINQ count allocates 8.4 KB instead of 9.5 KB, a 100-row filter 35.6 KB instead of 38.9 KB.
- **Large inserts and the entity cache:** an insert of more than 64 rows drops the table's key lookups as a whole instead of one tag per inserted row.
- **The entity cache drops only the rows a write names (behaviour change):** key lookups are tagged with their row instead of the table, so an update, delete, increment or adjustment by key keeps every other row's lookup cached, and an insert drops a cached "not found" for its keys. Writes whose rows are not known (set-based updates and deletes, upserts, bulk copy, raw SQL, `Invalidate` with a table tag, invalidations from the cache bus) still drop every lookup of the table. Query results are still dropped on any write to their tables. A 95% lookup / 5% update workload on a hot set went from 767 µs to 191 µs per operation (14.7 KB → 2.4 KB).
- **Faster key lookups:** `GetByIdAsync` / `GetByKeyAsync` build their SELECT once per entity, dialect, naming policy and schema and only bind the key (and tenant); entities with user filters keep the general path. A MySQL lookup by key allocates 8.9 KB instead of 11.9 KB and runs within the noise of raw ADO.NET and Dapper; a cached lookup takes 0.8 µs instead of 2.2 µs (1.6 KB instead of 4.0 KB).
- **Leaner command execution:** pooled async state machines on the per-command path, no closure per read, the registered value converter looked up once per type instead of per value, cached insert column lists, and an update's match check built only when the provider needs it. Under 32 parallel workers a lookup by key allocates 7.6 KB instead of 11.7 KB (Dapper: 6.8 KB), a 100-row filter runs 9% faster and an insert 13% faster; a single insert allocates 9.8 KB instead of 11.5 KB.
- **PostgreSQL resets only dirty sessions (behaviour change):** see `postgreSql.connectionReset` in the provider docs.
- **`prepareStatements` documents what it does:** PostgreSQL auto-prepare, and on MySQL `Prepare()` in code run through `WithConnectionAsync`. Preparing CL.Data's own key lookups on MySQL was measured (no gain) and not adopted.

- **MySQL skips the connection reset round trip (behaviour change):** pooled MySQL connections are no longer reset by the driver on every open, which cost one full round trip per command (a lookup by key took about twice as long against a remote server). CL.Data resets a connection itself, before it returns to the pool, only when it may carry session state: raw SQL that is not one plain query, DML or DDL statement (`SET`, `CALL`, transaction control, user variables, `GET_LOCK`, temporary tables, several statements), connections handed to `WithConnectionAsync`, advisory locks that were not released cleanly, SQL rewritten by an interceptor, and commands interrupted by anything but a plain statement error. `mySql.connectionReset` (`WhenDirty`, the default; `Always`, the old behaviour; `Never`) chooses. Without the reset on open, a pooled connection dropped while idle fails on its first command: keep `mySql.connectionIdleTimeoutSeconds` (default 180) below any proxy's idle timeout. See the MySQL section of the provider docs.
- N+1 diagnostics use a dedicated `System.Threading.Lock` for each counting window.

### Added

- **Benchmarks:** `--quick [name]` prints bytes and microseconds per call within seconds (`CL_BENCH_TYPES=1` adds the most allocated types), and the suite gains SQLite scenarios and key lookups inside a transaction.

- **Dialect-aware raw-SQL lexing:** `SqlDialect.Lexicon` (`SqlLexicon.Standard`, `.MySql`, `.PostgreSql`) tells the raw-SQL classifiers how the server lexes comments and literals: MySQL `#` comments and `-- ` with a space (`1--1` is arithmetic), PostgreSQL dollar quotes, `[identifiers]` only on SQL Server and SQLite. A `'...'` or `"..."` literal with a backslash is ambiguous (escaping depends on `sql_mode` / `E'...'`) and makes the statement count as a write that may change the session. Before, one lexer served every dialect, so text the server runs could look like a comment or literal (for example `SELECT 1--1, ...` on MySQL, or `$x$` identifiers), and `SqlIntent.Auto` could take a write for a plain read.
- **Dirty-session tracking for providers:** `DataProvider.ResetsDirtySessions`, `ResetSessionAsync`, `DiscardSessionAsync` and `MayChangeSession` let a provider run its pool without a reset on every open and reset only connections that may carry session state. `SessionSqlRules` classifies raw SQL conservatively (single statement, neutral first word, no user variables, executable comments or listed words; CL.Data's numbered parameters are not taken for variables). New metrics `cl.data.session.resets` and `cl.data.session.reset_failures`.
- **MySQL: index changes join the table's single ALTER:** `DROP INDEX` / `ADD [UNIQUE] INDEX` become clauses of the one `ALTER TABLE` per table (with the column changes), so a table copy no longer rebuilds an index that is dropped right after, and index-only changes are one in-place pass.
- **Slow-query log includes the plan:** with `captureExplainOnSlowQuery` the captured EXPLAIN is written to the library log under the SQL (`SlowQueryWithPlan`), not only carried on `SlowQueryEvent.Plan`.
- **Schema scripts:** `db.Schema.ScriptAsync(maps, level)` / `ScriptAsync<T>()` compares the models with the database and returns the DDL table sync would run, without running it (`SchemaScript`: per-table changes, pending items, `Statements`, `ToSql()`). Generate reviewed migration scripts from the models against a copy of production; an empty script means the database matches.
- **Stale-while-refresh cache:** `CachePolicy.InvalidateOnWrite = false` keeps a result through writes to its tables and reloads it at the next refresh tick (CL.MySQL2 SmartCache pools behaved like this). `DataCache.RefreshAsync(tags)` reloads warm results in place and drops the rest. `WithCache` on `ProjectedQuery` and `GroupProjection` (after `Select`, also after `GroupBy(...).Select(...)`).
- **Scoped raw SQL invalidation:** `ExecuteSqlAsync(sql, tables)` drops only the cached results reading those tables instead of the whole connection's.
- **Whole-second time columns:** `[Column(Precision = 0)]` on a time type gives MySQL `DATETIME`, SQL Server `datetime2(0)`, PostgreSQL `timestamptz(0)`. `Precision` now defaults to `-1` (unset: the provider maximum for time types, 18 for decimals).
- New package, part of the CL.Data database framework that replaces CL.MySQL2, CL.MSSQL, CL.PostgreSQL and
 CL.SQLite in the CodeLogic 5 line. Targets .NET 11 and CodeLogic 5.
- **Errors:** `DataErrors` holds every `data.*` error code, each always paired with the same error kind
 (NotFound, Conflict, Timeout, Unavailable, Unauthorized, Validation, Internal). Messages never contain
 parameter values or secrets.
- **Events:** `DataEvent` is a closed event hierarchy (query executed, slow query, N+1, cache hit/miss, schema
 synced and drift, migration, seed, retention, bulk copy, node health, circuit state). Each event carries the
 connection, provider and node.
- **Localization:** `DataStrings`, the localizable log messages (section `data`).
- **Configuration:**
 - `data` section (`DataConfig`): named connections (`DataConnectionConfig`) with provider, server, failover hosts,
   TLS, pooling, naming policy, sync mode, performance, diagnostics, resilience and replica settings, plus typed
   per-provider options (`MySql`, `PostgreSql`, `SqlServer`, `Sqlite`).
 - `data.cache` section (`DataCacheConfig`).
 - Validation reports field paths. Connection names stay case-insensitive after loading from JSON.
- **Mapping attributes:**
 - Entity and column attributes: `[Table]`, `[DataRow]`, `[Column]` (composite keys via `KeyOrder`), `[Ignore]`.
 - Relationship and index attributes: `[ForeignKey]` and `[ForeignKey<TEntity>]`, `[Index]` (multi-column, direction,
   included columns), `[CompositeIndex]`.
 - Behavior attributes: `[SoftDelete]`, `[RetainDays]`, `[ConcurrencyToken]`, `[CreatedAt]`/`[UpdatedAt]`/`[CreatedBy]`/
   `[UpdatedBy]` and `[TenantKey]`.
 - Types: a provider-neutral `DataType` enum, plus provider hint attributes (`[MySqlTable]`/`[MySqlColumn]`,
   `[PostgresTable]`/`[PostgresColumn]`, `[SqlServerTable]`/`[SqlServerColumn]`, `[SqliteTable]`/`[SqliteColumn]`).
- **Metadata model:**
 - `EntityMap` / `ColumnMap` / `KeyMap`: composite keys are first-class. Column roles: identity, soft delete,
   concurrency token, audit, tenant, retention. Also indexes and foreign keys.
 - Names are resolved per `NamingPolicy` (AsIs, SnakeCase, CamelCase) and cached.
 - Invalid models report every problem at once.
- **Entity keys:** `EntityKey` holds single and composite key values, normalized so `5` equals `5L`.
- **Accessors:** `IEntityAccessor<T>` with an `EntityAccessorRegistry` for generated accessors.
 - Types without generated mapping fall back to reflection (compiled delegates where dynamic code is available).
 - The fallback can be switched off with `CLDataReflectionMapping=false`; Native AOT publishes switch it off by default.
- **Value conversion:** `ValueReader<T>` / `ValueConversion` handle driver differences: integers as booleans or enums,
 bytes or text as GUIDs, `DateTime` as `DateTimeOffset` (UTC), and double → decimal through round-trip text.
- **Provider contract:**
 - `DataProvider` with `ProviderCapabilities`, `ITypeMapper`, `IErrorClassifier`, connection-string building per
   `NodeEndpoint` (the primary or a replica) and a `DbDataSource` per node.
 - `DataProviderRegistry` checks the contract version of each provider.
 - `SqlDialect` base with ANSI quoting, parameter names and LIKE escaping.
- **Database and raw SQL:**
 - `Database` (one per named connection) owns a pooled `DbDataSource` for the primary and each replica.
 - Raw SQL with `SqlQueryAsync<T>`, `ExecuteSqlAsync`, `SqlScalarAsync<T>` and `PingAsync`. Raw SQL is written as
   interpolated strings (`SqlText`): every hole becomes a typed parameter, and `Sql.Identifier(...)` holes become
   quoted identifiers. Values never reach the SQL text.
 - Failures and cancellation come back as `Result` errors, never exceptions.
- **Data sources:** `InitializingDataSource` is a base for data sources that need per-connection setup.
- **Command pipeline:** every command runs through one middleware chain (`ICommandMiddleware`, `CommandContext`,
 allocation-free `CommandNext<T>`). Application `IDataInterceptor`s run outermost: they can rewrite the SQL,
 refuse a command or observe its result. Caching, routing, resilience and telemetry plug into this chain.
- **Value converters:** one conversion layer for every boundary (reading rows, entity writes, raw SQL parameters,
 scalars).
 - `ValueConverter<TModel, TStore>` with a global `ValueConverters` registry and per-column `[Column(Converter = ...)]`.
 - Typed ids implement `IDataValue<TSelf, TStore>` (static abstract members, no reflection).
 - `EnumToStringConverter<TEnum>` stores enums by name.
 - `JsonValueConverter<T>` stores JSON columns through System.Text.Json source-gen metadata.
- **Background jobs:** `DataScheduler` runs CL.Data's background work on CL.Common's `JobScheduler`, with one
 clock (`TimeProvider`), no overlapping runs and failures reported per run. CL.Data now depends on CodeLogic.Common.
- **SQL tree:** statements are closed record trees (`SqlExpr`, `SqlStatement`: select with joins, grouping,
 HAVING, paging and locks; insert with RETURNING; update; limited delete; upsert).
 - `SqlWriter` renders them through dialect hooks with exhaustive switches, adding only the parentheses that
   precedence needs.
 - Limits and offsets are parameters, so the SQL text does not change with their values.
 - Literals accept only numbers and booleans.
- **LINQ translation** (`SqlTranslator`, strict). Anything that touches a column must translate or fail with
 `data.translation_failed`; values independent of the row are evaluated once and bound as parameters.
 - **Null handling:** comparisons with null (including captured nulls) become `IS NULL`.
 - **Booleans:** boolean columns used as conditions become `= TRUE`.
 - **Strings:** `Contains`/`StartsWith`/`EndsWith` become escaped LIKE patterns.
 - **Collections:** collection `Contains` becomes `IN`. This covers C# 14+ span-bound array calls, including the
   three-argument `MemoryExtensions.Contains` that nullable and enum arrays bind to. Long lists are split, and a
   null entry in the list matches NULL rows.
 - **Converters:** compared values go through the column's converter (enums stored as text bind their names).
 - **Functions:** arithmetic, string, math and date/time functions, plus `??` and `?:`.
- **SQL functions:** `SqlFn` holds query functions that work in `Where` too, not only in projections and updates.
 - Date parts, `BucketUtc`, `UtcNow`, `Coalesce`/`IfNull`, `Least`/`Greatest`, string functions, `Like`, and generic-math
   `Round`/`Floor`/`Ceiling`/`Abs`.
 - `SqlFn.Json.Value/Exists/Contains` and `SqlFn.FullText.Match` translate to neutral function nodes that each
   provider renders.
- **Dialect hooks:** conditions and boolean literals are rendered through dialect hooks (`RenderConditionLiteral`),
 `SqlPredicates.IsPredicate` is public, and dialects declare when an upsert needs a transaction
 (`UpsertRequiresTransaction`, `UpsertParameterMultiplier`).
- **Unified write path:** `WritePlanner` plans every entity write (insert, update, soft/hard delete, delete by key,
 upsert with overwrite, increments and ignore) in one place. It covers:
 - audit columns, tenant stamping and checks, and optimistic concurrency (integer tokens bumped after success;
   rowversion tokens returned);
 - server-generated identity (`RETURNING`/`OUTPUT`, or last-insert-id);
 - batching by the provider's parameter limit;
 - cache tags (`table:` and `entity:`).
- **Write context:** `IDataUserAccessor` and `IDataTenantAccessor` supply the current user and tenant.
- **Repositories:** `db.Repository<T>()`.
 - Writes: `InsertAsync`/`InsertManyAsync` write generated identities back, `UpdateAsync` (also for selected
   properties), `DeleteAsync` (soft), `HardDeleteAsync`, `DeleteByIdAsync`/`DeleteByKeyAsync`.
 - Reads: `GetByIdAsync`/`GetByKeyAsync` (composite keys), `GetAllAsync`, `FindAsync`, `CountAsync`, and
   `GetPagedAsync`, which counts only when asked and always sets `HasMore`.
 - Reads hide soft-deleted rows and other tenants' rows. A stale update fails with `data.concurrency`; a duplicate
   unique value fails with `data.duplicate_key`.
- **Write execution:** multi-statement upserts run in a serializable transaction. Generated values come from
 RETURNING/OUTPUT, or from the provider's last-insert-id. When the provider's RETURNING does not keep insert order,
 rows with generated keys are inserted one per statement.
- **Upserts and counters:** `UpsertAsync`/`UpsertManyAsync` (conflict on the key, a unique index or chosen properties),
 `UpsertWithIncrementsAsync`, `InsertIgnoreAsync` (`InsertOutcome.Inserted`/`Duplicate`) and `InsertManyIgnoreAsync`,
 plus atomic `IncrementAsync`/`DecrementAsync`/`AdjustAsync` (generic math). Each update also writes the audit
 columns and bumps the concurrency token.
- **Query builder:** `db.Query<T>()` is immutable and translates its lambdas at the terminal call.
 - Shaping: `Where`, `OrderBy`/`ThenBy` (and descending), `Take`/`Skip`, `Exclude`, `IncludeDeleted`.
 - Subqueries: correlated `WhereExists`/`WhereNotExists`, and `WhereIn`/`WhereNotIn` (the inner entity's filters apply).
 - Reads: `ToListAsync`, `FirstOrDefaultAsync`, `CountAsync`, `AnyAsync`, `Max`/`Min`/`Sum`/`AverageAsync`.
   Aggregates over no rows return null, and sums return zero.
 - Set-based writes: `UpdateAsync(x => new T { ... })` (values may use the row's columns; audit columns are written and
   the token bumped) and `DeleteAsync` (soft unless `hard`).
- **Paging and streaming:** `ToPagedListAsync` (offset paging, count only on request), `ToCursorPageAsync` and
 `AsAsyncEnumerable`.
 - Cursor paging is keyset paging: the order is completed with the key, so ties are broken and the cost does not grow
   with depth. Cursors from a differently ordered query fail with `data.invalid_cursor`.
 - Streams hold one connection while you enumerate. A failure mid-stream throws `DataQueryException`, the one place
   CL.Data throws.

---

Release notes truncated to fit NuGet's 35 000 character limit. The complete changelog ships inside this package as CHANGELOG.md.