Ark.Rapid.Database 0.0.23

dotnet add package Ark.Rapid.Database --version 0.0.23
                    
NuGet\Install-Package Ark.Rapid.Database -Version 0.0.23
                    
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="Ark.Rapid.Database" Version="0.0.23" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ark.Rapid.Database" Version="0.0.23" />
                    
Directory.Packages.props
<PackageReference Include="Ark.Rapid.Database" />
                    
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 Ark.Rapid.Database --version 0.0.23
                    
#r "nuget: Ark.Rapid.Database, 0.0.23"
                    
#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 Ark.Rapid.Database@0.0.23
                    
#: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=Ark.Rapid.Database&version=0.0.23
                    
Install as a Cake Addin
#tool nuget:?package=Ark.Rapid.Database&version=0.0.23
                    
Install as a Cake Tool

Ark.Rapid.Database

1. Install

dotnet add package Ark.Rapid.Database

That's it — no further setup. AI-SPEC.md, ai-spec.json, IMPLEMENTATION.md, and VALIDATION.md are copied automatically into your own project's build output on the next build — e.g. bin/Debug/net9.0/Ark.Rapid.Database/ if your agent is scoped to this project's folder, or <YourProject>/bin/Debug/net9.0/Ark.Rapid.Database/ if it's scoped to a multi-project solution root instead — so an AI coding agent working in your repo can read them locally without any network access. The prompts below assume that's already happened — build once after installing, then hand one of these to your agent.

2. Prompts for your AI coding agent

Paste one of these into Claude, Codex, Copilot, or any other coding agent working in your repo. Each one tells the agent exactly which bundled file to read first, so it grounds the generated code in this version's real behavior instead of guessing.

If your agent reports the file doesn't exist: bin/ and obj/ are almost always git-ignored, and many agents' code-search tools skip ignored paths by default — so a search can come back empty even right after a successful build. The **/bin/**/... glob below is deliberate: it resolves whether the agent is scoped to this project's own folder or to a multi-project solution root. If it still can't find the file, tell it to search the filesystem directly (e.g. a plain find/glob/ls call) instead of its indexed or .gitignore-aware search, or just give it the resolved path once you've located it yourself.

Wire up configuration + dependency injection

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md sections 1-2 in this project. Add an
ArkDatabaseOptions/ArkDatabaseConnectionOptions config model, an AddArkRapidDatabase(IConfiguration)
DI extension, and an "ArkDatabase:Databases" section in appsettings.json for a database named
"default" using <SQLite|MariaDB|PostgreSQL>. Wire it into Program.cs.

Migrate an existing database to Ark.Rapid.Database

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md sections 1-6 and 8, and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 8.1, in this project. I'm moving <existing
data-access code, e.g. "EF Core against SQL Server" or "raw ADO.NET against MySQL"> to
Ark.Rapid.Database targeting <SQLite|MariaDB|PostgreSQL>. For each existing table, map its
columns to the DataType/Constraint conventions in AI-SPEC.md section 8.1 and generate an
equivalent static schema class with an EnsureCreatedAsync method, then rewrite the existing
CRUD code to go through ArkDbManager using the config/DI pattern from sections 1-2 and the
provider-portable insert/read pattern from section 6. Do not port any hand-built SQL string
concatenation as-is — rebuild it using InsertTableAsync/UpdateTableAsync's dictionaries or
GetSqlValueAsync, per the security checklist in section 8.

Define a table schema and keep it idempotent

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 3-4 in this project. Generate a static
schema class for a table named "<table>" with columns: <column: type, column: type, ...>, plus an
EnsureCreatedAsync method, following the ColumnProp/Constraint conventions in
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 8.

Add a column safely, later

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 5.1 in this project. Add a migration step
that adds a "<column>" column of type <type> to the "<table>" table, appended to the existing
migration list so it's safe to run on every startup.

Change a column's type or default, via ModifyColumnAsync or expand/contract

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 5.3 in this project. Decide whether a
single ModifyColumnAsync call is enough to change "<table>.<column>" from <old type>/<old default>
to <new type>/<new default>, or whether the expand/contract pattern (backfill statement plus a
follow-up drop step for the retired column, spread across deploys) is the better fit — a large
PostgreSQL table or a need for a safe rollback window are the two reasons to prefer expand/contract
over ModifyColumnAsync's single, immediate, full-column-rewrite call. Write whichever migration.

Generate a repository class

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 9 and **/bin/**/Ark.Rapid.Database/AI-SPEC.md section 7.1 in this
project. Generate a repository class for "<Entity>" backed by ArkDbManager, with Create/Update/Get
methods that stay correct across SQLite, MariaDB, and PostgreSQL (do not cast InsertTableAsync's
return value directly).

Add vector search

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 7 and **/bin/**/Ark.Rapid.Database/AI-SPEC.md section 9 in this
project. Add an "embedding" BLOB column to "<table>" and a similarity-search method, gated so it
only runs against SqliteManager.

Write a portable raw SQL query (aggregates, dates, JSON, paging) that runs unmodified on SQLite, MariaDB, and PostgreSQL

Read **/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Before hand-writing any
dialect-specific SQL fragment inside a raw query string passed to ExecuteAsync/ExecuteSelectAsync/
SelectAsync/FirstAsync/ExecuteScalarAsync, check whether one of these <db>.Util (DatabaseUtil)
methods already covers it, and use the method instead of the literal syntax:

- Qry_Now() / Qry_UtcNow() — current timestamp, local or UTC
- Qry_Date_Day(days) — a date N days in the past/future
- Qry_To_Date(col) — cast/truncate a column to a date
- Qry_IfNull(col, default) — null-coalesce
- Qry_Sum(col) — aggregate sum
- Qry_Greatest(...values) / Qry_Least(...values) — row-wise (scalar) greatest/least of 2+
  expressions, e.g. clamping a shortfall at zero: Qry_Greatest("0", "required - filled"). This is
  NOT the same as the aggregate MAX()/MIN() over rows. Mind the per-provider NULL-handling
  differences documented on the method.
- Qry_Bool(bool) — a boolean literal safe for a native/conventional boolean column (required on
  PostgreSQL, whose boolean type rejects bare 1/0); do not confuse with GetSqlValueAsync(bool),
  which targets columns this library itself created via CreateTableAsync.
- Qry_Concat(...values) — string concatenation (mind the per-provider NULL-handling differences
  documented on the method)
- Qry_Limit(limit, offset) — LIMIT/OFFSET paging; on the SQL Server branch this requires the query
  to already have an ORDER BY
- Qry_Diff_Hrs(end, start) / Qry_Diff_Mins(end, start) / Qry_Diff_Days(end, start) — datetime
  difference. Qry_Diff_Days is always fractional on every provider (unlike Qry_Diff_Hrs/
  Qry_Diff_Mins, which truncate to a whole HOUR/MINUTE on MariaDB/mssql) — use it for duration/
  average reports (e.g. "average days a request has stayed open") where whole-day truncation would
  lose most of the signal.
- JsonExtract(jsonCol, path) — extract a field from a JSON column

Only fall back to writing the dialect-specific SQL yourself, gated per provider, if none of these
cover what you need — and if so, consider adding a new Qry_* method to DatabaseUtil.cs instead of
inlining it at the call site, so the next query gets it for free.

The prompt above is the general-purpose one — it works for any raw query. For a specific, common scenario, these narrower prompts point the agent straight at the matching worked pattern in IMPLEMENTATION.md section 10, so it doesn't have to compose the fragments itself:

Relative-date filter (e.g. "records from the last N days")

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.1 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that returns rows where "<date_column>" falls within the last <N> days, using
db.Util.Qry_Date_Day(-<N>) for the cutoff and db.Util.Qry_To_Date("<date_column>") if
<date_column> is a datetime column being compared against a date boundary. Run it through
ExecuteSelectAsync<<Entity>> and order by <date_column> descending.

Null-safe default / coalesce

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.2 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that selects "<column>" but substitutes <default_value> when it's NULL, using
db.Util.Qry_IfNull("<column>", "<default_value>"). Quote <default_value> yourself if it's a
string literal (e.g. "'unknown'") — Qry_IfNull interpolates it as-is.

Aggregate sum / totals report

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.3 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that groups by "<group_by_column>" and returns the sum of "<amount_column>" as
"<alias>", using db.Util.Qry_Sum("<amount_column>") wrapped in db.Util.Qry_IfNull(..., "0") so
groups with only NULL values report 0 instead of NULL. Run it through db.SelectAsync since the
result is an ad hoc aggregate shape, not an existing entity type.

Clamp a computed value to a floor or ceiling (row-wise, not an aggregate)

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.4 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that computes "<expression>" (e.g. "<required_column> - <filled_column>") per row and
clamps it to a minimum of <floor> using db.Util.Qry_Greatest("<floor>", "<expression>") — or a
maximum of <ceiling> using Qry_Least instead. If either input column can be NULL, wrap each one
individually in db.Util.Qry_IfNull(..., "0") before passing it to Qry_Greatest/Qry_Least, per the
per-provider NULL-handling table in AI-SPEC.md section 11 — do not skip this if the columns are nullable.

Filter or write against a hand-modeled boolean column

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.5 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that filters "<bool_column>" to <true|false> using db.Util.Qry_Bool(<true|false>) as
the literal, e.g. `WHERE <bool_column> = {db.Util.Qry_Bool(true)}`. First confirm <bool_column>
is an existing/hand-modeled boolean or 1/0 integer column, not one created by this library's own
CreateTableAsync with ColumnProp.DataType = typeof(bool) — that case needs
await db.GetSqlValueAsync(<true|false>) instead; the two are not interchangeable for the same
column (see AI-SPEC.md section 11).

Build a display or search string from multiple columns

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.6 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that projects "<alias>" as the concatenation of <column1>, a literal separator, and
<column2> (e.g. first/last name), using db.Util.Qry_Concat("<column1>", "' '", "<column2>"). If
this is also used in a WHERE ... LIKE clause against a caller-supplied search term, inline the
term via `await db.GetSqlValueAsync($"%{<searchTerm>}%")` — never interpolate <searchTerm>
directly into the query string (AI-SPEC.md section 7).

Paginated listing

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.7 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a paginated query
against "<table>" for page <page> of size <pageSize>, ordered by "<order_column>", using
db.Util.Qry_Limit(<pageSize>, (<page> - 1) * <pageSize>) appended after the ORDER BY clause. Every
provider requires the ORDER BY to be present for this to be deterministic, so don't omit it even
though only the SQL Server branch enforces it structurally.

Elapsed time / duration report between two datetime columns

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.8 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that returns the <hours|minutes> elapsed between "<start_column>" and "<end_column>" as
"<alias>", using db.Util.Qry_Diff_Hrs("<end_column>", "<start_column>") or
db.Util.Qry_Diff_Mins("<end_column>", "<start_column>") — note the argument order is (end, start).
If aggregating this across rows (e.g. total hours per <group_by_column>), wrap it in
db.Util.Qry_Sum(...) and then db.Util.Qry_IfNull(..., "0") so groups with no rows report 0. If the
unit you actually need is fractional days (e.g. an average duration), use
db.Util.Qry_Diff_Days(...) instead — see the next prompt below.

Convert a hand-written duration/date-diff expression (e.g. PostgreSQL EXTRACT(EPOCH FROM ...)) to a portable, fractional-day average or duration report

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.11 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. I have this existing raw SQL
query that only works on one provider (paste the query). Rewrite it so it runs unmodified on
SQLite, MariaDB, and PostgreSQL:

1. Replace any hand-written datetime-difference expression (e.g. PostgreSQL's
   `EXTRACT(EPOCH FROM (a - b))/<divisor>`, or an equivalent on another provider) with
   db.Util.Qry_Diff_Days("<end_expr>", "<start_expr>") if the result is reported as a fractional
   day count (an average, or any duration where truncating to whole days would lose most of the
   signal — e.g. "average days a request has stayed open"), or db.Util.Qry_Diff_Hrs/Qry_Diff_Mins
   (IMPLEMENTATION.md section 10.8) if whole-unit truncation is fine instead. Note the argument
   order is (end, start) for all three. If "now" is one side of the diff, use db.Util.Qry_UtcNow()
   rather than a provider-specific current-timestamp expression.
2. Replace any other dialect-specific fragment in the same query the same way — e.g. PostgreSQL's
   `GREATEST(a, b)`/`LEAST(a, b)` with db.Util.Qry_Greatest("<a>", "<b>")/Qry_Least(...)
   (IMPLEMENTATION.md section 10.4), and `COALESCE(col, default)` with
   db.Util.Qry_IfNull("<col>", "<default>") (section 10.2) — per the full helper list in
   AI-SPEC.md section 11.
3. Leave ANSI-standard constructs alone (COUNT, SUM, AVG, CASE WHEN, EXISTS/NOT EXISTS, IN, JOIN) —
   no Util wrapper exists or is needed for them.
4. If any comparison value in the query is caller-supplied or computed at runtime (a cutoff date, a
   threshold, a search term) rather than a fixed constant, replace it with
   await db.GetSqlValueAsync(<value>) instead of leaving it as a literal in the SQL text, per the
   security checklist in IMPLEMENTATION.md section 8.

Run the result through db.SelectAsync unless its shape matches an existing entity type, in which
case use db.ExecuteSelectAsync<<Entity>> instead.

Query a field inside a JSON column

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.9 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. Write a query against
"<table>" that filters or projects the "<json_path>" field of the JSON column "<json_column>",
using db.Util.JsonExtract("<json_column>", "<json_path>"). If comparing it against a literal
value, inline that value via `await db.GetSqlValueAsync(<value>)` rather than a bare string.

Combined multi-helper report (the common real-world case)

Read **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md section 10.10 and
**/bin/**/Ark.Rapid.Database/AI-SPEC.md section 11 in this project. I need a report query
against "<table>" grouped by "<group_by_column>" that computes <describe the metric — e.g. "total
hours between planned_start and planned_end, defaulting to 0" or "open positions clamped at
zero">. Compose it from the matching db.Util.Qry_* helpers (Qry_Sum, Qry_IfNull, Qry_Diff_Hrs,
Qry_Greatest, Qry_Date_Day, Qry_To_Date, Qry_Concat, JsonExtract, Qry_Bool, Qry_Limit as needed)
rather than hand-writing any dialect-specific SQL, nesting them in the order the underlying value
flows (e.g. Qry_IfNull(Qry_Sum(Qry_Diff_Hrs(end, start)), "0")). Run it through db.SelectAsync
unless the shape matches an existing entity type, in which case use
db.ExecuteSelectAsync<<Entity>> instead.

Validate a query or repository method against every known cross-provider failure mode

Read **/bin/**/Ark.Rapid.Database/VALIDATION.md in this project — it is a pass/fail checklist
(V1, V2, ...) of every known failure mode in this library, each with a Trigger/Symptom/Root
cause/Fix and a cross-reference to the fuller explanation in AI-SPEC.md/IMPLEMENTATION.md. Check
<the query, repository method, or file — paste it, or name it> against every item whose "Trigger"
matches something in it: any GROUP BY combined with a JOIN (V1), any cast of
InsertTableAsync/ExecuteQueryAsync's return value (V2), any FirstAsync<T> call on PostgreSQL whose
caller treats a default/null result as "no row" (V3), any IsTableExistAsync precheck before
CreateTableAsync (V4), any ColumnProp.Default/ConstraintName usage (V5/V6), any nullable
ColumnProp.DataType (V7), any Qry_Greatest/Qry_Least/Qry_Concat call with a possibly-NULL operand
not wrapped in Qry_IfNull (V8), any Qry_Bool/GetSqlValueAsync(bool) usage against a boolean column
(V9), any hand-written PostgreSQL datetime subtraction (V10), any reliance on CreateSchemaAsync
provisioning errors surfacing (V11), any mixed-case identifier read through
GetSafeTableName/GetSafeColumnName (V12), any GetSchemasAsync(partial_name) call expecting
filtering (V13), any ModifyColumnAsync call (V14), any external/caller-supplied value not
routed through GetSqlValueAsync (V15), and — if V1 applies — whether the joined table's id column
actually has a real PostgreSQL PRIMARY KEY, since a table created with Constraint.Primary_AutoIncrement
before package version 0.0.20 does not, and V1's fix silently does nothing on it (V16). For every item that matches, read its full write-up in
VALIDATION.md (and the AI-SPEC.md/IMPLEMENTATION.md section it references) before deciding whether
it's actually a problem here, then fix it using the pattern shown there. Report which items you
checked, which ones actually applied, and what you changed for each — including ones you confirmed
don't apply and why, so nothing was skipped rather than checked.

Fix a GROUP BY 42803 that persists after applying VALIDATION.md V1's fix (PostgreSQL)

Read **/bin/**/Ark.Rapid.Database/VALIDATION.md sections V1 and V16, and
**/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md sections 10.12-10.13, in this project. I'm getting
Npgsql.PostgresException 42803 ("column ... must appear in the GROUP BY clause or be used in an
aggregate function") on this query even though I've already added the joined table's own id
column to GROUP BY (paste the query). This is expected if the id column was created with
Constraint.Primary_AutoIncrement by an Ark.Rapid.Database build before 0.0.20 — that version never
gave such a column a real PostgreSQL PRIMARY KEY, so grouping by it doesn't satisfy the
functional-dependency rule V1's fix depends on (VALIDATION.md V16). Rewrite the query to list
every selected non-aggregate column explicitly in GROUP BY (the IMPLEMENTATION.md §10.13
workaround) so it works regardless of whether that constraint exists. Then check every other raw
GROUP BY query in this project for the same pattern (a driving-table GROUP BY plus a JOINed
table's other columns selected ungrouped) and apply the same fix to each one you find — do not
stop at the query I pasted. Report the Ark.Rapid.Database package version this project references
and note, separately, that upgrading to 0.0.20+ only gives newly created tables a real primary
key — existing tables need an explicit `ALTER TABLE <table> ADD PRIMARY KEY (<id_column>);`
migration per table if the shorter V1 form is wanted back; do not run that migration yourself
without being asked to, since it changes a live schema.

Sweep the whole project: migrate every existing raw SQL query to Ark.Rapid.Database's portable helpers

Read **/bin/**/Ark.Rapid.Database/AI-SPEC.md sections 7 and 11 and
**/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md sections 6 and 10 in this project. Find every raw
SQL string this project already executes — through ArkDbManager's ExecuteAsync/ExecuteSelectAsync/
SelectAsync/FirstAsync/ExecuteScalarAsync, and any leftover ADO.NET/Dapper/EF Core raw-SQL calls
not yet routed through ArkDbManager at all — and convert each one so it runs unmodified on SQLite,
MariaDB, and PostgreSQL. For every query found:

1. Identify each dialect-specific fragment (current date/time, relative-date filters, date
   truncation, NULL-coalescing, SUM/aggregate totals, row-wise clamping, boolean literals, string
   concatenation, LIMIT/OFFSET paging, datetime differences, JSON field access) and replace it
   with the matching db.Util.Qry_* method — Qry_Now/Qry_UtcNow, Qry_Date_Day, Qry_To_Date,
   Qry_IfNull, Qry_Sum, Qry_Greatest/Qry_Least, Qry_Bool, Qry_Concat, Qry_Limit, Qry_Diff_Hrs/
   Qry_Diff_Mins/Qry_Diff_Days, JsonExtract — per the worked patterns in IMPLEMENTATION.md section
   10 (section 10.11 for Qry_Diff_Days specifically — prefer it over Qry_Diff_Hrs/Qry_Diff_Mins
   whenever the result is a fractional-day average/duration). Do not
   leave any hand-written dialect-gated branch in place if a Qry_* helper already covers it; only
   keep a per-provider branch for the rare fragment none of them cover, and flag that spot as a
   candidate to add to DatabaseUtil.cs instead.
2. Replace any query built by concatenating or interpolating external input (user input, search
   terms, filter values) with parameters produced by `await db.GetSqlValueAsync(...)`, per the SQL
   construction/security model in AI-SPEC.md section 7 — treat every such spot you find as a SQL
   injection bug to fix, not just a portability one.
3. Preserve existing behavior exactly while converting: match the per-provider NULL-handling table
   in AI-SPEC.md section 11 (wrap nullable columns in Qry_IfNull before passing them to
   Qry_Greatest/Qry_Least/Qry_Concat/Qry_Diff_Hrs), keep an ORDER BY on every query that gains a
   Qry_Limit call, and don't conflate Qry_Bool(bool) (for pre-existing/hand-modeled boolean or
   1/0 columns) with GetSqlValueAsync(bool) (for columns this library created itself via
   CreateTableAsync with ColumnProp.DataType = typeof(bool)) — they are not interchangeable on the
   same column.
4. Route each converted query's results through ExecuteSelectAsync<TEntity> when the shape
   matches an existing entity/DTO, or SelectAsync for ad hoc/aggregate shapes, following the
   provider-portable insert/read pattern in IMPLEMENTATION.md section 6 — don't introduce a new
   mapping mechanism alongside it.
5. Leave everything else (schema declarations, migrations, DI registration) untouched — this pass
   is scoped to the query layer, not the schema.

When done, list every file and query you converted, the dialect-specific syntax or injection risk
each one had before, and the Qry_* helper(s) or GetSqlValueAsync call that replaced it, plus any
fragment you had to leave gated per-provider because no helper covers it yet.

Upgrade to a newer package version

Read **/bin/**/Ark.Rapid.Database/AI-SPEC.md section 12 (Versioning) in this project and note
the current package version plus every ArkDbManager member this project already calls. Then
run `dotnet add package Ark.Rapid.Database` to pull the latest version, rebuild so the
refreshed **/bin/**/Ark.Rapid.Database/IMPLEMENTATION.md, AI-SPEC.md, and ai-spec.json are
copied in, and re-read all three. List every behavior change, newly-unimplemented member, or
newly-supported capability (e.g. an added provider, or a method that used to throw
NotImplementedException and no longer does) that affects this project's existing usage, then
update the code to match — don't leave a call site relying on behavior that changed
underneath it.
Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.23 79 9/24/2026
0.0.22 86 9/10/2026
0.0.21 99 9/9/2026
0.0.20 90 9/9/2026
0.0.19 88 9/9/2026
0.0.18 88 9/9/2026
0.0.17 89 9/8/2026
0.0.16 90 9/8/2026
0.0.15 107 9/8/2026
0.0.14 92 9/8/2026
0.0.13 91 9/8/2026
0.0.12 103 9/7/2026
0.0.11 170 6/29/2026
0.0.10 120 6/27/2026
0.0.9 152 4/16/2026
0.0.8 119 4/16/2026
0.0.7 141 4/7/2026
0.0.6 453 11/20/2025
0.0.5 247 10/28/2025
0.0.4 225 10/14/2025
Loading failed

0.0.20 — all three providers (Postgres/TablePostgreScript.cs, Maria/TableMariaScript.cs,
Sqlite/TableSqliteScript.cs, and each provider's *Manager.cs), plus
VALIDATION.md/AI-SPEC.md/IMPLEMENTATION.md/README.md/ai-spec.json docs.

Added: VALIDATION.md — a new companion doc alongside AI-SPEC.md/IMPLEMENTATION.md/README.md, packed
the same way (docs\VALIDATION.md in the .nupkg, copied to every consuming project's own build
output). It is a scannable pass/fail checklist (stable IDs V1, V2, ...) of every known cross-
provider failure mode in this library, each with a Trigger/Symptom/Root cause/Fix and a cross-
reference back to the full detail in AI-SPEC.md/IMPLEMENTATION.md — meant to be run against code
right after writing or generating it, not just read once.

Added: V1, a previously-undocumented failure mode found and fixed in a consuming project —
PostgreSQL rejects a GROUP BY query that selects a JOINed table's other columns ungrouped unless
that table's own primary key (not just a foreign key that happens to equal it) is also in the
GROUP BY list ('42803: column ... must appear in the GROUP BY clause or be used in an aggregate
function'). SQLite/MariaDB run the identical query without complaint, so this is easy to ship
unnoticed if development/CI runs SQLite. See VALIDATION.md §V1, AI-SPEC.md §11, and
IMPLEMENTATION.md §10.12 for the full explanation and worked before/after fix.

Fixed: Constraint.Primary_AutoIncrement now adds a table-level PRIMARY KEY constraint on
PostgreSQL, matching Constraint.Primary. Before this fix, an autoincrement id column (the standard
way every "id" column in the AI-SPEC.md worked examples is declared) only got the column-level
GENERATED ALWAYS AS IDENTITY clause on PostgreSQL — no PRIMARY KEY, no UNIQUE constraint, nothing.
This silently invalidated V1's documented GROUP BY fix above, since PostgreSQL's
functional-dependency rule (which that fix relies on) only fires for a column with a real PRIMARY
KEY constraint. Found while debugging a live 42803 error that persisted even after applying V1's
fix exactly as documented.

Existing tables created by a pre-0.0.20 build are NOT retroactively fixed by upgrading —
CREATE TABLE IF NOT EXISTS is a no-op against a table that already exists, constraint gap and all.
They need either a one-time "ALTER TABLE ... ADD PRIMARY KEY (id)" migration per table, or every
affected query needs to list its selected non-aggregate columns explicitly in GROUP BY instead of
relying on V1's shorter "group by the joined table's id" form. See VALIDATION.md §V16, AI-SPEC.md
§8.2, and IMPLEMENTATION.md §10.13 for the full explanation and worked example of both options.

Fixed: Constraint.Default/ColumnProp.Default now works on SQLite and PostgreSQL — both previously
had no effect at all. SQLite's Constraint.Default case emitted an empty, uniquely-named
CONSTRAINT CST_DEF_xxxxx fragment and never appended DEFAULT <value>; PostgreSQL's
create-table/add-column generators had no case for it whatsoever. MariaDB was the only provider
where Default did anything, and even there the value was guessed from the raw string alone rather
than driven by ColumnProp.DataType. All three now parse Default against DataType (Nullable<T>
unwrapped) and re-format it through the same literal-formatting path an inserted value uses, so a
column's default and an app-supplied value for it always agree on format — across every CLR type
each provider's type-mapper recognizes, for CreateTableAsync, AddColumnAsync, and
ModifyColumnAsync.

Added: two case-insensitive keyword sentinels for ColumnProp.Default, independent of DataType —
"CURRENT_TIMESTAMP" (the engine's own now) and "UTC_TIMESTAMP" (guaranteed current UTC instant
regardless of session timezone: MariaDB's UTC_TIMESTAMP(), Postgres's
(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), SQLite's CURRENT_TIMESTAMP). Use "UTC_TIMESTAMP" for a
DateTime column that must always default to a real UTC instant on every provider.

Fixed: ModifyColumnAsync no longer throws NotImplementedException on any provider. MariaDB's and
PostgreSQL's previously-commented-out implementations are wired up (PostgreSQL's now also
sets/drops Default and NotNull). SQLite gets a new table-rebuild implementation (SQLite has no
ALTER COLUMN): rename the table aside, CREATE it fresh with the target column redefined and every
sibling column's original definition preserved verbatim, copy the data across, drop the renamed
original. All three share full-redefinition semantics matching MariaDB's own MODIFY COLUMN — a
constraint absent from the given ColumnProp is dropped, not left as-is. Known limitation: the
SQLite rebuild does not support a table with hand-written table-level constraints, and
PostgreSQL's ALTER COLUMN ... TYPE always rewrites the column even if only the default changed.

Fixed: SQLite's literal DateTime formatting (GetSqliteValue) used a 12-hour clock with no AM/PM
designator and appended the executing machine's local UTC offset regardless of the value's own
timezone info — both silent correctness bugs for any UTC DateTime, discovered while building this
release's cross-provider test. Now an unambiguous 24-hour, offset-free format whose date+time
prefix matches SQLite's own CURRENT_TIMESTAMP output.

Fixed: MariaDB's column-constraint clause order was caller-array-order-dependent, which is what
made a string Default paired with NotNull unreliable. Now canonical (NotNull, then Default, then
Primary_AutoIncrement) regardless of the order ColumnProp.Constraints lists them in.

Verified with a 42-check cross-provider test (SQLite, a local MariaDB container, a local
PostgreSQL container) covering CreateTableAsync/AddColumnAsync/ModifyColumnAsync with a DateTime
UTC_TIMESTAMP default, a literal DateTime default, and one default each for
bool/decimal/string/Guid — including confirming AddColumnAsync backfills pre-existing rows and
ModifyColumnAsync preserves pre-existing rows/sibling columns intact through Postgres's rewrite
and SQLite's rebuild.

Docs: VALIDATION.md gains §V1 and §V16 (cross-referenced from each other); AI-SPEC.md §5, §8.1
(bool/SQLite type-mapping row correction), §8.2, §11, and §12 are corrected/updated to match;
IMPLEMENTATION.md gains §10.12 and §10.13 with worked before/after patterns; README.md's "validate
a query" checklist prompt is extended to include V1 and V16, and gains prompts for the §10.13
workaround and for auditing which tables still need the migration.

Full detail and the machine-readable change log: see docs/AI-SPEC.md §12 and docs/ai-spec.json
(changeLog, entry for 0.0.20; capabilities.columnDefaultValueSupport_0_0_20 /
capabilities.modifyColumnAsyncImplemented_0_0_20 / capabilities.postgresAutoIncrementPrimaryKeyFix)
inside this package, or AI-SPEC.md/ai-spec.json in the repository.

0.0.18 — DatabaseUtil (Common/DatabaseUtil.cs), plus IMPLEMENTATION.md/README.md docs.

Added: Qry_Diff_Days(end, start) — day difference between two datetime columns, always fractional
(seconds-based) on every provider. Unlike Qry_Diff_Hrs/Qry_Diff_Mins, which follow each provider's
native truncation for their unit (whole HOUR/MINUTE on MariaDB/mssql), Qry_Diff_Days never
truncates — use it for duration/average reports (e.g. "average days a request has stayed open")
where whole-day truncation would lose most of the signal. Casts both operands to ::timestamptz on
PostgreSQL. Argument order is (end, start), same as Qry_Diff_Hrs/Qry_Diff_Mins.

Fixed: Qry_Diff_Mins's PostgreSQL branch now casts both operands to ::timestamptz before
subtracting, matching Qry_Diff_Hrs. Previously, diffing a `timestamp without time zone` expression
(e.g. Qry_UtcNow(), which returns "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'") against a `timestamptz`
column relied on Postgres's implicit cast of the naive side through the session's timezone, which
silently returns a wrong result whenever that session timezone isn't UTC.

Docs: added IMPLEMENTATION.md section 10.11 (average/fractional-day duration report worked
pattern — the Qry_Diff_Days counterpart to section 10.8) and a matching README.md AI-coding-agent
prompt for converting an existing hand-written, single-provider duration expression to this
pattern.

Full detail and a machine-readable change log for coding agents: see docs/AI-SPEC.md §11 and
docs/ai-spec.json (changeLog, capabilities.databaseUtilAdditions_0_0_18) inside this package, or
AI-SPEC.md/ai-spec.json in the repository.

0.0.17 — README.md only; no library code or runtime behavior changed.

Added: a new AI-coding-agent prompt, "Sweep the whole project: migrate every existing raw SQL
query to Ark.Rapid.Database's portable helpers." Unlike the existing per-scenario prompts (each
scoped to one new query), this one instructs an agent to audit an entire existing codebase —
including raw SQL not yet routed through ArkDbManager at all — replace every dialect-specific
fragment with the matching DatabaseUtil Qry_* method, replace concatenated external input with
GetSqlValueAsync per the security model in AI-SPEC.md §7, and report what it converted.

Full detail: see the "Sweep the whole project" prompt in README.md (repository root or packed at
the package root as PackageReadmeFile).

0.0.16 — DatabaseUtil only (Common/DatabaseUtil.cs); purely additive, no existing method's behavior
changed.

Added: Qry_Greatest(params string[] values) / Qry_Least(params string[] values) — row-wise
(scalar) greatest/least of 2+ expressions, e.g. clamping a shortfall at zero with
Qry_Greatest("0", "required - filled"). This is not the aggregate MAX()/MIN() over rows. NULL-
handling differs by provider if any argument can be NULL: sqlite/maria return NULL if any argument
is NULL, postgres ignores NULLs (returns NULL only if all arguments are NULL), and the mssql
emulation (no native GREATEST/LEAST pre-2022) is order-dependent and inconsistent with all three.
Wrap arguments in Qry_IfNull(value, "0") first for identical results across providers.

Added: Qry_Bool(bool value) — a boolean literal for a native/conventional boolean column ('1'/'0'
on sqlite/maria/mssql, 'TRUE'/'FALSE' on postgres — required there, since PostgreSQL's boolean type
rejects a bare 1/0 comparison). Not interchangeable with GetSqlValueAsync(bool), which targets a
column this library created itself via CreateTableAsync with ColumnProp.DataType = typeof(bool)
(stored as TEXT holding 'TRUE'/'FALSE' strings on SQLite).

Added: Qry_Concat(params string[] values) — string concatenation ( || on sqlite/postgres, CONCAT()
on maria/mssql). NULL propagates on sqlite/postgres/maria; mssql's CONCAT() treats NULL as an empty
string instead.

Added: Qry_Limit(int limit, int? offset = null) — a LIMIT/OFFSET paging fragment; the mssql branch
compiles to OFFSET/FETCH, which requires the query to already have an ORDER BY clause.

Full detail and a machine-readable change log for coding agents: see docs/AI-SPEC.md §11 and
docs/ai-spec.json (changeLog, capabilities.databaseUtilAdditions_0_0_16) inside this package, or
AI-SPEC.md/ai-spec.json in the repository.

0.0.14 — PostgreSQL provider only (TablePostgresScript.cs, PostgresManager.cs); SQLite and MariaDB unchanged.

BREAKING: InsertTableAsync now appends "RETURNING id;" instead of "RETURNING *;". The returned
dynamic row now carries only an `id` field instead of every inserted column, and the target table
must have a column literally named "id" (a different key name, a composite key, or no primary key
throws a Postgres "column \"id\" does not exist" error, or returns null for that field). There is
no flag to restore the old full-row behavior; run your own "INSERT ... RETURNING *" through
ExecuteQueryAsync/ExecuteAsync if you need it.

Fixed: nullable value types (int?, long?, DateTime?, Guid?, decimal?, enum?, etc.) passed as
ColumnProp.DataType now map to their real Postgres column type instead of always falling back to
TEXT.

Fixed: GetSqlValue (used by InsertTableAsync/UpdateTableAsync/GetSqlValueAsync) now correctly
quotes DateOnly, TimeOnly, TimeSpan (including a day component), and enum values, which previously
serialized as unquoted, syntactically invalid SQL. DBNull.Value now maps to NULL instead of an
empty string literal. JsonElement/JsonDocument/JsonObject values are now serialized via
JsonSerializer.Serialize into a proper JSON string literal for JSONB columns.

Full before/after detail, migration guidance, and a machine-readable change log for coding agents:
see docs/AI-SPEC.md §7.1-7.2/§8.1 and docs/ai-spec.json (changeLog, capabilities.insertReturnShape,
capabilities.nullableColumnTypeMapping, capabilities.postgresLiteralEncodingFixes) inside this
package, or IMPLEMENTATION.md/AI-SPEC.md/ai-spec.json in the repository.