diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml
index 208478ce..9531b944 100644
--- a/.github/workflows/create-tag.yml
+++ b/.github/workflows/create-tag.yml
@@ -10,7 +10,7 @@ on:
options:
- acp
- console
- - iii-database
+ - database
- iii-directory
- iii-lsp
- iii-lsp-vscode
diff --git a/.github/workflows/iii-database-e2e.yml b/.github/workflows/database-e2e.yml
similarity index 81%
rename from .github/workflows/iii-database-e2e.yml
rename to .github/workflows/database-e2e.yml
index 3509d249..2b09fca3 100644
--- a/.github/workflows/iii-database-e2e.yml
+++ b/.github/workflows/database-e2e.yml
@@ -1,14 +1,14 @@
-name: iii-database E2E
+name: database E2E
on:
pull_request:
paths:
- - 'iii-database/**'
- - '.github/workflows/iii-database-e2e.yml'
+ - 'database/**'
+ - '.github/workflows/database-e2e.yml'
workflow_dispatch:
concurrency:
- group: iii-database-e2e-${{ github.ref }}
+ group: database-e2e-${{ github.ref }}
cancel-in-progress: true
env:
@@ -31,14 +31,14 @@ jobs:
- name: Cache cargo registry & build
uses: Swatinem/rust-cache@v2
with:
- workspaces: iii-database
+ workspaces: database
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- cache-dependency-path: iii-database/tests/e2e/workers/harness/package-lock.json
+ cache-dependency-path: database/tests/e2e/workers/harness/package-lock.json
# GHA `services:` blocks can't pass `-c wal_level=logical` to postgres,
# which the row-change tests require. Reuse the same docker-compose
@@ -53,7 +53,7 @@ jobs:
run: iii --version
- name: Run harness
- working-directory: iii-database/tests/e2e
+ working-directory: database/tests/e2e
# --with-cargo-test runs `cargo test --all-features` against the
# already-running postgres + mysql so the gated driver/pool tests
# actually exercise their target DBs (otherwise they early-return).
@@ -63,7 +63,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
- name: iii-database-e2e-report
+ name: database-e2e-report
path: |
- iii-database/tests/e2e/reports/
+ database/tests/e2e/reports/
retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8e643bc3..0cc7d89d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,7 +5,7 @@ on:
tags:
- 'acp/v*'
- 'console/v*'
- - 'iii-database/v*'
+ - 'database/v*'
- 'iii-directory/v*'
- 'iii-lsp/v*'
- 'image-resize/v*'
diff --git a/.gitignore b/.gitignore
index cac0c6b8..8af4a57b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,7 @@ node_modules
package-lock.json
pnpm-lock.yaml
yarn.lock
-!iii-database/tests/e2e/workers/harness/package-lock.json
+!database/tests/e2e/workers/harness/package-lock.json
!shell/tests/e2e/workers/harness/package-lock.json
!console/web/pnpm-lock.yaml
diff --git a/README.md b/README.md
index 02519b33..7fa0fc12 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ asset for the host from the workers registry API.
|---|---|---|
| [`acp`](acp/) | Rust | Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions. |
| [`harness-node`](harness-node/) | Node | TS port of the iii harness stack — bundles `harness`, `turn-orchestrator`, `approval-gate`, `session`, `hook-fanout`, `auth-credentials`, `models-catalog`, `provider-anthropic`, `provider-openai`, `llm-budget`, and `context-compaction` as one pnpm monorepo. See [`harness-node/README.md`](harness-node/README.md). |
-| [`iii-database`](iii-database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. |
+| [`database`](database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. |
| [`iii-directory`](iii-directory/) | Rust | Engine introspection (functions / triggers / workers), workers-registry proxy, and filesystem-backed skill + prompt reader. |
| [`iii-lsp`](iii-lsp/) | Rust | Language Server for iii function ids, trigger configs, and worker discovery. Autocomplete / hover across JS/TS, Python, Rust. |
| [`iii-lsp-vscode`](iii-lsp-vscode/) | Node | VS Code extension that embeds `iii-lsp`. |
diff --git a/iii-database/.gitignore b/database/.gitignore
similarity index 100%
rename from iii-database/.gitignore
rename to database/.gitignore
diff --git a/iii-database/Cargo.lock b/database/Cargo.lock
similarity index 99%
rename from iii-database/Cargo.lock
rename to database/Cargo.lock
index 90b49779..cde4513c 100644
--- a/iii-database/Cargo.lock
+++ b/database/Cargo.lock
@@ -594,6 +594,46 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+[[package]]
+name = "database"
+version = "0.0.4"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "clap",
+ "deadpool-postgres",
+ "futures-util",
+ "iii-sdk",
+ "mysql_async",
+ "postgres-protocol",
+ "postgres-types",
+ "r2d2",
+ "r2d2_sqlite",
+ "rusqlite",
+ "rust_decimal",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "schemars",
+ "serde",
+ "serde_json",
+ "serde_yml",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-postgres",
+ "tokio-postgres-rustls",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "uuid",
+]
+
[[package]]
name = "deadpool"
version = "0.12.3"
@@ -1268,46 +1308,6 @@ dependencies = [
"icu_properties",
]
-[[package]]
-name = "iii-database"
-version = "0.0.4"
-dependencies = [
- "anyhow",
- "async-trait",
- "base64 0.22.1",
- "bytes",
- "chrono",
- "clap",
- "deadpool-postgres",
- "futures-util",
- "iii-sdk",
- "mysql_async",
- "postgres-protocol",
- "postgres-types",
- "r2d2",
- "r2d2_sqlite",
- "rusqlite",
- "rust_decimal",
- "rustls",
- "rustls-native-certs",
- "rustls-pemfile",
- "rustls-pki-types",
- "rustls-webpki",
- "schemars",
- "serde",
- "serde_json",
- "serde_yml",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "tokio-postgres",
- "tokio-postgres-rustls",
- "tracing",
- "tracing-subscriber",
- "url",
- "uuid",
-]
-
[[package]]
name = "iii-sdk"
version = "0.12.0-next.1"
diff --git a/iii-database/Cargo.toml b/database/Cargo.toml
similarity index 98%
rename from iii-database/Cargo.toml
rename to database/Cargo.toml
index 3b945074..99ddcbf4 100644
--- a/iii-database/Cargo.toml
+++ b/database/Cargo.toml
@@ -1,13 +1,13 @@
[workspace]
[package]
-name = "iii-database"
+name = "database"
version = "0.0.4"
edition = "2021"
publish = false
[[bin]]
-name = "iii-database"
+name = "database"
path = "src/main.rs"
[lib]
diff --git a/iii-database/README.md b/database/README.md
similarity index 81%
rename from iii-database/README.md
rename to database/README.md
index b4fbb881..08039d42 100644
--- a/iii-database/README.md
+++ b/database/README.md
@@ -1,4 +1,4 @@
-# iii-database
+# database
> Connect to PostgreSQL, MySQL, and SQLite. Run queries, prepared statements, transactions, and subscribe to row-level change feeds.
@@ -12,7 +12,7 @@
## Install
```sh
-iii worker add iii-database@1.0.0
+iii worker add database@1.0.0
```
## Configure
@@ -21,7 +21,7 @@ Add a single `databases` block to your `config.yaml`. SQLite is the recommended
```yaml
workers:
- - name: iii-database
+ - name: database
config:
databases:
primary:
@@ -95,18 +95,18 @@ SQLite ignores the `tls` block (local-file driver).
```ts
import { call } from 'iii-sdk'
-await call('iii-database::execute', {
+await call('database::execute', {
db: 'primary',
sql: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)'
})
-await call('iii-database::execute', {
+await call('database::execute', {
db: 'primary',
sql: 'INSERT INTO users (email) VALUES (?), (?)',
params: ['a@x', 'b@x']
})
-const { rows } = await call('iii-database::query', {
+const { rows } = await call('database::query', {
db: 'primary',
sql: 'SELECT id, email FROM users ORDER BY id'
})
@@ -116,27 +116,27 @@ const { rows } = await call('iii-database::query', {
| Function | Purpose |
|---|---|
-| `iii-database::query` | Read SQL. Returns `{ rows, row_count, columns }`. |
-| `iii-database::execute` | Write SQL. Returns `{ affected_rows, last_insert_id, returned_rows }`.
**`last_insert_id` semantics:** SQLite/MySQL surface the engine's `last_insert_rowid()` / `LAST_INSERT_ID()` (only populated for INSERT). Postgres has no equivalent — `last_insert_id` is set from the **first column of the first RETURNING row**, so put your PK first: `RETURNING id, name`, not `RETURNING name, id`. |
-| `iii-database::prepareStatement` | Pin a connection and return `{ handle: { id, expires_at } }`. |
-| `iii-database::runStatement` | Run a previously-prepared handle. (No `timeout_ms` — uses the pinned connection's session lifetime; configure via `ttl_seconds` on `prepareStatement`.) |
-| `iii-database::transaction` | Atomic batch sequence; rolls back on first failure. One-shot — pass all statements together. |
-| `iii-database::beginTransaction` | Open an interactive transaction. Returns `{ transaction: { id, expires_at } }`. Configurable `timeout_ms` (default 30 000, max 300 000) auto-rolls back if the deadline elapses. |
-| `iii-database::transactionQuery` | Read SQL inside an interactive transaction. Same envelope as `query`. |
-| `iii-database::transactionExecute` | Write SQL inside an interactive transaction. Same envelope as `execute`. Rejects bare `BEGIN`/`COMMIT`/`ROLLBACK`/`SAVEPOINT`/`SET TRANSACTION` with `INVALID_PARAM` — finalize via the dedicated handlers below. |
-| `iii-database::commitTransaction` | Commit and finalize an interactive transaction. Subsequent calls against the same id return `TRANSACTION_NOT_FOUND`. |
-| `iii-database::rollbackTransaction` | Rollback and finalize an interactive transaction. Subsequent calls against the same id return `TRANSACTION_NOT_FOUND`. |
+| `database::query` | Read SQL. Returns `{ rows, row_count, columns }`. |
+| `database::execute` | Write SQL. Returns `{ affected_rows, last_insert_id, returned_rows }`.
**`last_insert_id` semantics:** SQLite/MySQL surface the engine's `last_insert_rowid()` / `LAST_INSERT_ID()` (only populated for INSERT). Postgres has no equivalent — `last_insert_id` is set from the **first column of the first RETURNING row**, so put your PK first: `RETURNING id, name`, not `RETURNING name, id`. |
+| `database::prepareStatement` | Pin a connection and return `{ handle: { id, expires_at } }`. |
+| `database::runStatement` | Run a previously-prepared handle. (No `timeout_ms` — uses the pinned connection's session lifetime; configure via `ttl_seconds` on `prepareStatement`.) |
+| `database::transaction` | Atomic batch sequence; rolls back on first failure. One-shot — pass all statements together. |
+| `database::beginTransaction` | Open an interactive transaction. Returns `{ transaction: { id, expires_at } }`. Configurable `timeout_ms` (default 30 000, max 300 000) auto-rolls back if the deadline elapses. |
+| `database::transactionQuery` | Read SQL inside an interactive transaction. Same envelope as `query`. |
+| `database::transactionExecute` | Write SQL inside an interactive transaction. Same envelope as `execute`. Rejects bare `BEGIN`/`COMMIT`/`ROLLBACK`/`SAVEPOINT`/`SET TRANSACTION` with `INVALID_PARAM` — finalize via the dedicated handlers below. |
+| `database::commitTransaction` | Commit and finalize an interactive transaction. Subsequent calls against the same id return `TRANSACTION_NOT_FOUND`. |
+| `database::rollbackTransaction` | Rollback and finalize an interactive transaction. Subsequent calls against the same id return `TRANSACTION_NOT_FOUND`. |
## Triggers
-### `iii-database::row-change`
+### `database::row-change`
Postgres only. Streams row-level changes via logical replication (`pgoutput`).
> **NOTE (v1.0.0):** Event dispatch is not yet functional. The publication and replication slot are created at startup, but the streaming decode loop is stubbed pending an upstream `tokio-postgres` replication API release. Operators can pre-provision slots and publications now; events will start flowing in a later release.
```yaml
triggers:
- - type: iii-database::row-change
+ - type: database::row-change
config:
db: primary
schema: public
@@ -171,7 +171,7 @@ A few operations are no-ops on certain drivers. They emit a `tracing::warn!` rat
| `execute` with `returning: [...]` | ✓ | ✓ | warn-once + ignore |
| `transaction` `isolation: read_committed` / `repeatable_read` | warn + use serializable | ✓ | ✓ |
| `transaction` `isolation: serializable` | ✓ (`BEGIN IMMEDIATE`) | ✓ | ✓ |
-| `iii-database::row-change` trigger | — | setup-only in v1.0.0 (see above) | — |
+| `database::row-change` trigger | — | setup-only in v1.0.0 (see above) | — |
## Troubleshooting
diff --git a/iii-database/config.yaml b/database/config.yaml
similarity index 100%
rename from iii-database/config.yaml
rename to database/config.yaml
diff --git a/iii-database/config.yaml.example b/database/config.yaml.example
similarity index 100%
rename from iii-database/config.yaml.example
rename to database/config.yaml.example
diff --git a/iii-database/iii.worker.yaml b/database/iii.worker.yaml
similarity index 83%
rename from iii-database/iii.worker.yaml
rename to database/iii.worker.yaml
index d764755e..2698d88d 100644
--- a/iii-database/iii.worker.yaml
+++ b/database/iii.worker.yaml
@@ -1,7 +1,7 @@
iii: v1
-name: iii-database
+name: database
language: rust
deploy: binary
manifest: Cargo.toml
-bin: iii-database
+bin: database
description: Talk to PostgreSQL, MySQL, and SQLite from iii — query, execute, transactions, prepared statements, and change feeds.
diff --git a/iii-database/skills/iii-database/execute.md b/database/skills/iii-database/execute.md
similarity index 85%
rename from iii-database/skills/iii-database/execute.md
rename to database/skills/iii-database/execute.md
index 9e6ce272..9bcc6609 100644
--- a/iii-database/skills/iii-database/execute.md
+++ b/database/skills/iii-database/execute.md
@@ -1,12 +1,12 @@
---
type: how-to
-function_id: iii-database::execute
+function_id: database::execute
title: Run a write statement and return affected rows
---
# When to use
-Call `iii-database::execute` for any write-side SQL — `INSERT`, `UPDATE`,
+Call `database::execute` for any write-side SQL — `INSERT`, `UPDATE`,
`DELETE`, or DDL (`CREATE TABLE`, `ALTER TABLE`, `DROP INDEX`, ...). The
response carries `affected_rows`, an optional `last_insert_id`, and a
`returned_rows` array populated when the caller asks for `RETURNING`-style
@@ -21,12 +21,12 @@ Reach for it when:
(e.g. server-defaulted `id` + `created_at`) — set `returning` on
Postgres or SQLite.
-Use [`iii-database::query`](iii://iii-database/query) instead when the
+Use [`database::query`](iii://database/query) instead when the
statement reads — `execute` does run a `SELECT` if you give it one but
discards the rows and reports `affected_rows: 0`, which is rarely what a
SELECT caller wants.
-Use [`iii-database::transaction`](iii://iii-database/transaction) instead
+Use [`database::transaction`](iii://database/transaction) instead
when you need several writes to commit atomically — `execute` runs each
call as its own implicit transaction.
@@ -43,7 +43,7 @@ call as its own implicit transaction.
`db` and `sql` are required. Empty/whitespace-only `sql` is rejected
uniformly with `DRIVER_ERROR` carrying `message: "empty SQL"` (matches
-[`iii-database::query`](iii://iii-database/query)'s contract).
+[`database::query`](iii://database/query)'s contract).
`params` accepts JSON primitives, arrays, and objects exactly like
`query` — same per-driver placeholder syntax (`?` for sqlite/mysql,
@@ -85,7 +85,7 @@ directly in `sql` for sqlite/postgres rather than passing
`RETURNING id, name` works; `RETURNING name, id` returns `name` as
`last_insert_id`. With no `RETURNING` clause, the field is `null`.
- `returned_rows` mirrors the row-of-objects shape from
- [`iii-database::query`](iii://iii-database/query). Empty `[]` when the
+ [`database::query`](iii://database/query). Empty `[]` when the
statement omits `RETURNING` or runs on MySQL.
# Worked example
@@ -131,6 +131,6 @@ Returns `{ "affected_rows": 17, "last_insert_id": null, "returned_rows": [] }`.
# Related
-- `iii-database::query` — for read SQL; returns materialized rows + column metadata instead of `affected_rows`.
-- `iii-database::transaction` — group several writes into one atomic batch with rollback on first failure.
-- `iii-database::prepareStatement` + `iii-database::runStatement` — re-run the same parameterized write many times without re-parsing on each call.
+- `database::query` — for read SQL; returns materialized rows + column metadata instead of `affected_rows`.
+- `database::transaction` — group several writes into one atomic batch with rollback on first failure.
+- `database::prepareStatement` + `database::runStatement` — re-run the same parameterized write many times without re-parsing on each call.
diff --git a/iii-database/skills/iii-database/interactive-transaction.md b/database/skills/iii-database/interactive-transaction.md
similarity index 85%
rename from iii-database/skills/iii-database/interactive-transaction.md
rename to database/skills/iii-database/interactive-transaction.md
index 0edcd58c..8aa6fb00 100644
--- a/iii-database/skills/iii-database/interactive-transaction.md
+++ b/database/skills/iii-database/interactive-transaction.md
@@ -1,6 +1,6 @@
---
type: how-to
-functions: [iii-database::beginTransaction, iii-database::transactionQuery, iii-database::transactionExecute, iii-database::commitTransaction, iii-database::rollbackTransaction]
+functions: [database::beginTransaction, database::transactionQuery, database::transactionExecute, database::commitTransaction, database::rollbackTransaction]
title: Run a stateful transaction across multiple RPC calls with a timeout-driven auto-rollback
---
@@ -17,17 +17,17 @@ Reach for it when:
- You need to take a decision in application code *between* statements
that must commit together (the batch
- [`iii-database::transaction`](iii://iii-database/transaction) requires
+ [`database::transaction`](iii://database/transaction) requires
every statement up-front, so it can't carry inter-statement logic).
- You want a single transaction id to thread through a long-running
workflow without holding a network connection open in your code.
- You need stronger-than-default isolation for a multi-step read+write
flow (pass `isolation: "serializable"` to `beginTransaction`).
-Use [`iii-database::transaction`](iii://iii-database/transaction) instead
+Use [`database::transaction`](iii://database/transaction) instead
when every statement is known in advance — it skips the registry overhead
and the per-call round-trip cost. Use
-[`iii-database::execute`](iii://iii-database/execute) for one-off writes;
+[`database::execute`](iii://database/execute) for one-off writes;
each `execute` call is its own auto-committed transaction.
# Lifecycle
@@ -47,7 +47,7 @@ configured `timeout_ms` elapses before either finalizer lands, the worker
auto-rolls back and removes the id; the next call also gets
`TRANSACTION_NOT_FOUND`.
-# `iii-database::beginTransaction`
+# `database::beginTransaction`
## Inputs
@@ -60,7 +60,7 @@ auto-rolls back and removes the id; the next call also gets
```
`db` is required. `isolation` accepts the same three values as the batch
-[`iii-database::transaction`](iii://iii-database/transaction); any other
+[`database::transaction`](iii://database/transaction); any other
string is rejected with `INVALID_PARAM`. On SQLite, `read_committed` /
`repeatable_read` log a one-line `tracing::warn!` and fall back to
`BEGIN IMMEDIATE` (serializable in practice).
@@ -90,11 +90,11 @@ between functions if needed, but never share it across unrelated requests.
already auto-rolled back. Re-issue `beginTransaction` and start over;
the original id is gone.
-# `iii-database::transactionQuery` / `iii-database::transactionExecute`
+# `database::transactionQuery` / `database::transactionExecute`
The envelopes are **identical to the standalone
-[`query`](iii://iii-database/query) and
-[`execute`](iii://iii-database/execute)** handlers — same row-of-objects
+[`query`](iii://database/query) and
+[`execute`](iii://database/execute)** handlers — same row-of-objects
shape, same `columns` metadata, same `affected_rows` /
`last_insert_id` / `returned_rows` semantics. The only difference: SQL
runs on the pinned transaction connection.
@@ -108,7 +108,7 @@ runs on the pinned transaction connection.
```
`transactionExecute` adds a `returning` array exactly like
-[`execute`](iii://iii-database/execute) for Postgres + SQLite
+[`execute`](iii://database/execute) for Postgres + SQLite
`RETURNING` clauses; MySQL ignores it (logged warn-once).
`transactionExecute` **rejects** bare `BEGIN`, `COMMIT`, `ROLLBACK`,
@@ -122,7 +122,7 @@ Concurrent calls against the same `transaction_id` serialize on the
per-conn mutex. The worker doesn't pipeline statements within one
transaction.
-# `iii-database::commitTransaction` / `iii-database::rollbackTransaction`
+# `database::commitTransaction` / `database::rollbackTransaction`
```json
{ "transaction_id": "550e8400-..." }
@@ -176,7 +176,7 @@ must see the updated balance before deciding whether to credit:
```ts
const { transaction } = await iii.trigger({
- function_id: 'iii-database::beginTransaction',
+ function_id: 'database::beginTransaction',
payload: {
db: 'primary',
isolation: 'serializable',
@@ -186,7 +186,7 @@ const { transaction } = await iii.trigger({
try {
const debit = await iii.trigger({
- function_id: 'iii-database::transactionExecute',
+ function_id: 'database::transactionExecute',
payload: {
transaction_id: transaction.id,
sql: 'UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?',
@@ -195,13 +195,13 @@ try {
})
if (debit.affected_rows !== 1) {
await iii.trigger({
- function_id: 'iii-database::rollbackTransaction',
+ function_id: 'database::rollbackTransaction',
payload: { transaction_id: transaction.id },
})
throw new Error('insufficient funds')
}
await iii.trigger({
- function_id: 'iii-database::transactionExecute',
+ function_id: 'database::transactionExecute',
payload: {
transaction_id: transaction.id,
sql: 'UPDATE accounts SET balance = balance + ? WHERE id = ?',
@@ -209,7 +209,7 @@ try {
},
})
await iii.trigger({
- function_id: 'iii-database::commitTransaction',
+ function_id: 'database::commitTransaction',
payload: { transaction_id: transaction.id },
})
} catch (e) {
@@ -217,7 +217,7 @@ try {
// if a prior step already rolled it back or the deadline elapsed.
try {
await iii.trigger({
- function_id: 'iii-database::rollbackTransaction',
+ function_id: 'database::rollbackTransaction',
payload: { transaction_id: transaction.id },
})
} catch {/* ignore */}
@@ -227,14 +227,14 @@ try {
# Related
-- [`iii-database::transaction`](iii://iii-database/transaction) — atomic
+- [`database::transaction`](iii://database/transaction) — atomic
batch when every statement is known up-front; skip the round-trip
overhead of the interactive surface.
-- [`iii-database::query`](iii://iii-database/query) /
- [`iii-database::execute`](iii://iii-database/execute) — one-off
+- [`database::query`](iii://database/query) /
+ [`database::execute`](iii://database/execute) — one-off
read/write outside any transaction.
-- [`iii-database::prepareStatement` +
- `runStatement`](iii://iii-database/prepared-statements) — pin a
+- [`database::prepareStatement` +
+ `runStatement`](iii://database/prepared-statements) — pin a
connection for repeated parameterized calls **without** transactional
semantics. Useful for read-heavy workloads where the prepared plan is
the bottleneck.
diff --git a/iii-database/skills/iii-database/prepared-statements.md b/database/skills/iii-database/prepared-statements.md
similarity index 82%
rename from iii-database/skills/iii-database/prepared-statements.md
rename to database/skills/iii-database/prepared-statements.md
index 93c2bfbb..8ba45fb6 100644
--- a/iii-database/skills/iii-database/prepared-statements.md
+++ b/database/skills/iii-database/prepared-statements.md
@@ -1,6 +1,6 @@
---
type: how-to
-functions: [iii-database::prepareStatement, iii-database::runStatement]
+functions: [database::prepareStatement, database::runStatement]
title: Prepare a SQL statement once, run it many times against a pinned connection
---
@@ -15,10 +15,10 @@ re-executes that handle with new params on the same connection.
| Question | Use this |
|------------------------------------------------------------------|-----------------------------------------------------------|
-| Will this exact SQL run more than a handful of times? | `iii-database::prepareStatement` first, then re-use. |
-| Do I have a UUID handle already? | `iii-database::runStatement` with new `params`. |
-| Need session-scoped state (e.g. a Postgres advisory lock) across calls? | `iii-database::prepareStatement` pins one connection. |
-| Just running this SQL once? | [`iii-database::query`](iii://iii-database/query) — no handle, no pool pinning. |
+| Will this exact SQL run more than a handful of times? | `database::prepareStatement` first, then re-use. |
+| Do I have a UUID handle already? | `database::runStatement` with new `params`. |
+| Need session-scoped state (e.g. a Postgres advisory lock) across calls? | `database::prepareStatement` pins one connection. |
+| Just running this SQL once? | [`database::query`](iii://database/query) — no handle, no pool pinning. |
**A live handle pins one pool connection until its TTL expires.** The
default TTL is 1 hour and the cap is 24 hours; while a handle exists,
@@ -27,14 +27,14 @@ many handles as you need concurrently, and let them expire (or simply
stop calling them) when the workload ends — there is no `release` or
`close` function.
-Use [`iii-database::query`](iii://iii-database/query) instead when the
+Use [`database::query`](iii://database/query) instead when the
SQL runs only once. Use
-[`iii-database::transaction`](iii://iii-database/transaction) instead
+[`database::transaction`](iii://database/transaction) instead
when several statements need atomic commit/rollback semantics — handles
are not transactions; commit boundaries are defined by the SQL you run
through them.
-# `iii-database::prepareStatement`
+# `database::prepareStatement`
## Inputs
@@ -73,7 +73,7 @@ entry as a side effect.
- `handle.expires_at` is when the worker will stop honouring the
handle. After that point `runStatement` returns `STATEMENT_NOT_FOUND`.
-# `iii-database::runStatement`
+# `database::runStatement`
## Inputs
@@ -89,7 +89,7 @@ expired ids return `STATEMENT_NOT_FOUND`; the response carries the
`handle_id` echo so callers can correlate failures.
`params` follows the same JSON-to-driver coercion as
-[`iii-database::query`](iii://iii-database/query). The placeholder
+[`database::query`](iii://database/query). The placeholder
syntax matches the SQL given to `prepareStatement` (`?` for
sqlite/mysql, `$1`/`$2`/... for postgres).
@@ -104,7 +104,7 @@ configure the per-call ceiling via the connection's session lifetime
```json
{
- "rows": [{ "id": 1, "email": "a@x" }], // row-of-objects, same shape as `iii-database::query`
+ "rows": [{ "id": 1, "email": "a@x" }], // row-of-objects, same shape as `database::query`
"row_count": 1,
"columns": [
{ "name": "id", "type_name": "INTEGER" },
@@ -114,13 +114,13 @@ configure the per-call ceiling via the connection's session lifetime
```
The envelope is bit-for-bit identical to
-[`iii-database::query`](iii://iii-database/query) — same row coercion
+[`database::query`](iii://database/query) — same row coercion
rules, same `columns[i]` metadata, same empty-result handling. Callers
can share one parser for both surfaces.
`runStatement` does not surface write counts or `last_insert_id`. To
re-run an INSERT/UPDATE/DELETE many times and read those fields, use
-[`iii-database::execute`](iii://iii-database/execute) per call — write
+[`database::execute`](iii://database/execute) per call — write
statements are typically cheap to re-parse and the prepared path saves
less than the pool-pinning cost.
@@ -161,8 +161,8 @@ the last successfully-read cursor. Do **not** retry the same
# Related
-- `iii-database::query` — drop the handle altogether for one-shot reads; same response envelope.
-- `iii-database::execute` — for repeated writes; pair its own `last_insert_id` with `affected_rows` per call.
-- `iii-database::transaction` — group writes into an atomic batch instead of holding a pinned connection across many calls.
+- `database::query` — drop the handle altogether for one-shot reads; same response envelope.
+- `database::execute` — for repeated writes; pair its own `last_insert_id` with `affected_rows` per call.
+- `database::transaction` — group writes into an atomic batch instead of holding a pinned connection across many calls.
- Error code `STATEMENT_NOT_FOUND` — re-prepare and retry with the new `handle.id`; the old one is gone.
- Error code `POOL_TIMEOUT` — too many live handles can starve the pool. Bump `pool.max` in your `databases:` config or shorten `ttl_seconds`.
diff --git a/iii-database/skills/iii-database/query.md b/database/skills/iii-database/query.md
similarity index 84%
rename from iii-database/skills/iii-database/query.md
rename to database/skills/iii-database/query.md
index 8147699f..d81cd2ee 100644
--- a/iii-database/skills/iii-database/query.md
+++ b/database/skills/iii-database/query.md
@@ -1,12 +1,12 @@
---
type: how-to
-function_id: iii-database::query
+function_id: database::query
title: Run a read-only SQL query and return rows
---
# When to use
-Call `iii-database::query` for any read-side SQL — `SELECT`, `WITH`,
+Call `database::query` for any read-side SQL — `SELECT`, `WITH`,
`PRAGMA`, `EXPLAIN`, anything that produces a result set you want
materialized as JSON. The response carries the rows as objects keyed by
column name plus a `columns` array with per-column type metadata, so
@@ -21,11 +21,11 @@ Reach for it when:
- You want column-name + driver-type metadata alongside the rows
(`columns[i].name` and `columns[i].type_name`).
-Use [`iii-database::execute`](iii://iii-database/execute) instead when the
+Use [`database::execute`](iii://database/execute) instead when the
statement writes (INSERT/UPDATE/DELETE/DDL) — `query` only returns rows;
write counts and `last_insert_id` come from `execute`.
-Use [`iii-database::prepareStatement` + `runStatement`](iii://iii-database/prepared-statements)
+Use [`database::prepareStatement` + `runStatement`](iii://database/prepared-statements)
instead when you'll re-run the same SQL many times in a hot loop — the
prepared path skips the per-call parse/plan cost and pins a pool
connection so isolation primitives like temp tables stay alive across
@@ -121,6 +121,6 @@ shape-compatible.
# Related
-- `iii-database::execute` — for the write side (INSERT/UPDATE/DELETE/DDL); returns affected-row counts instead of materialized rows.
-- `iii-database::prepareStatement` + `iii-database::runStatement` — re-run the same SQL many times without re-parsing; also pins a pool connection so session-scoped state (temp tables, `SET LOCAL`, ...) survives across calls.
-- `iii-database::transaction` — group several statements (mixed read/write) into one atomic batch with a single `committed` flag.
+- `database::execute` — for the write side (INSERT/UPDATE/DELETE/DDL); returns affected-row counts instead of materialized rows.
+- `database::prepareStatement` + `database::runStatement` — re-run the same SQL many times without re-parsing; also pins a pool connection so session-scoped state (temp tables, `SET LOCAL`, ...) survives across calls.
+- `database::transaction` — group several statements (mixed read/write) into one atomic batch with a single `committed` flag.
diff --git a/iii-database/skills/iii-database/transaction.md b/database/skills/iii-database/transaction.md
similarity index 80%
rename from iii-database/skills/iii-database/transaction.md
rename to database/skills/iii-database/transaction.md
index 8194518c..6a954fa8 100644
--- a/iii-database/skills/iii-database/transaction.md
+++ b/database/skills/iii-database/transaction.md
@@ -1,12 +1,12 @@
---
type: how-to
-function_id: iii-database::transaction
+function_id: database::transaction
title: Run a sequence of statements atomically with rollback on first failure
---
# When to use
-Call `iii-database::transaction` when several SQL statements must commit
+Call `database::transaction` when several SQL statements must commit
or roll back together — the canonical "transfer money between accounts"
shape, plus any multi-step write that would leave the DB in an
inconsistent state if a later statement failed. The worker opens a
@@ -25,10 +25,10 @@ Reach for it when:
- You want a single response that tells you *which* statement failed
when something rolls back (`failed_index`).
-Use [`iii-database::execute`](iii://iii-database/execute) instead for a
+Use [`database::execute`](iii://database/execute) instead for a
single write — `execute` is implicitly its own transaction and skips the
multi-statement framing cost. Use
-[`iii-database::prepareStatement` + `runStatement`](iii://iii-database/prepared-statements)
+[`database::prepareStatement` + `runStatement`](iii://database/prepared-statements)
instead when the goal is repeating one parameterized statement many
times rather than committing several different statements as a unit.
@@ -48,7 +48,7 @@ times rather than committing several different statements as a unit.
`db` and `statements` are required; an empty `statements` array
commits a no-op transaction (`committed: true`, `results: []`). Each
statement carries its own `sql` (non-empty, like
-[`iii-database::query`](iii://iii-database/query) and `execute`) plus
+[`database::query`](iii://database/query) and `execute`) plus
optional `params` with the same JSON-to-driver coercion rules.
`isolation` is optional and accepts exactly three values:
@@ -107,7 +107,7 @@ Failure (rollback):
committed every statement or rolled back every statement; partial
commit is impossible.
- `results` is present only on success. Each entry mirrors the
- `affected_rows` count `iii-database::execute` would have produced for
+ `affected_rows` count `database::execute` would have produced for
that statement, plus a positional `rows` array (NOT keyed by column —
this is intentionally lighter than `query`'s row-of-objects shape;
parse with the input order in mind).
@@ -159,7 +159,7 @@ failed step) once the underlying constraint condition is fixed.
# Related
-- [`iii-database::beginTransaction` + `transactionQuery` / `transactionExecute` + `commitTransaction` / `rollbackTransaction`](iii://iii-database/interactive-transaction) — **stateful interactive** transaction with a configurable timeout-driven auto-rollback. Use this surface when you need to take a decision in application code *between* statements (read-your-writes across round-trips). The batch handler on this page requires every statement up-front, so it can't carry that inter-statement logic.
-- `iii-database::execute` — single-statement variant; skips the BEGIN/COMMIT framing for one-shot writes.
-- `iii-database::query` — read-only; cannot be combined with writes inside this surface but is fine to mix into the `statements` array if you only need its rows for `affected_rows`-equivalent counts.
-- `iii-database::prepareStatement` + `iii-database::runStatement` — for repeating one parameterized statement many times; not a substitute for atomic multi-statement commit.
+- [`database::beginTransaction` + `transactionQuery` / `transactionExecute` + `commitTransaction` / `rollbackTransaction`](iii://database/interactive-transaction) — **stateful interactive** transaction with a configurable timeout-driven auto-rollback. Use this surface when you need to take a decision in application code *between* statements (read-your-writes across round-trips). The batch handler on this page requires every statement up-front, so it can't carry that inter-statement logic.
+- `database::execute` — single-statement variant; skips the BEGIN/COMMIT framing for one-shot writes.
+- `database::query` — read-only; cannot be combined with writes inside this surface but is fine to mix into the `statements` array if you only need its rows for `affected_rows`-equivalent counts.
+- `database::prepareStatement` + `database::runStatement` — for repeating one parameterized statement many times; not a substitute for atomic multi-statement commit.
diff --git a/database/skills/index.md b/database/skills/index.md
new file mode 100644
index 00000000..31bdfdc6
--- /dev/null
+++ b/database/skills/index.md
@@ -0,0 +1,36 @@
+---
+type: index
+title: database
+---
+
+# database
+
+Connect to PostgreSQL, MySQL, and SQLite from the iii engine. Run read-only
+queries, write statements, atomic transactions, and prepared-statement
+sequences over a managed per-database connection pool. Every callable
+surface lives under the single `database::*` namespace; SQLite is the
+recommended starting point because it needs no server, just a file.
+
+The worker resolves the driver from each database's URL scheme (`sqlite:`,
+`postgres://`, `postgresql://`, `mysql://`). For the `databases:` config
+block, TLS modes, error-code reference, and the per-driver compatibility
+table (e.g. `returning:` is a no-op on MySQL; SQLite degrades
+`read_committed` / `repeatable_read` to `serializable`), see
+[the README](../README.md).
+
+## How-tos
+
+### `database::*`
+
+- [`database::query`](iii://database/query) — read-only SQL; returns `{ rows, row_count, columns }` for SELECT-style statements.
+- [`database::execute`](iii://database/execute) — write SQL (INSERT/UPDATE/DELETE/DDL); returns `{ affected_rows, last_insert_id, returned_rows }`.
+- [`database::prepareStatement` + `database::runStatement`](iii://database/prepared-statements) — prepare-once, run-many parameterized SQL; the prepare step pins a pool connection until TTL expiry, so always run before re-issuing the same statement many times.
+- [`database::transaction`](iii://database/transaction) — atomic batch sequence; one call, pass every statement together, rolls back on first failure with a `failed_index` pointer at the offending step.
+- [`database::beginTransaction` + `transactionQuery` / `transactionExecute` + `commitTransaction` / `rollbackTransaction`](iii://database/interactive-transaction) — stateful interactive transaction with a configurable timeout-driven auto-rollback. Use this when you need read-your-writes across several round-trips inside a single transaction.
+
+`database::row-change` (Postgres logical replication via `pgoutput`) is
+registered as a trigger type but is **not yet functional in v1.0.0**:
+`register_trigger` returns `UNSUPPORTED` while the streaming decode loop
+waits on an upstream `tokio-postgres` replication API release. Operators
+can pre-provision slots and publications now; see the **Triggers** section
+of [the README](../README.md) for current status and cleanup commands.
diff --git a/iii-database/src/config.rs b/database/src/config.rs
similarity index 100%
rename from iii-database/src/config.rs
rename to database/src/config.rs
diff --git a/iii-database/src/driver/mod.rs b/database/src/driver/mod.rs
similarity index 100%
rename from iii-database/src/driver/mod.rs
rename to database/src/driver/mod.rs
diff --git a/iii-database/src/driver/mysql.rs b/database/src/driver/mysql.rs
similarity index 100%
rename from iii-database/src/driver/mysql.rs
rename to database/src/driver/mysql.rs
diff --git a/iii-database/src/driver/postgres.rs b/database/src/driver/postgres.rs
similarity index 100%
rename from iii-database/src/driver/postgres.rs
rename to database/src/driver/postgres.rs
diff --git a/iii-database/src/driver/sqlite.rs b/database/src/driver/sqlite.rs
similarity index 100%
rename from iii-database/src/driver/sqlite.rs
rename to database/src/driver/sqlite.rs
diff --git a/iii-database/src/error.rs b/database/src/error.rs
similarity index 100%
rename from iii-database/src/error.rs
rename to database/src/error.rs
diff --git a/iii-database/src/handle.rs b/database/src/handle.rs
similarity index 100%
rename from iii-database/src/handle.rs
rename to database/src/handle.rs
diff --git a/iii-database/src/handlers/begin_transaction.rs b/database/src/handlers/begin_transaction.rs
similarity index 98%
rename from iii-database/src/handlers/begin_transaction.rs
rename to database/src/handlers/begin_transaction.rs
index 88ef9ef4..59d8cf2a 100644
--- a/iii-database/src/handlers/begin_transaction.rs
+++ b/database/src/handlers/begin_transaction.rs
@@ -1,4 +1,4 @@
-//! `iii-database::beginTransaction` — open an interactive transaction and
+//! `database::beginTransaction` — open an interactive transaction and
//! return a handle. The handle pins one pool connection inside a server-
//! side `BEGIN ... COMMIT/ROLLBACK` until either the matching
//! `commitTransaction` / `rollbackTransaction` call lands, or the
diff --git a/iii-database/src/handlers/commit_transaction.rs b/database/src/handlers/commit_transaction.rs
similarity index 98%
rename from iii-database/src/handlers/commit_transaction.rs
rename to database/src/handlers/commit_transaction.rs
index faa09ef5..92d0b11e 100644
--- a/iii-database/src/handlers/commit_transaction.rs
+++ b/database/src/handlers/commit_transaction.rs
@@ -1,4 +1,4 @@
-//! `iii-database::commitTransaction` — finalize an interactive transaction
+//! `database::commitTransaction` — finalize an interactive transaction
//! by issuing `COMMIT`. Removes the entry from the registry before locking
//! the connection so concurrent `transactionQuery` / `transactionExecute`
//! calls against the same id fast-fail with `TRANSACTION_NOT_FOUND` once
diff --git a/iii-database/src/handlers/execute.rs b/database/src/handlers/execute.rs
similarity index 99%
rename from iii-database/src/handlers/execute.rs
rename to database/src/handlers/execute.rs
index 6c27f37c..61ed210b 100644
--- a/iii-database/src/handlers/execute.rs
+++ b/database/src/handlers/execute.rs
@@ -1,4 +1,4 @@
-//! `iii-database::execute` — write SQL.
+//! `database::execute` — write SQL.
use super::AppState;
use crate::driver;
diff --git a/iii-database/src/handlers/mod.rs b/database/src/handlers/mod.rs
similarity index 100%
rename from iii-database/src/handlers/mod.rs
rename to database/src/handlers/mod.rs
diff --git a/iii-database/src/handlers/prepare.rs b/database/src/handlers/prepare.rs
similarity index 98%
rename from iii-database/src/handlers/prepare.rs
rename to database/src/handlers/prepare.rs
index cae29c6b..414d8111 100644
--- a/iii-database/src/handlers/prepare.rs
+++ b/database/src/handlers/prepare.rs
@@ -1,4 +1,4 @@
-//! `iii-database::prepareStatement` — pin a connection and return a UUID handle.
+//! `database::prepareStatement` — pin a connection and return a UUID handle.
use super::AppState;
use crate::handle::HandleResponse;
diff --git a/iii-database/src/handlers/query.rs b/database/src/handlers/query.rs
similarity index 99%
rename from iii-database/src/handlers/query.rs
rename to database/src/handlers/query.rs
index 24d1fad9..7d18c28e 100644
--- a/iii-database/src/handlers/query.rs
+++ b/database/src/handlers/query.rs
@@ -1,4 +1,4 @@
-//! `iii-database::query` — read-only SQL.
+//! `database::query` — read-only SQL.
use super::AppState;
use crate::driver::{self, ColumnMeta};
diff --git a/iii-database/src/handlers/rollback_transaction.rs b/database/src/handlers/rollback_transaction.rs
similarity index 98%
rename from iii-database/src/handlers/rollback_transaction.rs
rename to database/src/handlers/rollback_transaction.rs
index 5d2ab21c..3375a19b 100644
--- a/iii-database/src/handlers/rollback_transaction.rs
+++ b/database/src/handlers/rollback_transaction.rs
@@ -1,4 +1,4 @@
-//! `iii-database::rollbackTransaction` — finalize an interactive transaction
+//! `database::rollbackTransaction` — finalize an interactive transaction
//! by issuing `ROLLBACK`. Like `commitTransaction`, takes the entry out of
//! the registry first so concurrent statement calls fast-fail with
//! `TRANSACTION_NOT_FOUND`.
diff --git a/iii-database/src/handlers/run_statement.rs b/database/src/handlers/run_statement.rs
similarity index 98%
rename from iii-database/src/handlers/run_statement.rs
rename to database/src/handlers/run_statement.rs
index d79f2508..799fdc24 100644
--- a/iii-database/src/handlers/run_statement.rs
+++ b/database/src/handlers/run_statement.rs
@@ -1,4 +1,4 @@
-//! `iii-database::runStatement` — run a previously-prepared handle.
+//! `database::runStatement` — run a previously-prepared handle.
use super::AppState;
use crate::driver;
diff --git a/iii-database/src/handlers/transaction.rs b/database/src/handlers/transaction.rs
similarity index 99%
rename from iii-database/src/handlers/transaction.rs
rename to database/src/handlers/transaction.rs
index ece56ea0..e4e87b0b 100644
--- a/iii-database/src/handlers/transaction.rs
+++ b/database/src/handlers/transaction.rs
@@ -1,4 +1,4 @@
-//! `iii-database::transaction` — atomic sequence of statements.
+//! `database::transaction` — atomic sequence of statements.
use super::AppState;
use crate::driver::{self, Isolation, TxStatement};
diff --git a/iii-database/src/handlers/transaction_execute.rs b/database/src/handlers/transaction_execute.rs
similarity index 98%
rename from iii-database/src/handlers/transaction_execute.rs
rename to database/src/handlers/transaction_execute.rs
index dbd351e3..d57d4d58 100644
--- a/iii-database/src/handlers/transaction_execute.rs
+++ b/database/src/handlers/transaction_execute.rs
@@ -1,5 +1,5 @@
-//! `iii-database::transactionExecute` — write SQL inside an interactive
-//! transaction. Same response envelope as `iii-database::execute`. Bare
+//! `database::transactionExecute` — write SQL inside an interactive
+//! transaction. Same response envelope as `database::execute`. Bare
//! `BEGIN` / `COMMIT` / `ROLLBACK` / `END` / `SAVEPOINT` / `RELEASE` /
//! `SET TRANSACTION` / `START TRANSACTION` statements are rejected with
//! `INVALID_PARAM` so callers cannot side-channel finalization through SQL —
@@ -268,7 +268,7 @@ mod tests {
/// Success-path INSERT inside an interactive transaction surfaces
/// `affected_rows` and `last_insert_id` just like the standalone
- /// `iii-database::execute`. Uses an on-disk sqlite so the txn's pinned
+ /// `database::execute`. Uses an on-disk sqlite so the txn's pinned
/// conn and the post-commit verification share one database file.
#[tokio::test(flavor = "multi_thread")]
async fn execute_insert_inside_tx_returns_affected_rows_and_last_insert_id() {
diff --git a/iii-database/src/handlers/transaction_query.rs b/database/src/handlers/transaction_query.rs
similarity index 98%
rename from iii-database/src/handlers/transaction_query.rs
rename to database/src/handlers/transaction_query.rs
index fe5a6962..0d49a295 100644
--- a/iii-database/src/handlers/transaction_query.rs
+++ b/database/src/handlers/transaction_query.rs
@@ -1,5 +1,5 @@
-//! `iii-database::transactionQuery` — read SQL inside an interactive
-//! transaction. Same response envelope as `iii-database::query`; the only
+//! `database::transactionQuery` — read SQL inside an interactive
+//! transaction. Same response envelope as `database::query`; the only
//! difference is the SQL runs on the pinned transaction connection rather
//! than a freshly-pooled one.
//!
diff --git a/iii-database/src/handlers/tx_sql_guard.rs b/database/src/handlers/tx_sql_guard.rs
similarity index 100%
rename from iii-database/src/handlers/tx_sql_guard.rs
rename to database/src/handlers/tx_sql_guard.rs
diff --git a/iii-database/src/lib.rs b/database/src/lib.rs
similarity index 69%
rename from iii-database/src/lib.rs
rename to database/src/lib.rs
index a798c3f7..8db979e2 100644
--- a/iii-database/src/lib.rs
+++ b/database/src/lib.rs
@@ -1,4 +1,4 @@
-//! iii-database worker — public surface for the binary and tests.
+//! database worker — public surface for the binary and tests.
pub mod config;
pub(crate) mod driver;
@@ -11,5 +11,5 @@ pub mod triggers;
pub mod value;
pub fn worker_name() -> &'static str {
- "iii-database"
+ "database"
}
diff --git a/iii-database/src/main.rs b/database/src/main.rs
similarity index 79%
rename from iii-database/src/main.rs
rename to database/src/main.rs
index c0568340..ff1fae42 100644
--- a/iii-database/src/main.rs
+++ b/database/src/main.rs
@@ -1,8 +1,8 @@
use anyhow::{Context, Result};
use clap::Parser;
-use iii_database::config::WorkerConfig;
-use iii_database::handle::HandleRegistry;
-use iii_database::handlers::{
+use database::config::WorkerConfig;
+use database::handle::HandleRegistry;
+use database::handlers::{
begin_transaction::{self, BeginTxReq},
commit_transaction::{self, CommitTxReq},
execute::{self, ExecuteReq},
@@ -15,9 +15,9 @@ use iii_database::handlers::{
transaction_query::{self, TxQueryReq},
AppState,
};
-use iii_database::pool;
-use iii_database::transaction::TxRegistry;
-use iii_database::triggers::handler::RowChangeTrigger;
+use database::pool;
+use database::transaction::TxRegistry;
+use database::triggers::handler::RowChangeTrigger;
use iii_sdk::{
register_worker, InitOptions, Logger, OtelConfig, RegisterFunction, RegisterTriggerType,
};
@@ -26,8 +26,8 @@ use std::sync::Arc;
#[derive(Parser, Debug)]
#[command(
- name = "iii-database",
- about = "iii-database worker (PostgreSQL, MySQL, SQLite)"
+ name = "database",
+ about = "database worker (PostgreSQL, MySQL, SQLite)"
)]
struct Cli {
/// Path to config.yaml file
@@ -50,7 +50,7 @@ async fn main() -> Result<()> {
let cli = Cli::parse();
tracing::info!(
- name = iii_database::worker_name(),
+ name = database::worker_name(),
config = %cli.config,
url = %redact_url(&cli.url),
"starting"
@@ -94,7 +94,7 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async("iii-database::query", move |req: QueryReq| {
+ RegisterFunction::new_async("database::query", move |req: QueryReq| {
let st = st.clone();
async move { query::handle(&st, req).await }
})
@@ -104,7 +104,7 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async("iii-database::execute", move |req: ExecuteReq| {
+ RegisterFunction::new_async("database::execute", move |req: ExecuteReq| {
let st = st.clone();
async move { execute::handle(&st, req).await }
})
@@ -114,20 +114,17 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async(
- "iii-database::prepareStatement",
- move |req: PrepareReq| {
- let st = st.clone();
- async move { prepare::handle(&st, req).await }
- },
- )
+ RegisterFunction::new_async("database::prepareStatement", move |req: PrepareReq| {
+ let st = st.clone();
+ async move { prepare::handle(&st, req).await }
+ })
.description("Prepare a parameterized statement once."),
);
}
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async("iii-database::runStatement", move |req: RunReq| {
+ RegisterFunction::new_async("database::runStatement", move |req: RunReq| {
let st = st.clone();
async move { run_statement::handle(&st, req).await }
})
@@ -137,7 +134,7 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async("iii-database::transaction", move |req: TxReq| {
+ RegisterFunction::new_async("database::transaction", move |req: TxReq| {
let st = st.clone();
async move { transaction::handle(&st, req).await }
})
@@ -147,13 +144,10 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async(
- "iii-database::beginTransaction",
- move |req: BeginTxReq| {
- let st = st.clone();
- async move { begin_transaction::handle(&st, req).await }
- },
- )
+ RegisterFunction::new_async("database::beginTransaction", move |req: BeginTxReq| {
+ let st = st.clone();
+ async move { begin_transaction::handle(&st, req).await }
+ })
.description(
"Open an interactive transaction; returns a handle to use with \
transactionQuery/transactionExecute/commitTransaction/rollbackTransaction.",
@@ -163,13 +157,10 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async(
- "iii-database::transactionQuery",
- move |req: TxQueryReq| {
- let st = st.clone();
- async move { transaction_query::handle(&st, req).await }
- },
- )
+ RegisterFunction::new_async("database::transactionQuery", move |req: TxQueryReq| {
+ let st = st.clone();
+ async move { transaction_query::handle(&st, req).await }
+ })
.description("Run a read-only SQL query inside an interactive transaction."),
);
}
@@ -177,7 +168,7 @@ async fn main() -> Result<()> {
let st = state.clone();
iii.register_function(
RegisterFunction::new_async(
- "iii-database::transactionExecute",
+ "database::transactionExecute",
move |req: TxExecuteReq| {
let st = st.clone();
async move { transaction_execute::handle(&st, req).await }
@@ -192,13 +183,10 @@ async fn main() -> Result<()> {
{
let st = state.clone();
iii.register_function(
- RegisterFunction::new_async(
- "iii-database::commitTransaction",
- move |req: CommitTxReq| {
- let st = st.clone();
- async move { commit_transaction::handle(&st, req).await }
- },
- )
+ RegisterFunction::new_async("database::commitTransaction", move |req: CommitTxReq| {
+ let st = st.clone();
+ async move { commit_transaction::handle(&st, req).await }
+ })
.description("Commit and finalize an interactive transaction."),
);
}
@@ -206,7 +194,7 @@ async fn main() -> Result<()> {
let st = state.clone();
iii.register_function(
RegisterFunction::new_async(
- "iii-database::rollbackTransaction",
+ "database::rollbackTransaction",
move |req: RollbackTxReq| {
let st = st.clone();
async move { rollback_transaction::handle(&st, req).await }
@@ -217,16 +205,16 @@ async fn main() -> Result<()> {
}
let _row_change = iii.register_trigger_type(RegisterTriggerType::new(
- "iii-database::row-change",
+ "database::row-change",
"Postgres logical replication. Stubbed in v1.0 pending tokio-postgres replication API.",
RowChangeTrigger,
));
tracing::info!(
- "iii-database worker registered 10 functions and 1 trigger type, waiting for invocations"
+ "database worker registered 10 functions and 1 trigger type, waiting for invocations"
);
wait_for_shutdown_signal().await?;
- tracing::info!("iii-database worker shutting down");
+ tracing::info!("database worker shutting down");
iii.shutdown_async().await;
Ok(())
}
diff --git a/iii-database/src/pool/mod.rs b/database/src/pool/mod.rs
similarity index 100%
rename from iii-database/src/pool/mod.rs
rename to database/src/pool/mod.rs
diff --git a/iii-database/src/pool/mysql.rs b/database/src/pool/mysql.rs
similarity index 100%
rename from iii-database/src/pool/mysql.rs
rename to database/src/pool/mysql.rs
diff --git a/iii-database/src/pool/postgres.rs b/database/src/pool/postgres.rs
similarity index 100%
rename from iii-database/src/pool/postgres.rs
rename to database/src/pool/postgres.rs
diff --git a/iii-database/src/pool/sqlite.rs b/database/src/pool/sqlite.rs
similarity index 100%
rename from iii-database/src/pool/sqlite.rs
rename to database/src/pool/sqlite.rs
diff --git a/iii-database/src/pool/tls.rs b/database/src/pool/tls.rs
similarity index 100%
rename from iii-database/src/pool/tls.rs
rename to database/src/pool/tls.rs
diff --git a/iii-database/src/transaction.rs b/database/src/transaction.rs
similarity index 100%
rename from iii-database/src/transaction.rs
rename to database/src/transaction.rs
diff --git a/iii-database/src/triggers/handler.rs b/database/src/triggers/handler.rs
similarity index 85%
rename from iii-database/src/triggers/handler.rs
rename to database/src/triggers/handler.rs
index 3a00ff2e..77d7f95d 100644
--- a/iii-database/src/triggers/handler.rs
+++ b/database/src/triggers/handler.rs
@@ -1,4 +1,4 @@
-//! TriggerHandler implementations for `iii-database::row-change`. Wired into
+//! TriggerHandler implementations for `database::row-change`. Wired into
//! the worker via `iii.register_trigger_type` from main.rs.
use async_trait::async_trait;
@@ -8,7 +8,7 @@ fn iii_err(err: T) -> IIIError {
IIIError::Handler(serde_json::to_string(&err).unwrap_or_else(|_| "{}".into()))
}
-/// `iii-database::row-change` trigger handler. v1.0 stubs the streaming decoder
+/// `database::row-change` trigger handler. v1.0 stubs the streaming decoder
/// pending an upstream tokio-postgres replication API release. `register_trigger`
/// returns Unsupported so callers see a clear error instead of silently never
/// receiving events.
diff --git a/iii-database/src/triggers/mod.rs b/database/src/triggers/mod.rs
similarity index 100%
rename from iii-database/src/triggers/mod.rs
rename to database/src/triggers/mod.rs
diff --git a/iii-database/src/triggers/row_change.rs b/database/src/triggers/row_change.rs
similarity index 100%
rename from iii-database/src/triggers/row_change.rs
rename to database/src/triggers/row_change.rs
diff --git a/iii-database/src/value.rs b/database/src/value.rs
similarity index 100%
rename from iii-database/src/value.rs
rename to database/src/value.rs
diff --git a/iii-database/tests/e2e/.gitignore b/database/tests/e2e/.gitignore
similarity index 100%
rename from iii-database/tests/e2e/.gitignore
rename to database/tests/e2e/.gitignore
diff --git a/iii-database/tests/e2e/README.md b/database/tests/e2e/README.md
similarity index 87%
rename from iii-database/tests/e2e/README.md
rename to database/tests/e2e/README.md
index d710ca09..62f83155 100644
--- a/iii-database/tests/e2e/README.md
+++ b/database/tests/e2e/README.md
@@ -1,6 +1,6 @@
-# iii-database worker — end-to-end harness
+# database worker — end-to-end harness
-Self-asserting smoke harness for the `iii-database` worker. Validates the
+Self-asserting smoke harness for the `database` worker. Validates the
function surface (query / execute / prepareStatement / runStatement /
transaction), the **interactive-transaction** surface (begin /
transactionQuery / transactionExecute / commit / rollback), the
@@ -9,7 +9,7 @@ finalization repros from the `/review` of branch `feat/database-and-skills`
against real **SQLite**, **PostgreSQL 16**, and **MySQL 8.4** with one
command.
-Runs locally and in CI (`.github/workflows/iii-database-e2e.yml`).
+Runs locally and in CI (`.github/workflows/database-e2e.yml`).
## Prerequisites
@@ -33,7 +33,7 @@ Runs locally and in CI (`.github/workflows/iii-database-e2e.yml`).
# when the worker hasn't shipped the fix yet)
```
-Builds the worker (`cargo build --release --bin iii-database`), brings up
+Builds the worker (`cargo build --release --bin database`), brings up
the docker stack with `wal_level=logical`, starts the engine, and runs the
selected case groups across all 3 drivers. Exits 0 on PASS, 1 on any FAIL.
@@ -55,10 +55,10 @@ overridden:
| Var | Default | Purpose |
|---|---|---|
-| `WORKER_SRC` | `../..` (the `iii-database/` crate) | Where to `cargo build` |
+| `WORKER_SRC` | `../..` (the `database/` crate) | Where to `cargo build` |
| `III_BIN` | `$(command -v iii)` then `$HOME/.local/bin/iii` | Engine binary |
-| `WORKER_BIN_TARGET` | `$WORKER_SRC/target/release/iii-database` | Built worker |
-| `WORKER_BIN_LINK` | `$HOME/.iii/workers/iii-database` | Symlink the engine reads |
+| `WORKER_BIN_TARGET` | `$WORKER_SRC/target/release/database` | Built worker |
+| `WORKER_BIN_LINK` | `$HOME/.iii/workers/database` | Symlink the engine reads |
| `COMPOSE` | `docker compose` | Compose command. Set to `podman-compose` for rootless podman; the script auto-switches its healthcheck strategy to `podman inspect` (since podman-compose 1.x doesn't implement compose v2's `--wait`). |
| `HARNESS_MODE` | `full` | `full` / `no-bypass` / `bypass-only`. Set by the flags above; you usually don't need to set this directly. |
| `HARNESS_TIMEOUT` | `180` | Seconds to wait for the test sentinel |
@@ -93,7 +93,7 @@ accepted; outside-tx COUNT=1`).
|---|---|
| `run-tests.sh` | Orchestrator |
| `docker-compose.yml` | Postgres (wal_level=logical) + MySQL with healthchecks |
-| `config.yaml` | Engine config (queue, observability, iii-database, harness) |
+| `config.yaml` | Engine config (queue, observability, database, harness) |
| `workers/harness/` | TypeScript smoke-test worker (runs as a host process) |
| `workers/harness/src/cases-interactive-tx.ts` | Interactive-transaction lifecycle cases |
| `workers/harness/src/cases-tx-control-bypass.ts` | Side-channel-finalization repros |
@@ -101,8 +101,8 @@ accepted; outside-tx COUNT=1`).
## CI
-The harness runs in `.github/workflows/iii-database-e2e.yml` on any PR
-that touches `iii-database/**`. The workflow installs the engine via the install
+The harness runs in `.github/workflows/database-e2e.yml` on any PR
+that touches `database/**`. The workflow installs the engine via the install
script (always tracks `main`, no version pin), builds the worker, brings up
the same docker compose stack used locally, and shells out to
`./run-tests.sh`.
diff --git a/iii-database/tests/e2e/config.yaml b/database/tests/e2e/config.yaml
similarity index 90%
rename from iii-database/tests/e2e/config.yaml
rename to database/tests/e2e/config.yaml
index dada83a0..7d87e3ed 100644
--- a/iii-database/tests/e2e/config.yaml
+++ b/database/tests/e2e/config.yaml
@@ -5,8 +5,8 @@
# WebSocket like any external client — sidesteps the libkrun-VM-based
# managed-worker setup, which is overkill for a test harness.
#
-# The iii-database worker config is inlined under its worker entry. The
-# engine serializes this `config:` value to /tmp/iii-database-config.yaml
+# The database worker config is inlined under its worker entry. The
+# engine serializes this `config:` value to /tmp/database-config.yaml
# and threads `--config ` through `iii-worker start` to the spawned
# binary (see engine/src/workers/registry_worker.rs::spawn and
# crates/iii-worker/src/cli/managed.rs::start_binary_worker on branch
@@ -21,12 +21,12 @@ workers:
- name: iii-observability
config:
enabled: true
- service_name: iii-database-tests
+ service_name: database-tests
exporter: memory
logs_console_output: true
sampling_ratio: 1.0
- - name: iii-database
+ - name: database
config:
databases:
sqlite_db:
diff --git a/iii-database/tests/e2e/docker-compose.yml b/database/tests/e2e/docker-compose.yml
similarity index 100%
rename from iii-database/tests/e2e/docker-compose.yml
rename to database/tests/e2e/docker-compose.yml
diff --git a/iii-database/tests/e2e/data/.gitkeep b/database/tests/e2e/reports/.gitkeep
similarity index 100%
rename from iii-database/tests/e2e/data/.gitkeep
rename to database/tests/e2e/reports/.gitkeep
diff --git a/iii-database/tests/e2e/run-tests.sh b/database/tests/e2e/run-tests.sh
similarity index 96%
rename from iii-database/tests/e2e/run-tests.sh
rename to database/tests/e2e/run-tests.sh
index 99632298..e215e7a8 100755
--- a/iii-database/tests/e2e/run-tests.sh
+++ b/database/tests/e2e/run-tests.sh
@@ -4,13 +4,13 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Path overrides (set in CI; defaults assume the harness lives at
-# iii-database/tests/e2e/ inside the workers repo and the iii engine is on
+# database/tests/e2e/ inside the workers repo and the iii engine is on
# $PATH or at $HOME/.local/bin/iii — which is where the install script
# `curl -fsSL https://install.iii.dev/iii/main/install.sh | sh` puts it).
WORKER_SRC="${WORKER_SRC:-$(cd "$ROOT_DIR/../.." && pwd)}"
III_BIN="${III_BIN:-$(command -v iii 2>/dev/null || echo "$HOME/.local/bin/iii")}"
-WORKER_BIN_TARGET="${WORKER_BIN_TARGET:-$WORKER_SRC/target/release/iii-database}"
-WORKER_BIN_LINK="${WORKER_BIN_LINK:-$HOME/.iii/workers/iii-database}"
+WORKER_BIN_TARGET="${WORKER_BIN_TARGET:-$WORKER_SRC/target/release/database}"
+WORKER_BIN_LINK="${WORKER_BIN_LINK:-$HOME/.iii/workers/database}"
# Container runtime. Defaults to `docker compose` (compose v2; what CI runs).
# Override to `podman-compose` for rootless podman on dev laptops; the script
@@ -45,7 +45,7 @@ Usage: $0 [--keep] [--no-build] [--with-cargo-test]
[--filter=] [--bypass-only|--no-bypass]
--keep Leave the compose stack running after the run.
- --no-build Skip cargo build of the iii-database worker.
+ --no-build Skip cargo build of the database worker.
--with-cargo-test Run \`cargo test --all-features\` after compose is healthy
with TEST_POSTGRES_URL and TEST_MYSQL_URL pointing at the
stack — exercises gated driver/pool tests with real DBs.
@@ -61,8 +61,8 @@ Usage: $0 [--keep] [--no-build] [--with-cargo-test]
Env overrides:
WORKER_SRC Path to the database worker crate (default: ../..).
III_BIN Path to the iii engine binary (default: \$(command -v iii) or \$HOME/.local/bin/iii).
- WORKER_BIN_TARGET Path to the built worker binary (default: \$WORKER_SRC/target/release/iii-database).
- WORKER_BIN_LINK Path to the symlink the engine reads (default: \$HOME/.iii/workers/iii-database).
+ WORKER_BIN_TARGET Path to the built worker binary (default: \$WORKER_SRC/target/release/database).
+ WORKER_BIN_LINK Path to the symlink the engine reads (default: \$HOME/.iii/workers/database).
COMPOSE Compose command (default: 'docker compose'; set to
'podman-compose' for rootless podman).
HARNESS_TIMEOUT Seconds to wait for the harness sentinel (default: 180).
@@ -103,8 +103,8 @@ fi
# 2. Build the worker (unless --no-build)
if [[ "$NO_BUILD" -eq 0 ]]; then
- echo "[run-tests] cargo build --release (iii-database worker)"
- (cd "$WORKER_SRC" && cargo build --release --bin iii-database)
+ echo "[run-tests] cargo build --release (database worker)"
+ (cd "$WORKER_SRC" && cargo build --release --bin database)
fi
if [[ ! -x "$WORKER_BIN_TARGET" ]]; then
echo "[run-tests] FATAL: worker binary missing at $WORKER_BIN_TARGET — run without --no-build" >&2
diff --git a/iii-database/tests/e2e/workers/harness/iii.worker.yaml b/database/tests/e2e/workers/harness/iii.worker.yaml
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/iii.worker.yaml
rename to database/tests/e2e/workers/harness/iii.worker.yaml
diff --git a/iii-database/tests/e2e/workers/harness/package-lock.json b/database/tests/e2e/workers/harness/package-lock.json
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/package-lock.json
rename to database/tests/e2e/workers/harness/package-lock.json
diff --git a/iii-database/tests/e2e/workers/harness/package.json b/database/tests/e2e/workers/harness/package.json
similarity index 91%
rename from iii-database/tests/e2e/workers/harness/package.json
rename to database/tests/e2e/workers/harness/package.json
index cd956afc..c7032dce 100644
--- a/iii-database/tests/e2e/workers/harness/package.json
+++ b/database/tests/e2e/workers/harness/package.json
@@ -1,5 +1,5 @@
{
- "name": "iii-database-tests-harness",
+ "name": "database-tests-harness",
"version": "0.1.0",
"type": "module",
"private": true,
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-boundary.ts b/database/tests/e2e/workers/harness/src/cases-boundary.ts
similarity index 62%
rename from iii-database/tests/e2e/workers/harness/src/cases-boundary.ts
rename to database/tests/e2e/workers/harness/src/cases-boundary.ts
index b6d4af36..b3f487b1 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-boundary.ts
+++ b/database/tests/e2e/workers/harness/src/cases-boundary.ts
@@ -1,5 +1,5 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Boundary-value cases targeting type encoding, NULL handling, and string
@@ -25,31 +25,31 @@ export const BOUNDARY_CASES: TestCase[] = [
// also drops to Int despite having distinct BIGINT type info available.
applies: ['pg_db'],
async run({ driver, call }) {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64max' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_i64max (n BIGINT NOT NULL)' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64max' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_i64max (n BIGINT NOT NULL)' })
+ await call('database::execute', {
db: driver,
sql: 'INSERT INTO bx_i64max (n) VALUES (9223372036854775807)',
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM bx_i64max' });
- const v = q.rows[0].n;
- expectEqual(v, '9223372036854775807', 'i64::MAX preserved as JSON string');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_i64max' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT n FROM bx_i64max' })
+ const v = q.rows[0].n
+ expectEqual(v, '9223372036854775807', 'i64::MAX preserved as JSON string')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_i64max' })
},
},
{
name: 'i64 min round-trip (BIGINT-as-string)',
applies: ['pg_db'],
async run({ driver, call }) {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64min' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_i64min (n BIGINT NOT NULL)' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64min' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_i64min (n BIGINT NOT NULL)' })
+ await call('database::execute', {
db: driver,
sql: 'INSERT INTO bx_i64min (n) VALUES (-9223372036854775808)',
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM bx_i64min' });
- expectEqual(q.rows[0].n, '-9223372036854775808', 'i64::MIN preserved as JSON string');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_i64min' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT n FROM bx_i64min' })
+ expectEqual(q.rows[0].n, '-9223372036854775808', 'i64::MIN preserved as JSON string')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_i64min' })
},
},
{
@@ -60,20 +60,20 @@ export const BOUNDARY_CASES: TestCase[] = [
// contract while the BIGINT-as-string-test (pg-only) holds the bar above.
applies: ['sqlite_db', 'mysql_db'],
async run({ driver, call }) {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64safe' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_i64safe (n BIGINT NOT NULL)' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_i64safe' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_i64safe (n BIGINT NOT NULL)' })
// 9007199254740991 = Number.MAX_SAFE_INTEGER
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: 'INSERT INTO bx_i64safe (n) VALUES (9007199254740991)',
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM bx_i64safe' });
- const v = q.rows[0].n;
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT n FROM bx_i64safe' })
+ const v = q.rows[0].n
expect(
v === 9007199254740991 || v === '9007199254740991',
`MAX_SAFE_INTEGER round-trip: got ${JSON.stringify(v)}`,
- );
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_i64safe' });
+ )
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_i64safe' })
},
},
{
@@ -83,134 +83,130 @@ export const BOUNDARY_CASES: TestCase[] = [
// a 4-byte INT4 column, surfacing as `22P03 invalid_binary_representation`.
// This case binds an i64-shaped JSON number (within Number.MAX_SAFE_INTEGER)
// to a 32-bit-wide column type. Drivers must dispatch on column type width.
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_int4' });
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_int4' })
// Use INT (postgres maps to INT4, mysql to INT, sqlite stores as INTEGER affinity).
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_int4 (n INT NOT NULL)' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_int4 (n INT NOT NULL)' })
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_int4 (n) VALUES (${ph1})`,
params: [12345],
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM bx_int4' });
- expectEqual(Number(q.rows[0].n), 12345, 'INT column round-trip');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_int4' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT n FROM bx_int4' })
+ expectEqual(Number(q.rows[0].n), 12345, 'INT column round-trip')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_int4' })
},
},
{
name: 'NULL param insert and select',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const ph2 = dialect.placeholder(2);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_null' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_null (a INT NULL, b TEXT NULL)' });
- const r = await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const ph2 = dialect.placeholder(2)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_null' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_null (a INT NULL, b TEXT NULL)' })
+ const r = await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_null (a, b) VALUES (${ph1}, ${ph2})`,
params: [null, null],
- });
- expectEqual(r.affected_rows, 1, 'insert with null params');
- const q = await call('iii-database::query', {
+ })
+ expectEqual(r.affected_rows, 1, 'insert with null params')
+ const q = await call('database::query', {
db: driver,
sql: 'SELECT a, b FROM bx_null WHERE a IS NULL AND b IS NULL',
- });
- expectEqual(q.row_count, 1, 'one matching row with both nulls');
- expectEqual(q.rows[0].a, null, 'a is JSON null');
- expectEqual(q.rows[0].b, null, 'b is JSON null');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_null' });
+ })
+ expectEqual(q.row_count, 1, 'one matching row with both nulls')
+ expectEqual(q.rows[0].a, null, 'a is JSON null')
+ expectEqual(q.rows[0].b, null, 'b is JSON null')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_null' })
},
},
{
name: 'empty string vs NULL distinction',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const ph2 = dialect.placeholder(2);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_emptynull' });
+ const ph1 = dialect.placeholder(1)
+ const ph2 = dialect.placeholder(2)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_emptynull' })
// postgres/mysql/sqlite all distinguish '' from NULL; assert the worker doesn't conflate.
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE bx_emptynull (id ${dialect.idColumnDDL()}, s TEXT NULL)`,
- });
- await call('iii-database::execute', {
+ })
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_emptynull (s) VALUES (${ph1}), (${ph2})`,
params: ['', null],
- });
- const q = await call('iii-database::query', {
+ })
+ const q = await call('database::query', {
db: driver,
sql: 'SELECT s FROM bx_emptynull ORDER BY id',
- });
- expectEqual(q.rows[0].s, '', 'first row is empty string, not null');
- expectEqual(q.rows[1].s, null, 'second row is null, not empty string');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_emptynull' });
+ })
+ expectEqual(q.rows[0].s, '', 'first row is empty string, not null')
+ expectEqual(q.rows[1].s, null, 'second row is null, not empty string')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_emptynull' })
},
},
{
name: 'UTF-8 round-trip (emoji + RTL + combining marks)',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_utf8' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_utf8 (s TEXT NOT NULL)' });
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_utf8' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_utf8 (s TEXT NOT NULL)' })
// Mix: emoji (4-byte UTF-8), RTL Arabic, Latin with combining acute, ZWSP, Han ideograph.
- const payload = '🔥مرحبا é汉';
- await call('iii-database::execute', {
+ const payload = '🔥مرحبا é汉'
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_utf8 (s) VALUES (${ph1})`,
params: [payload],
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT s FROM bx_utf8' });
- expectEqual(q.rows[0].s, payload, 'utf-8 round-trip exact equality');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_utf8' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT s FROM bx_utf8' })
+ expectEqual(q.rows[0].s, payload, 'utf-8 round-trip exact equality')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_utf8' })
},
},
{
name: 'long string round-trip (64KB)',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_long' });
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_long' })
// MySQL TEXT caps at 64KB; LONGTEXT is unbounded. Use LONGTEXT on mysql to stay clear of headers.
- const colType = driver === 'mysql_db' ? 'LONGTEXT' : 'TEXT';
- await call('iii-database::execute', {
+ const colType = driver === 'mysql_db' ? 'LONGTEXT' : 'TEXT'
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE bx_long (s ${colType} NOT NULL)`,
- });
- const payload = 'x'.repeat(64 * 1024 - 16);
- await call('iii-database::execute', {
+ })
+ const payload = 'x'.repeat(64 * 1024 - 16)
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_long (s) VALUES (${ph1})`,
params: [payload],
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT s FROM bx_long' });
- expectEqual(
- (q.rows[0].s as string).length,
- payload.length,
- '64KB string length preserved',
- );
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_long' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT s FROM bx_long' })
+ expectEqual((q.rows[0].s as string).length, payload.length, '64KB string length preserved')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_long' })
},
},
{
name: 'float values including small subnormal',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const ph2 = dialect.placeholder(2);
- const ph3 = dialect.placeholder(3);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_float' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const ph2 = dialect.placeholder(2)
+ const ph3 = dialect.placeholder(3)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_float' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE bx_float (id ${dialect.idColumnDDL()}, f DOUBLE PRECISION NOT NULL)`,
- });
- await call('iii-database::execute', {
+ })
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_float (f) VALUES (${ph1}), (${ph2}), (${ph3})`,
params: [0.0, 2.5, 1.5e-300],
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT f FROM bx_float ORDER BY id' });
- const fs = q.rows.map((r: any) => Number(r.f));
- expect(Math.abs(fs[0] - 0.0) < 1e-12, `f0 ≈ 0.0, got ${fs[0]}`);
- expect(Math.abs(fs[1] - 2.5) < 1e-12, `f1 ≈ 2.5, got ${fs[1]}`);
- expect(fs[2] < 1e-200 && fs[2] > 0, `f2 is small positive double, got ${fs[2]}`);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_float' });
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT f FROM bx_float ORDER BY id' })
+ const fs = q.rows.map((r: any) => Number(r.f))
+ expect(Math.abs(fs[0] - 0.0) < 1e-12, `f0 ≈ 0.0, got ${fs[0]}`)
+ expect(Math.abs(fs[1] - 2.5) < 1e-12, `f1 ≈ 2.5, got ${fs[1]}`)
+ expect(fs[2] < 1e-200 && fs[2] > 0, `f2 is small positive double, got ${fs[2]}`)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_float' })
},
},
{
@@ -227,79 +223,79 @@ export const BOUNDARY_CASES: TestCase[] = [
// bind as TEXT, not JSONB — that's a different code path.
applies: ['pg_db'],
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_jsonb' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_jsonb' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE bx_jsonb (id ${dialect.idColumnDDL()}, body JSONB NOT NULL, label TEXT NOT NULL)`,
- });
+ })
// Each shape must round-trip exactly through the
// RowValue::Json → into_json path without re-serialization quirks.
const cases: Array<{ label: string; body: unknown }> = [
- { label: 'obj', body: { user: { id: 7, name: 'O\'Brien', tags: ['a', 'b'] }, count: 42 } },
+ { label: 'obj', body: { user: { id: 7, name: "O'Brien", tags: ['a', 'b'] }, count: 42 } },
{ label: 'arr', body: [1, 'two', null, true, { k: 'v' }] },
{ label: 'with_null', body: { a: null, b: 0 } },
{ label: 'empty_obj', body: {} },
{ label: 'empty_arr', body: [] },
- ];
+ ]
for (const c of cases) {
- const r = await call('iii-database::execute', {
+ const r = await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_jsonb (body, label) VALUES (${ph1}, '${c.label}')`,
params: [c.body],
- });
- expectEqual(r.affected_rows, 1, `inserted ${c.label}`);
+ })
+ expectEqual(r.affected_rows, 1, `inserted ${c.label}`)
}
- const q = await call('iii-database::query', {
+ const q = await call('database::query', {
db: driver,
sql: 'SELECT label, body FROM bx_jsonb ORDER BY id',
- });
- expectEqual(q.row_count, cases.length, 'all jsonb rows returned');
+ })
+ expectEqual(q.row_count, cases.length, 'all jsonb rows returned')
// Postgres jsonb canonicalizes both whitespace AND object key order
// (alphabetical by key). Compare semantic equality with a stable-key
// canonicalization on both sides.
const canon = (v: unknown): unknown => {
- if (Array.isArray(v)) return v.map(canon);
+ if (Array.isArray(v)) return v.map(canon)
if (v !== null && typeof v === 'object') {
- const out: Record = {};
+ const out: Record = {}
for (const k of Object.keys(v as Record).sort()) {
- out[k] = canon((v as Record)[k]);
+ out[k] = canon((v as Record)[k])
}
- return out;
+ return out
}
- return v;
- };
+ return v
+ }
for (let i = 0; i < cases.length; i++) {
- expectEqual(q.rows[i].label, cases[i].label, `row ${i} label`);
- expectEqual(canon(q.rows[i].body), canon(cases[i].body), `row ${i} body (${cases[i].label}) round-trip`);
+ expectEqual(q.rows[i].label, cases[i].label, `row ${i} label`)
+ expectEqual(canon(q.rows[i].body), canon(cases[i].body), `row ${i} body (${cases[i].label}) round-trip`)
}
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_jsonb' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_jsonb' })
},
},
{
name: 'special characters in string params (parameterized binding)',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_special' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_special (s TEXT NOT NULL)' });
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_special' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_special (s TEXT NOT NULL)' })
// If the worker were string-interpolating, single-quote would terminate the literal
// and the trailing "; DROP TABLE …" would execute. Proper parameter binding makes
// the value inert — round-trip equality + table-still-exists asserts that.
- const payload = `O'Brien "the\\quoted"\n\t\r-- ; DROP TABLE bx_special`;
- await call('iii-database::execute', {
+ const payload = `O'Brien "the\\quoted"\n\t\r-- ; DROP TABLE bx_special`
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_special (s) VALUES (${ph1})`,
params: [payload],
- });
- const q = await call('iii-database::query', { db: driver, sql: 'SELECT s FROM bx_special' });
- expectEqual(q.rows[0].s, payload, 'special-char string round-trip');
- const q2 = await call('iii-database::query', {
+ })
+ const q = await call('database::query', { db: driver, sql: 'SELECT s FROM bx_special' })
+ expectEqual(q.rows[0].s, payload, 'special-char string round-trip')
+ const q2 = await call('database::query', {
db: driver,
sql: 'SELECT COUNT(*) AS c FROM bx_special',
- });
- expectEqual(Number(q2.rows[0].c), 1, 'table not dropped by injection-shaped payload');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_special' });
+ })
+ expectEqual(Number(q2.rows[0].c), 1, 'table not dropped by injection-shaped payload')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_special' })
},
},
{
@@ -314,17 +310,21 @@ export const BOUNDARY_CASES: TestCase[] = [
name: 'NUMERIC edge cases route through binary fallback (postgres)',
applies: ['pg_db'],
async run({ driver, call }) {
- const q = await call('iii-database::query', {
+ const q = await call('database::query', {
db: driver,
sql: `SELECT 'NaN'::numeric AS nan,
'Infinity'::numeric AS pinf,
'-Infinity'::numeric AS ninf,
100000000000000000000000000000::numeric AS big`,
- });
- expectEqual(q.rows[0].nan, 'NaN', 'NaN survives via binary fallback');
- expectEqual(q.rows[0].pinf, 'Infinity', '+Infinity survives via binary fallback');
- expectEqual(q.rows[0].ninf, '-Infinity', '-Infinity survives via binary fallback');
- expectEqual(q.rows[0].big, '100000000000000000000000000000', '10^29 (beyond rust_decimal) survives via binary fallback');
+ })
+ expectEqual(q.rows[0].nan, 'NaN', 'NaN survives via binary fallback')
+ expectEqual(q.rows[0].pinf, 'Infinity', '+Infinity survives via binary fallback')
+ expectEqual(q.rows[0].ninf, '-Infinity', '-Infinity survives via binary fallback')
+ expectEqual(
+ q.rows[0].big,
+ '100000000000000000000000000000',
+ '10^29 (beyond rust_decimal) survives via binary fallback',
+ )
},
},
{
@@ -337,31 +337,31 @@ export const BOUNDARY_CASES: TestCase[] = [
name: 'NUMERIC columns decode to string (postgres)',
applies: ['pg_db'],
async run({ driver, call }) {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_numeric' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_numeric' })
+ await call('database::execute', {
db: driver,
sql: 'CREATE TABLE bx_numeric (label TEXT NOT NULL, n NUMERIC NOT NULL)',
- });
- await call('iii-database::execute', {
+ })
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_numeric (label, n) VALUES
('exact', 12345.6789),
('negf', -0.001),
('zero', 0),
('big', 999999999999.99)`,
- });
- const q = await call('iii-database::query', {
+ })
+ const q = await call('database::query', {
db: driver,
sql: 'SELECT label, n FROM bx_numeric ORDER BY label',
- });
- const byLabel = Object.fromEntries(q.rows.map((r: Record) => [r.label, r.n]));
+ })
+ const byLabel = Object.fromEntries(q.rows.map((r: Record) => [r.label, r.n]))
// rust_decimal canonical stringification — no trailing zeros beyond what
// the Decimal carries.
- expectEqual(byLabel.exact, '12345.6789', 'NUMERIC positive fractional');
- expectEqual(byLabel.negf, '-0.001', 'NUMERIC negative fractional');
- expectEqual(byLabel.zero, '0', 'NUMERIC zero');
- expectEqual(byLabel.big, '999999999999.99', 'NUMERIC large value');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_numeric' });
+ expectEqual(byLabel.exact, '12345.6789', 'NUMERIC positive fractional')
+ expectEqual(byLabel.negf, '-0.001', 'NUMERIC negative fractional')
+ expectEqual(byLabel.zero, '0', 'NUMERIC zero')
+ expectEqual(byLabel.big, '999999999999.99', 'NUMERIC large value')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_numeric' })
},
},
{
@@ -375,24 +375,24 @@ export const BOUNDARY_CASES: TestCase[] = [
name: 'TIMESTAMP without time zone decodes (postgres)',
applies: ['pg_db'],
async run({ driver, call }) {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_ts' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_ts' })
+ await call('database::execute', {
db: driver,
sql: 'CREATE TABLE bx_ts (naive TIMESTAMP NOT NULL, with_tz TIMESTAMPTZ NOT NULL)',
- });
- await call('iii-database::execute', {
+ })
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_ts (naive, with_tz) VALUES ('2026-04-29 12:00:00', '2026-04-29 12:00:00+00')`,
- });
- const q = await call('iii-database::query', {
+ })
+ const q = await call('database::query', {
db: driver,
sql: 'SELECT naive, with_tz FROM bx_ts',
- });
+ })
// Pre-fix, querying the `naive` column raised `WrongType` and the entire
// call rejected. Now both columns surface as RFC 3339 UTC strings.
- expectEqual(q.rows[0].naive, '2026-04-29T12:00:00Z', 'TIMESTAMP without tz round-trips');
- expectEqual(q.rows[0].with_tz, '2026-04-29T12:00:00Z', 'TIMESTAMPTZ round-trips');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_ts' });
+ expectEqual(q.rows[0].naive, '2026-04-29T12:00:00Z', 'TIMESTAMP without tz round-trips')
+ expectEqual(q.rows[0].with_tz, '2026-04-29T12:00:00Z', 'TIMESTAMPTZ round-trips')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_ts' })
},
},
{
@@ -412,21 +412,18 @@ export const BOUNDARY_CASES: TestCase[] = [
// 3 rows × SLEEP(2) = ~6s of server-side row generation. With
// timeout_ms=500 the wrapped pipeline must surface QUERY_TIMEOUT well
// before the stream completes.
- const start = Date.now();
+ const start = Date.now()
await expectError(
() =>
- call('iii-database::query', {
+ call('database::query', {
db: driver,
sql: 'SELECT SLEEP(2), n FROM (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3) AS t',
timeout_ms: 500,
}),
'QUERY_TIMEOUT',
- );
- const elapsed = Date.now() - start;
- expect(
- elapsed < 3_000,
- `timeout should fire well before the ~6s row stream completes; elapsed=${elapsed}ms`,
- );
+ )
+ const elapsed = Date.now() - start
+ expect(elapsed < 3_000, `timeout should fire well before the ~6s row stream completes; elapsed=${elapsed}ms`)
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-concurrency.ts b/database/tests/e2e/workers/harness/src/cases-concurrency.ts
similarity index 64%
rename from iii-database/tests/e2e/workers/harness/src/cases-concurrency.ts
rename to database/tests/e2e/workers/harness/src/cases-concurrency.ts
index cfa63e6b..a97fb9c5 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-concurrency.ts
+++ b/database/tests/e2e/workers/harness/src/cases-concurrency.ts
@@ -1,5 +1,5 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Pool + handle concurrency cases. Pool max=10 / acquire_timeout=5s per
@@ -11,49 +11,47 @@ export const CONCURRENCY_CASES: TestCase[] = [
name: '10 parallel SELECT 1 against same db',
async run({ driver, call }) {
// Pool max = 10. All 10 queries should finish without queue contention.
- const promises = Array.from({ length: 10 }, () =>
- call('iii-database::query', { db: driver, sql: 'SELECT 1 AS n' }),
- );
- const results = await Promise.all(promises);
- expectEqual(results.length, 10, '10 results');
+ const promises = Array.from({ length: 10 }, () => call('database::query', { db: driver, sql: 'SELECT 1 AS n' }))
+ const results = await Promise.all(promises)
+ expectEqual(results.length, 10, '10 results')
for (const r of results) {
- expectEqual(r.row_count, 1, 'each query returned 1 row');
+ expectEqual(r.row_count, 1, 'each query returned 1 row')
}
},
},
{
name: 'prepared statement reused 50 times sequentially',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
+ const ph1 = dialect.placeholder(1)
// Bare `SELECT ${ph1} AS v` defaults the param to `text` on Postgres
// because the polymorphic param has no type context — sending int4 binary
// there triggers SQL state 22021 ("character not in repertoire") at decode.
// The fix: anchor the param's type via a real schema column (matches the
// existing prepareStatement + runStatement test in cases.ts).
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_prep_50x' });
- await call('iii-database::execute', { db: driver, sql: 'CREATE TABLE bx_prep_50x (n INT NOT NULL)' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS bx_prep_50x' })
+ await call('database::execute', { db: driver, sql: 'CREATE TABLE bx_prep_50x (n INT NOT NULL)' })
// Seed 50 rows so each iteration can match a unique value.
for (let i = 0; i < 50; i++) {
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: `INSERT INTO bx_prep_50x (n) VALUES (${ph1})`,
params: [i],
- });
+ })
}
- const prep = await call('iii-database::prepareStatement', {
+ const prep = await call('database::prepareStatement', {
db: driver,
sql: `SELECT n FROM bx_prep_50x WHERE n = ${ph1} LIMIT 1`,
- });
- const handleId = prep.handle?.id;
- expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present');
+ })
+ const handleId = prep.handle?.id
+ expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present')
// Default TTL is far longer than 50 iterations; this catches handle-cache
// lifetime bugs where a hot handle gets evicted mid-loop.
for (let i = 0; i < 50; i++) {
- const r = await call('iii-database::runStatement', { handle_id: handleId, params: [i] });
- expectEqual(r.row_count, 1, `iter ${i} row_count`);
- expectEqual(Number(r.rows[0].n), i, `iter ${i} value`);
+ const r = await call('database::runStatement', { handle_id: handleId, params: [i] })
+ expectEqual(r.row_count, 1, `iter ${i} row_count`)
+ expectEqual(Number(r.rows[0].n), i, `iter ${i} value`)
}
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE bx_prep_50x' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE bx_prep_50x' })
},
},
{
@@ -63,31 +61,28 @@ export const CONCURRENCY_CASES: TestCase[] = [
// config.yaml; we hold connections 6s to force the timeout.
applies: ['pg_db', 'mysql_db'],
async run({ driver, call }) {
- const sleepSql = driver === 'pg_db' ? 'SELECT pg_sleep(6)' : 'SELECT SLEEP(6)';
+ const sleepSql = driver === 'pg_db' ? 'SELECT pg_sleep(6)' : 'SELECT SLEEP(6)'
// 12 concurrent queries against a max=10 pool. Acquire timeout = 5s, query
// hold = 6s, so the 11th and 12th waiters must time out before any holder
// releases. We assert at least one rejection contains POOL_TIMEOUT — the
// exact count depends on scheduler timing.
const promises = Array.from({ length: 12 }, () =>
- call('iii-database::query', { db: driver, sql: sleepSql, timeout_ms: 30_000 }),
- );
- const settled = await Promise.allSettled(promises);
- const rejected = settled.filter((s) => s.status === 'rejected') as PromiseRejectedResult[];
- const fulfilled = settled.filter((s) => s.status === 'fulfilled');
- expect(
- rejected.length >= 1,
- `expected at least 1 POOL_TIMEOUT rejection, got 0 (fulfilled=${fulfilled.length})`,
- );
+ call('database::query', { db: driver, sql: sleepSql, timeout_ms: 30_000 }),
+ )
+ const settled = await Promise.allSettled(promises)
+ const rejected = settled.filter((s) => s.status === 'rejected') as PromiseRejectedResult[]
+ const fulfilled = settled.filter((s) => s.status === 'fulfilled')
+ expect(rejected.length >= 1, `expected at least 1 POOL_TIMEOUT rejection, got 0 (fulfilled=${fulfilled.length})`)
const sawPoolTimeout = rejected.some((r) => {
- const msg = (r.reason as any)?.message ?? String(r.reason);
- return msg.includes('POOL_TIMEOUT');
- });
+ const msg = (r.reason as any)?.message ?? String(r.reason)
+ return msg.includes('POOL_TIMEOUT')
+ })
expect(
sawPoolTimeout,
`at least one rejection should be POOL_TIMEOUT; reasons: ${rejected
.map((r) => (r.reason as any)?.message ?? String(r.reason))
.join(' | ')}`,
- );
+ )
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-interactive-tx.ts b/database/tests/e2e/workers/harness/src/cases-interactive-tx.ts
similarity index 56%
rename from iii-database/tests/e2e/workers/harness/src/cases-interactive-tx.ts
rename to database/tests/e2e/workers/harness/src/cases-interactive-tx.ts
index 17e540c4..16bc53ab 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-interactive-tx.ts
+++ b/database/tests/e2e/workers/harness/src/cases-interactive-tx.ts
@@ -1,5 +1,5 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Interactive-transaction surface end-to-end cases. Drives the full
@@ -15,121 +15,120 @@ export const INTERACTIVE_TX_CASES: TestCase[] = [
{
name: 'interactive tx commit lands writes',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS itx_commit' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS itx_commit' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE itx_commit (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
try {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction?.id;
- expect(typeof id === 'string' && id.length === 36, `tx id must be UUID, got ${id}`);
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction?.id
+ expect(typeof id === 'string' && id.length === 36, `tx id must be UUID, got ${id}`)
- const ins = await call('iii-database::transactionExecute', {
+ const ins = await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO itx_commit (n) VALUES (${ph1})`,
params: [42],
- });
- expectEqual(ins.affected_rows, 1, 'insert affected_rows inside tx');
+ })
+ expectEqual(ins.affected_rows, 1, 'insert affected_rows inside tx')
// Read-your-writes: the INSERT must be visible from inside the same tx.
- const ryw = await call('iii-database::transactionQuery', {
+ const ryw = await call('database::transactionQuery', {
transaction_id: id,
sql: 'SELECT n FROM itx_commit',
- });
- expectEqual(ryw.row_count, 1, 'tx sees its own insert');
- expectEqual(Number(ryw.rows[0].n), 42, 'tx read-your-writes value');
+ })
+ expectEqual(ryw.row_count, 1, 'tx sees its own insert')
+ expectEqual(Number(ryw.rows[0].n), 42, 'tx read-your-writes value')
- const c = await call('iii-database::commitTransaction', { transaction_id: id });
- expectEqual(c.committed, true, 'commit returns committed=true');
+ const c = await call('database::commitTransaction', { transaction_id: id })
+ expectEqual(c.committed, true, 'commit returns committed=true')
// Verify outside the tx that the write landed.
- const verify = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM itx_commit' });
- expectEqual(verify.row_count, 1, 'committed write visible after commit');
- expectEqual(Number(verify.rows[0].n), 42, 'post-commit value');
+ const verify = await call('database::query', { db: driver, sql: 'SELECT n FROM itx_commit' })
+ expectEqual(verify.row_count, 1, 'committed write visible after commit')
+ expectEqual(Number(verify.rows[0].n), 42, 'post-commit value')
} finally {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE itx_commit' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE itx_commit' })
}
},
},
{
name: 'interactive tx rollback discards writes',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS itx_rollback' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS itx_rollback' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE itx_rollback (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
try {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
- await call('iii-database::transactionExecute', {
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
+ await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO itx_rollback (n) VALUES (${ph1})`,
params: [99],
- });
- const r = await call('iii-database::rollbackTransaction', { transaction_id: id });
- expectEqual(r.rolled_back, true, 'rollback returns rolled_back=true');
+ })
+ const r = await call('database::rollbackTransaction', { transaction_id: id })
+ expectEqual(r.rolled_back, true, 'rollback returns rolled_back=true')
- const verify = await call('iii-database::query', {
+ const verify = await call('database::query', {
db: driver,
sql: 'SELECT COUNT(*) AS c FROM itx_rollback',
- });
- expectEqual(Number(verify.rows[0].c), 0, 'rolled-back insert not visible');
+ })
+ expectEqual(Number(verify.rows[0].c), 0, 'rolled-back insert not visible')
} finally {
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE itx_rollback' });
+ await call('database::execute', { db: driver, sql: 'DROP TABLE itx_rollback' })
}
},
},
{
name: 'interactive tx commit then query returns TRANSACTION_NOT_FOUND',
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
- await call('iii-database::commitTransaction', { transaction_id: id });
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
+ await call('database::commitTransaction', { transaction_id: id })
await expectError(
- () => call('iii-database::transactionQuery', { transaction_id: id, sql: 'SELECT 1' }),
+ () => call('database::transactionQuery', { transaction_id: id, sql: 'SELECT 1' }),
'TRANSACTION_NOT_FOUND',
- );
+ )
},
},
{
name: 'interactive tx double-commit returns TRANSACTION_NOT_FOUND',
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
- await call('iii-database::commitTransaction', { transaction_id: id });
- await expectError(
- () => call('iii-database::commitTransaction', { transaction_id: id }),
- 'TRANSACTION_NOT_FOUND',
- );
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
+ await call('database::commitTransaction', { transaction_id: id })
+ await expectError(() => call('database::commitTransaction', { transaction_id: id }), 'TRANSACTION_NOT_FOUND')
},
},
{
name: 'transactionExecute rejects COMMIT/ROLLBACK SQL with INVALID_PARAM',
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
await expectError(
- () => call('iii-database::transactionExecute', { transaction_id: id, sql: 'COMMIT' }),
+ () => call('database::transactionExecute', { transaction_id: id, sql: 'COMMIT' }),
'INVALID_PARAM',
- );
+ )
await expectError(
- () => call('iii-database::transactionExecute', { transaction_id: id, sql: 'ROLLBACK' }),
+ () => call('database::transactionExecute', { transaction_id: id, sql: 'ROLLBACK' }),
'INVALID_PARAM',
- );
+ )
await expectError(
- () => call('iii-database::transactionExecute', { transaction_id: id, sql: 'BEGIN' }),
+ () => call('database::transactionExecute', { transaction_id: id, sql: 'BEGIN' }),
'INVALID_PARAM',
- );
+ )
} finally {
// The tx is still open (rejections don't finalize); clean up.
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
}
},
},
@@ -141,37 +140,39 @@ export const INTERACTIVE_TX_CASES: TestCase[] = [
// and reached the driver, desyncing the registry from the conn's txn state.
name: 'transactionExecute rejects comment-prefixed and START-form finalization',
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
await expectError(
() =>
- call('iii-database::transactionExecute', {
+ call('database::transactionExecute', {
transaction_id: id,
sql: '/* sneak */COMMIT',
}),
'INVALID_PARAM',
- );
+ )
await expectError(
() =>
- call('iii-database::transactionExecute', {
+ call('database::transactionExecute', {
transaction_id: id,
sql: '-- sneak\nCOMMIT',
}),
'INVALID_PARAM',
- );
+ )
await expectError(
() =>
- call('iii-database::transactionExecute', {
+ call('database::transactionExecute', {
transaction_id: id,
sql: 'START TRANSACTION',
}),
'INVALID_PARAM',
- );
+ )
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
}
},
},
@@ -181,38 +182,39 @@ export const INTERACTIVE_TX_CASES: TestCase[] = [
// prepared-statement path used by `run_prepared`. Must now reject.
name: 'transactionQuery rejects COMMIT/ROLLBACK and comment-prefixed forms',
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
await expectError(
- () => call('iii-database::transactionQuery', { transaction_id: id, sql: 'COMMIT' }),
+ () => call('database::transactionQuery', { transaction_id: id, sql: 'COMMIT' }),
'INVALID_PARAM',
- );
+ )
await expectError(
- () =>
- call('iii-database::transactionQuery', { transaction_id: id, sql: 'ROLLBACK' }),
+ () => call('database::transactionQuery', { transaction_id: id, sql: 'ROLLBACK' }),
'INVALID_PARAM',
- );
+ )
await expectError(
() =>
- call('iii-database::transactionQuery', {
+ call('database::transactionQuery', {
transaction_id: id,
sql: '/* sneak */COMMIT',
}),
'INVALID_PARAM',
- );
+ )
await expectError(
() =>
- call('iii-database::transactionQuery', {
+ call('database::transactionQuery', {
transaction_id: id,
sql: 'START TRANSACTION',
}),
'INVALID_PARAM',
- );
+ )
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
}
},
},
@@ -220,10 +222,9 @@ export const INTERACTIVE_TX_CASES: TestCase[] = [
name: 'interactive tx beginTransaction unknown isolation returns INVALID_PARAM',
async run({ driver, call, expectError }) {
await expectError(
- () =>
- call('iii-database::beginTransaction', { db: driver, isolation: 'bogus_level' }),
+ () => call('database::beginTransaction', { db: driver, isolation: 'bogus_level' }),
'INVALID_PARAM',
- );
+ )
},
},
{
@@ -235,17 +236,17 @@ export const INTERACTIVE_TX_CASES: TestCase[] = [
name: 'interactive tx timeout auto-rolls-back and frees the id',
applies: ['sqlite_db'],
async run({ driver, call, expectError }) {
- const begin = await call('iii-database::beginTransaction', {
+ const begin = await call('database::beginTransaction', {
db: driver,
timeout_ms: 300,
- });
- const id = begin.transaction.id;
+ })
+ const id = begin.transaction.id
// Wait past the deadline + one watcher tick + a safety margin.
- await new Promise((r) => setTimeout(r, 1800));
+ await new Promise((r) => setTimeout(r, 1800))
await expectError(
- () => call('iii-database::transactionQuery', { transaction_id: id, sql: 'SELECT 1' }),
+ () => call('database::transactionQuery', { transaction_id: id, sql: 'SELECT 1' }),
'TRANSACTION_NOT_FOUND',
- );
+ )
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-protocol.ts b/database/tests/e2e/workers/harness/src/cases-protocol.ts
similarity index 66%
rename from iii-database/tests/e2e/workers/harness/src/cases-protocol.ts
rename to database/tests/e2e/workers/harness/src/cases-protocol.ts
index 62fbb783..f94defdc 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-protocol.ts
+++ b/database/tests/e2e/workers/harness/src/cases-protocol.ts
@@ -1,5 +1,5 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Protocol-misuse cases — assert the worker returns the documented error code
@@ -12,19 +12,13 @@ export const PROTOCOL_CASES: TestCase[] = [
{
name: 'unknown db rejects with UNKNOWN_DB',
async run({ call, expectError }) {
- await expectError(
- () => call('iii-database::query', { db: 'no_such_db', sql: 'SELECT 1' }),
- 'UNKNOWN_DB',
- );
+ await expectError(() => call('database::query', { db: 'no_such_db', sql: 'SELECT 1' }), 'UNKNOWN_DB')
},
},
{
name: 'empty SQL rejects with DRIVER_ERROR',
async run({ driver, call, expectError }) {
- await expectError(
- () => call('iii-database::query', { db: driver, sql: '' }),
- 'DRIVER_ERROR',
- );
+ await expectError(() => call('database::query', { db: driver, sql: '' }), 'DRIVER_ERROR')
},
},
{
@@ -32,51 +26,48 @@ export const PROTOCOL_CASES: TestCase[] = [
async run({ call, expectError }) {
await expectError(
() =>
- call('iii-database::runStatement', {
+ call('database::runStatement', {
handle_id: '00000000-0000-0000-0000-000000000000',
params: [],
}),
'STATEMENT_NOT_FOUND',
- );
+ )
},
},
{
name: 'runStatement with wrong param count rejects',
async run({ driver, dialect, call, expectError }) {
- const ph1 = dialect.placeholder(1);
+ const ph1 = dialect.placeholder(1)
// Use a fresh prepare so test order doesn't matter.
- const prep = await call('iii-database::prepareStatement', {
+ const prep = await call('database::prepareStatement', {
db: driver,
sql: `SELECT ${ph1} AS v`,
- });
- const handleId = prep.handle?.id;
- expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present');
+ })
+ const handleId = prep.handle?.id
+ expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present')
// Driver should reject param-count mismatch. Exact code varies by driver
// (DRIVER_ERROR with inner SQL state); we match on DRIVER_ERROR.
- await expectError(
- () => call('iii-database::runStatement', { handle_id: handleId, params: [] }),
- 'DRIVER_ERROR',
- );
+ await expectError(() => call('database::runStatement', { handle_id: handleId, params: [] }), 'DRIVER_ERROR')
},
},
{
name: 'prepared statement after TTL expiry rejects',
async run({ driver, dialect, call, expectError }) {
- const ph1 = dialect.placeholder(1);
- const prep = await call('iii-database::prepareStatement', {
+ const ph1 = dialect.placeholder(1)
+ const prep = await call('database::prepareStatement', {
db: driver,
sql: `SELECT ${ph1} AS v`,
ttl_seconds: 1,
- });
- const handleId = prep.handle?.id;
- expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present');
+ })
+ const handleId = prep.handle?.id
+ expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present')
// TTL is 1s; the registry evictor sweeps periodically. Wait long enough
// that any reasonable evictor cadence will have run.
- await new Promise((r) => setTimeout(r, 1500));
+ await new Promise((r) => setTimeout(r, 1500))
await expectError(
- () => call('iii-database::runStatement', { handle_id: handleId, params: [42] }),
+ () => call('database::runStatement', { handle_id: handleId, params: [42] }),
'STATEMENT_NOT_FOUND',
- );
+ )
},
},
{
@@ -86,9 +77,9 @@ export const PROTOCOL_CASES: TestCase[] = [
// gracefully. affected_rows is undefined for SELECT in most drivers; the
// worker normalizes that to 0. Asserting "no throw" is the main goal —
// the value of affected_rows is a softer assertion.
- const r = await call('iii-database::execute', { db: driver, sql: 'SELECT 1 AS v' });
- expect(typeof r === 'object' && r !== null, 'response is object');
- expectEqual(typeof r.affected_rows, 'number', 'affected_rows is a number');
+ const r = await call('database::execute', { db: driver, sql: 'SELECT 1 AS v' })
+ expect(typeof r === 'object' && r !== null, 'response is object')
+ expectEqual(typeof r.affected_rows, 'number', 'affected_rows is a number')
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-row-change.ts b/database/tests/e2e/workers/harness/src/cases-row-change.ts
similarity index 72%
rename from iii-database/tests/e2e/workers/harness/src/cases-row-change.ts
rename to database/tests/e2e/workers/harness/src/cases-row-change.ts
index eb671379..ecc69107 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-row-change.ts
+++ b/database/tests/e2e/workers/harness/src/cases-row-change.ts
@@ -1,9 +1,9 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* row-change trigger validation. The streaming decoder is stubbed in v1.0
- * (worker rejects `iii-database::row-change` registration with `UNSUPPORTED`),
+ * (worker rejects `database::row-change` registration with `UNSUPPORTED`),
* so we can't exercise the dispatch path end-to-end yet. What we CAN validate
* is the slot/publication name derivation contract that the worker pins in
* its README — distinct caller-supplied `trigger_id`s must produce distinct
@@ -22,13 +22,13 @@ import { expect, expectEqual } from './cases.ts';
/** FNV-1a-32 over UTF-8 bytes. Mirrors `triggers/row_change.rs::fnv1a_32`. */
function fnv1a32(s: string): string {
- let hash = 0x811c9dc5 >>> 0;
- const bytes = Buffer.from(s, 'utf8');
+ let hash = 0x811c9dc5 >>> 0
+ const bytes = Buffer.from(s, 'utf8')
for (const b of bytes) {
- hash = (hash ^ b) >>> 0;
- hash = Math.imul(hash, 0x01000193) >>> 0;
+ hash = (hash ^ b) >>> 0
+ hash = Math.imul(hash, 0x01000193) >>> 0
}
- return hash.toString(16).padStart(8, '0');
+ return hash.toString(16).padStart(8, '0')
}
/**
@@ -41,8 +41,8 @@ function deriveSlotName(triggerId: string): string {
const sanitized = Array.from(triggerId)
.map((c) => (/[a-zA-Z0-9]/.test(c) ? c.toLowerCase() : '_'))
.slice(0, 40)
- .join('');
- return `iii_slot_${sanitized}_${fnv1a32(triggerId)}`;
+ .join('')
+ return `iii_slot_${sanitized}_${fnv1a32(triggerId)}`
}
async function dropSlotIfExists(
@@ -53,16 +53,16 @@ async function dropSlotIfExists(
// pg_drop_replication_slot errors if the slot is missing; pre-check then drop.
// Quote-escape the slot name as a SQL literal: replace any `'` with `''`
// (slot names from derive_names are `[a-z0-9_]` only, so this is defensive).
- const lit = slot.replace(/'/g, "''");
- const exists = await call('iii-database::query', {
+ const lit = slot.replace(/'/g, "''")
+ const exists = await call('database::query', {
db: driver,
sql: `SELECT 1 FROM pg_replication_slots WHERE slot_name = '${lit}'`,
- });
+ })
if (exists.row_count > 0) {
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: `SELECT pg_drop_replication_slot('${lit}')`,
- });
+ })
}
}
@@ -76,45 +76,45 @@ export const ROW_CHANGE_CASES: TestCase[] = [
// makes them distinct. We use 3 (not the full 5) because the docker
// postgres image is configured with `max_replication_slots=4`, leaving
// headroom for the long-trigger-id test that runs immediately after.
- const ids = ['Orders.v1', 'orders-v1', 'orders v1'];
- const slots = ids.map(deriveSlotName);
+ const ids = ['Orders.v1', 'orders-v1', 'orders v1']
+ const slots = ids.map(deriveSlotName)
// Sanity: TS-derived names must all be distinct.
- const unique = new Set(slots);
- expectEqual(unique.size, ids.length, 'TS-derived slot names must be unique across collision-prone inputs');
+ const unique = new Set(slots)
+ expectEqual(unique.size, ids.length, 'TS-derived slot names must be unique across collision-prone inputs')
// Each slot must respect Postgres' 63-byte limit.
for (const s of slots) {
- expect(s.length <= 63, `slot name too long (${s.length} bytes): ${s}`);
+ expect(s.length <= 63, `slot name too long (${s.length} bytes): ${s}`)
}
// Pre-clean any leftovers from a previous run.
for (const slot of slots) {
- await dropSlotIfExists(call, driver, slot);
+ await dropSlotIfExists(call, driver, slot)
}
try {
// Create all five slots. If two collided, the second create call would
// fail with `replication slot ... already exists`.
for (const slot of slots) {
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: `SELECT * FROM pg_create_logical_replication_slot('${slot}', 'pgoutput')`,
- });
+ })
}
// Verify Postgres now lists all five as distinct slots.
- const inList = slots.map((s) => `'${s}'`).join(', ');
- const q = await call('iii-database::query', {
+ const inList = slots.map((s) => `'${s}'`).join(', ')
+ const q = await call('database::query', {
db: driver,
sql: `SELECT slot_name FROM pg_replication_slots WHERE slot_name IN (${inList}) ORDER BY slot_name`,
- });
- expectEqual(q.row_count, ids.length, 'all collision-prone inputs produced distinct slots in postgres');
+ })
+ expectEqual(q.row_count, ids.length, 'all collision-prone inputs produced distinct slots in postgres')
} finally {
// Cleanup so re-running the harness against the same docker volume is idempotent.
for (const slot of slots) {
try {
- await dropSlotIfExists(call, driver, slot);
+ await dropSlotIfExists(call, driver, slot)
} catch {
/* best-effort cleanup */
}
@@ -129,37 +129,45 @@ export const ROW_CHANGE_CASES: TestCase[] = [
// Pathological trigger_id: 200 chars. Without truncation the derived
// name would exceed Postgres' 63-byte slot_name cap and slot creation
// would fail; the hash suffix preserves uniqueness across the truncation.
- const a = 'a'.repeat(200);
- const b = 'a'.repeat(200) + 'b'; // distinct trigger_id, same first-40 sanitized prefix
- const slotA = deriveSlotName(a);
- const slotB = deriveSlotName(b);
+ const a = 'a'.repeat(200)
+ const b = 'a'.repeat(200) + 'b' // distinct trigger_id, same first-40 sanitized prefix
+ const slotA = deriveSlotName(a)
+ const slotB = deriveSlotName(b)
- expect(slotA !== slotB, `long trigger_ids collided: ${slotA}`);
- expect(slotA.length <= 63, `slotA too long (${slotA.length}): ${slotA}`);
- expect(slotB.length <= 63, `slotB too long (${slotB.length}): ${slotB}`);
+ expect(slotA !== slotB, `long trigger_ids collided: ${slotA}`)
+ expect(slotA.length <= 63, `slotA too long (${slotA.length}): ${slotA}`)
+ expect(slotB.length <= 63, `slotB too long (${slotB.length}): ${slotB}`)
// Pre-clean.
- await dropSlotIfExists(call, driver, slotA);
- await dropSlotIfExists(call, driver, slotB);
+ await dropSlotIfExists(call, driver, slotA)
+ await dropSlotIfExists(call, driver, slotB)
try {
- await call('iii-database::execute', {
+ await call('database::execute', {
db: driver,
sql: `SELECT * FROM pg_create_logical_replication_slot('${slotA}', 'pgoutput')`,
- });
- await call('iii-database::execute', {
+ })
+ await call('database::execute', {
db: driver,
sql: `SELECT * FROM pg_create_logical_replication_slot('${slotB}', 'pgoutput')`,
- });
- const q = await call('iii-database::query', {
+ })
+ const q = await call('database::query', {
db: driver,
sql: `SELECT slot_name FROM pg_replication_slots WHERE slot_name IN ('${slotA}', '${slotB}')`,
- });
- expectEqual(q.row_count, 2, 'long-trigger-id slots created and distinct');
+ })
+ expectEqual(q.row_count, 2, 'long-trigger-id slots created and distinct')
} finally {
- try { await dropSlotIfExists(call, driver, slotA); } catch { /* best-effort */ }
- try { await dropSlotIfExists(call, driver, slotB); } catch { /* best-effort */ }
+ try {
+ await dropSlotIfExists(call, driver, slotA)
+ } catch {
+ /* best-effort */
+ }
+ try {
+ await dropSlotIfExists(call, driver, slotB)
+ } catch {
+ /* best-effort */
+ }
}
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-transaction.ts b/database/tests/e2e/workers/harness/src/cases-transaction.ts
similarity index 64%
rename from iii-database/tests/e2e/workers/harness/src/cases-transaction.ts
rename to database/tests/e2e/workers/harness/src/cases-transaction.ts
index a4c433b7..cb7fe7a1 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-transaction.ts
+++ b/database/tests/e2e/workers/harness/src/cases-transaction.ts
@@ -1,5 +1,5 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Transaction edge cases. The function suite covers commit + rollback at
@@ -14,93 +14,93 @@ export const TRANSACTION_EDGE_CASES: TestCase[] = [
async run({ driver, call }) {
// Spec ambiguity: an empty txn is a no-op. Drivers commit an empty
// transaction without error; the worker should pass that through.
- const r = await call('iii-database::transaction', { db: driver, statements: [] });
- expectEqual(r.committed, true, 'empty transaction commits');
- expectEqual(Array.isArray(r.results) ? r.results.length : 0, 0, 'no results');
+ const r = await call('database::transaction', { db: driver, statements: [] })
+ expectEqual(r.committed, true, 'empty transaction commits')
+ expectEqual(Array.isArray(r.results) ? r.results.length : 0, 0, 'no results')
},
},
{
name: 'transaction with single statement',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_single' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_single' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE tx_single (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
- const r = await call('iii-database::transaction', {
+ })
+ const r = await call('database::transaction', {
db: driver,
statements: [{ sql: `INSERT INTO tx_single (n) VALUES (${ph1})`, params: [7] }],
- });
- expectEqual(r.committed, true, 'committed=true for single-statement txn');
- const verify = await call('iii-database::query', {
+ })
+ expectEqual(r.committed, true, 'committed=true for single-statement txn')
+ const verify = await call('database::query', {
db: driver,
sql: 'SELECT n FROM tx_single',
- });
- expectEqual(Number(verify.rows[0].n), 7, 'row landed');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE tx_single' });
+ })
+ expectEqual(Number(verify.rows[0].n), 7, 'row landed')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE tx_single' })
},
},
{
name: 'transaction read-your-writes (mixed INSERT/SELECT/INSERT)',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_ryw' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_ryw' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE tx_ryw (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
- const r = await call('iii-database::transaction', {
+ })
+ const r = await call('database::transaction', {
db: driver,
statements: [
{ sql: `INSERT INTO tx_ryw (n) VALUES (${ph1})`, params: [100] },
{ sql: `SELECT n FROM tx_ryw`, params: [] },
{ sql: `INSERT INTO tx_ryw (n) VALUES (${ph1})`, params: [200] },
],
- });
- expectEqual(r.committed, true, 'committed=true for mixed txn');
- expect(Array.isArray(r.results), 'results is array');
- expectEqual(r.results.length, 3, 'three results');
+ })
+ expectEqual(r.committed, true, 'committed=true for mixed txn')
+ expect(Array.isArray(r.results), 'results is array')
+ expectEqual(r.results.length, 3, 'three results')
// Note: txn results carry rows positionally (Vec>) per
// transaction.rs:64-67 — no column names. This differs from query/runStatement
// which return column-keyed objects. The first SELECT row is `[100]`, not `{n: 100}`.
- const selectResult = r.results[1];
+ const selectResult = r.results[1]
expect(
Array.isArray(selectResult.rows) && selectResult.rows.length === 1,
`select sees exactly one row, got ${JSON.stringify(selectResult)}`,
- );
- const firstRow = selectResult.rows[0];
- expect(Array.isArray(firstRow), `row is positional array, got ${JSON.stringify(firstRow)}`);
- expectEqual(Number(firstRow[0]), 100, 'read-your-writes: select sees the just-inserted value');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE tx_ryw' });
+ )
+ const firstRow = selectResult.rows[0]
+ expect(Array.isArray(firstRow), `row is positional array, got ${JSON.stringify(firstRow)}`)
+ expectEqual(Number(firstRow[0]), 100, 'read-your-writes: select sees the just-inserted value')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE tx_ryw' })
},
},
{
name: 'transaction failure at index 0 reports failed_index=0',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_fail0' });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS tx_fail0' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE tx_fail0 (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
// First statement violates NOT NULL; second never runs. failed_index must be 0.
- const r = await call('iii-database::transaction', {
+ const r = await call('database::transaction', {
db: driver,
statements: [
{ sql: `INSERT INTO tx_fail0 (n) VALUES (${ph1})`, params: [null] },
{ sql: `INSERT INTO tx_fail0 (n) VALUES (${ph1})`, params: [99] },
],
- });
- expectEqual(r.committed, false, 'committed=false');
- expectEqual(r.failed_index, 0, 'failed_index=0');
+ })
+ expectEqual(r.committed, false, 'committed=false')
+ expectEqual(r.failed_index, 0, 'failed_index=0')
// Confirm rollback: zero rows.
- const verify = await call('iii-database::query', {
+ const verify = await call('database::query', {
db: driver,
sql: 'SELECT COUNT(*) AS c FROM tx_fail0',
- });
- expectEqual(Number(verify.rows[0].c), 0, 'rollback dropped all writes');
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE tx_fail0' });
+ })
+ expectEqual(Number(verify.rows[0].c), 0, 'rollback dropped all writes')
+ await call('database::execute', { db: driver, sql: 'DROP TABLE tx_fail0' })
},
},
{
@@ -117,19 +117,16 @@ export const TRANSACTION_EDGE_CASES: TestCase[] = [
name: 'transaction handles CTE SELECT and VALUES (sqlite)',
applies: ['sqlite_db'],
async run({ driver, call }) {
- const r = await call('iii-database::transaction', {
+ const r = await call('database::transaction', {
db: driver,
- statements: [
- { sql: 'WITH cte AS (SELECT 1 AS n) SELECT n FROM cte' },
- { sql: 'VALUES (10), (20), (30)' },
- ],
- });
- expectEqual(r.committed, true, 'CTE+VALUES tx committed');
- expect(Array.isArray(r.results) && r.results.length === 2, 'two step results');
+ statements: [{ sql: 'WITH cte AS (SELECT 1 AS n) SELECT n FROM cte' }, { sql: 'VALUES (10), (20), (30)' }],
+ })
+ expectEqual(r.committed, true, 'CTE+VALUES tx committed')
+ expect(Array.isArray(r.results) && r.results.length === 2, 'two step results')
// CTE SELECT → 1 row
- expectEqual(r.results[0].rows.length, 1, 'CTE step row count');
+ expectEqual(r.results[0].rows.length, 1, 'CTE step row count')
// VALUES → 3 rows
- expectEqual(r.results[1].rows.length, 3, 'VALUES step row count');
+ expectEqual(r.results[1].rows.length, 3, 'VALUES step row count')
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts b/database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts
similarity index 66%
rename from iii-database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts
rename to database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts
index 73b265e9..1daa6ab4 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts
+++ b/database/tests/e2e/workers/harness/src/cases-tx-control-bypass.ts
@@ -1,12 +1,12 @@
-import type { TestCase } from './cases.ts';
-import { expect, expectEqual } from './cases.ts';
+import type { TestCase } from './cases.ts'
+import { expect, expectEqual } from './cases.ts'
/**
* Side-channel finalization bypass repros.
*
* Each case demonstrates a separate way the
* `transactionExecute`/`transactionQuery` defense in
- * `iii-database/src/handlers/transaction_execute.rs::is_transaction_control_sql`
+ * `database/src/handlers/transaction_execute.rs::is_transaction_control_sql`
* can be bypassed to finalize an interactive transaction without going through
* `commitTransaction` / `rollbackTransaction`.
*
@@ -38,40 +38,40 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
// until the watcher fires.
name: 'bypass #1: transactionExecute /* */COMMIT must be rejected',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const table = 'bypass_comment_commit';
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const table = 'bypass_comment_commit'
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE ${table} (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
// Stage a row inside the txn. Visible from inside, not yet committed.
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO ${table} (n) VALUES (${ph1})`,
params: [1],
- });
+ })
// The bypass attempt. The defense MUST reject this — the user must
// be told to use `commitTransaction`.
- let rejected = false;
- let acceptedErr: unknown = null;
+ let rejected = false
+ let acceptedErr: unknown = null
try {
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: '/* sneak */COMMIT',
- });
+ })
} catch (e: any) {
- const msg = e?.message ?? String(e);
+ const msg = e?.message ?? String(e)
if (msg.includes('INVALID_PARAM')) {
- rejected = true;
+ rejected = true
} else {
- acceptedErr = e;
+ acceptedErr = e
}
}
@@ -79,25 +79,27 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
// The worker accepted the SQL. Now prove it desynced the state.
// Read from outside the txn — if the COMMIT landed at the driver
// level, the staged row is visible from a fresh connection.
- const outside = await call('iii-database::query', {
+ const outside = await call('database::query', {
db: driver,
sql: `SELECT COUNT(*) AS c FROM ${table}`,
- });
- const leakedCount = Number(outside.rows[0].c);
+ })
+ const leakedCount = Number(outside.rows[0].c)
throw new Error(
`BYPASS CONFIRMED [#1 comment-prefix]: '/* */COMMIT' was accepted ` +
`by transactionExecute; outside-tx COUNT=${leakedCount} ` +
`(>0 means the side-channel COMMIT landed at the driver, ` +
`desyncing the registry from the connection state)` +
(acceptedErr ? ` — non-INVALID_PARAM error: ${acceptedErr}` : ''),
- );
+ )
}
- expect(rejected, 'transactionExecute must reject /* */COMMIT with INVALID_PARAM');
+ expect(rejected, 'transactionExecute must reject /* */COMMIT with INVALID_PARAM')
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* may already be gone if the bypass succeeded */}
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* may already be gone if the bypass succeeded */
+ }
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
}
},
},
@@ -109,60 +111,62 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
// pinned connection until the watcher sweeps.
name: 'bypass #2: transactionQuery COMMIT must be rejected',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const table = 'bypass_query_commit';
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const table = 'bypass_query_commit'
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE ${table} (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO ${table} (n) VALUES (${ph1})`,
params: [2],
- });
+ })
- let rejected = false;
- let acceptedErr: unknown = null;
+ let rejected = false
+ let acceptedErr: unknown = null
try {
- await call('iii-database::transactionQuery', {
+ await call('database::transactionQuery', {
transaction_id: id,
sql: 'COMMIT',
- });
+ })
} catch (e: any) {
- const msg = e?.message ?? String(e);
+ const msg = e?.message ?? String(e)
if (msg.includes('INVALID_PARAM')) {
- rejected = true;
+ rejected = true
} else {
- acceptedErr = e;
+ acceptedErr = e
}
}
if (!rejected) {
- const outside = await call('iii-database::query', {
+ const outside = await call('database::query', {
db: driver,
sql: `SELECT COUNT(*) AS c FROM ${table}`,
- });
- const leakedCount = Number(outside.rows[0].c);
+ })
+ const leakedCount = Number(outside.rows[0].c)
throw new Error(
`BYPASS CONFIRMED [#2 transactionQuery]: 'COMMIT' was accepted by ` +
`transactionQuery (no is_transaction_control_sql guard exists ` +
`on the query path); outside-tx COUNT=${leakedCount} ` +
`(>0 means the side-channel COMMIT landed at the driver)` +
(acceptedErr ? ` — non-INVALID_PARAM error: ${acceptedErr}` : ''),
- );
+ )
}
- expect(rejected, 'transactionQuery must reject COMMIT with INVALID_PARAM');
+ expect(rejected, 'transactionQuery must reject COMMIT with INVALID_PARAM')
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
}
},
},
@@ -181,55 +185,57 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
name: 'bypass #3: transactionExecute START TRANSACTION must be rejected (mysql)',
applies: ['mysql_db'],
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const table = 'bypass_start_transaction';
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const table = 'bypass_start_transaction'
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE ${table} (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
// Stage a row in the OUTER txn that — on a healthy worker — would be
// discarded when we `rollbackTransaction` below. If `START TRANSACTION`
// is accepted, MySQL implicitly COMMITs that row before opening the
// new (untracked) txn, leaking the row past the eventual rollback.
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO ${table} (n) VALUES (${ph1})`,
params: [3],
- });
+ })
- let rejected = false;
- let acceptedErr: unknown = null;
+ let rejected = false
+ let acceptedErr: unknown = null
try {
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: 'START TRANSACTION',
- });
+ })
} catch (e: any) {
- const msg = e?.message ?? String(e);
+ const msg = e?.message ?? String(e)
if (msg.includes('INVALID_PARAM')) {
- rejected = true;
+ rejected = true
} else {
- acceptedErr = e;
+ acceptedErr = e
}
}
if (!rejected) {
// Roll back the (now-second) txn the user thinks they're in.
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
- const outside = await call('iii-database::query', {
+ const outside = await call('database::query', {
db: driver,
sql: `SELECT COUNT(*) AS c FROM ${table}`,
- });
- const leakedCount = Number(outside.rows[0].c);
+ })
+ const leakedCount = Number(outside.rows[0].c)
throw new Error(
`BYPASS CONFIRMED [#3 START TRANSACTION on MySQL]: ` +
`'START TRANSACTION' was accepted by transactionExecute; ` +
@@ -237,14 +243,16 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
`have undone the staged INSERT (>0 means MySQL implicitly ` +
`COMMITed the outer txn when START TRANSACTION ran)` +
(acceptedErr ? ` — non-INVALID_PARAM error: ${acceptedErr}` : ''),
- );
+ )
}
- expect(rejected, 'transactionExecute must reject START TRANSACTION with INVALID_PARAM');
+ expect(rejected, 'transactionExecute must reject START TRANSACTION with INVALID_PARAM')
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* may already be gone */}
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* may already be gone */
+ }
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
}
},
},
@@ -260,61 +268,63 @@ export const TX_CONTROL_BYPASS_CASES: TestCase[] = [
// predictable. PG/MySQL both reliably strip the line comment.
applies: ['pg_db', 'mysql_db'],
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const table = 'bypass_line_comment';
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
- await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const table = 'bypass_line_comment'
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE ${table} (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
- const begin = await call('iii-database::beginTransaction', { db: driver });
- const id = begin.transaction.id;
+ const begin = await call('database::beginTransaction', { db: driver })
+ const id = begin.transaction.id
try {
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: `INSERT INTO ${table} (n) VALUES (${ph1})`,
params: [4],
- });
+ })
- let rejected = false;
- let acceptedErr: unknown = null;
+ let rejected = false
+ let acceptedErr: unknown = null
try {
// Newline is required after `--` so the server sees COMMIT as a
// statement; otherwise the comment swallows the rest of the line.
- await call('iii-database::transactionExecute', {
+ await call('database::transactionExecute', {
transaction_id: id,
sql: '-- sneak\nCOMMIT',
- });
+ })
} catch (e: any) {
- const msg = e?.message ?? String(e);
+ const msg = e?.message ?? String(e)
if (msg.includes('INVALID_PARAM')) {
- rejected = true;
+ rejected = true
} else {
- acceptedErr = e;
+ acceptedErr = e
}
}
if (!rejected) {
- const outside = await call('iii-database::query', {
+ const outside = await call('database::query', {
db: driver,
sql: `SELECT COUNT(*) AS c FROM ${table}`,
- });
- const leakedCount = Number(outside.rows[0].c);
+ })
+ const leakedCount = Number(outside.rows[0].c)
throw new Error(
`BYPASS CONFIRMED [#1b line-comment]: '-- sneak\\nCOMMIT' was ` +
`accepted by transactionExecute; outside-tx COUNT=${leakedCount}` +
(acceptedErr ? ` — non-INVALID_PARAM error: ${acceptedErr}` : ''),
- );
+ )
}
- expectEqual(rejected, true, 'transactionExecute must reject --COMMIT');
+ expectEqual(rejected, true, 'transactionExecute must reject --COMMIT')
} finally {
try {
- await call('iii-database::rollbackTransaction', { transaction_id: id });
- } catch {/* ignore */}
- await call('iii-database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` });
+ await call('database::rollbackTransaction', { transaction_id: id })
+ } catch {
+ /* ignore */
+ }
+ await call('database::execute', { db: driver, sql: `DROP TABLE IF EXISTS ${table}` })
}
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/cases.ts b/database/tests/e2e/workers/harness/src/cases.ts
similarity index 55%
rename from iii-database/tests/e2e/workers/harness/src/cases.ts
rename to database/tests/e2e/workers/harness/src/cases.ts
index 1712ac00..51ba19df 100644
--- a/iii-database/tests/e2e/workers/harness/src/cases.ts
+++ b/database/tests/e2e/workers/harness/src/cases.ts
@@ -1,37 +1,37 @@
-import type { ISdk } from 'iii-sdk';
-import type { DriverKey, Dialect } from './dialect.ts';
+import type { ISdk } from 'iii-sdk'
+import type { DriverKey, Dialect } from './dialect.ts'
export interface CaseContext {
- driver: DriverKey;
- dialect: Dialect;
+ driver: DriverKey
+ dialect: Dialect
/** Calls a database worker function; returns parsed JSON or throws on engine error. */
- call: (functionId: string, payload: unknown) => Promise;
+ call: (functionId: string, payload: unknown) => Promise
/** Direct SDK access for trigger-config edge-case tests. */
- iii: ISdk;
+ iii: ISdk
/**
* Asserts that `fn()` rejects and the rejection message contains `expectedCode`.
* The worker wraps DbError as `IIIError::Handler(json_string)`, which the engine
* surfaces as the JS Error message; substring match is more resilient than
* strict JSON parsing across SDK versions.
*/
- expectError: (fn: () => Promise, expectedCode: string) => Promise;
+ expectError: (fn: () => Promise, expectedCode: string) => Promise
}
export interface TestCase {
- name: string;
+ name: string
/** If set, this case only runs on the listed drivers; otherwise it runs on all. */
- applies?: readonly DriverKey[];
- run(ctx: CaseContext): Promise;
+ applies?: readonly DriverKey[]
+ run(ctx: CaseContext): Promise
}
export function expectEqual(actual: unknown, expected: unknown, msg: string): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
- throw new Error(`${msg}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
+ throw new Error(`${msg}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}
}
export function expect(cond: boolean, msg: string): asserts cond {
- if (!cond) throw new Error(msg);
+ if (!cond) throw new Error(msg)
}
export const SCHEMA_RESET: TestCase = {
@@ -41,113 +41,113 @@ export const SCHEMA_RESET: TestCase = {
// surface that was removed on feat/database-and-skills. Drop defensively
// so re-runs against a stale data volume (docker / podman named volumes
// preserved across `--keep`) still come up clean.
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS outbox' });
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS __iii_cursors' });
- await call('iii-database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS t' });
- await call('iii-database::execute', {
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS outbox' })
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS __iii_cursors' })
+ await call('database::execute', { db: driver, sql: 'DROP TABLE IF EXISTS t' })
+ await call('database::execute', {
db: driver,
sql: `CREATE TABLE t (id ${dialect.idColumnDDL()}, n INT NOT NULL)`,
- });
+ })
},
-};
+}
export const FUNCTION_CASES: TestCase[] = [
{
name: 'query SELECT 1',
async run({ driver, call }) {
- const r = await call('iii-database::query', { db: driver, sql: 'SELECT 1 AS n' });
- expectEqual(r.row_count, 1, 'row_count');
- expect(Array.isArray(r.columns), 'columns is array');
- expect(r.columns.length === 1, 'one column');
- expectEqual(r.columns[0].name, 'n', 'column name');
+ const r = await call('database::query', { db: driver, sql: 'SELECT 1 AS n' })
+ expectEqual(r.row_count, 1, 'row_count')
+ expect(Array.isArray(r.columns), 'columns is array')
+ expect(r.columns.length === 1, 'one column')
+ expectEqual(r.columns[0].name, 'n', 'column name')
// Value may be number or numeric string depending on driver — accept either.
- const v = r.rows[0].n;
- expect(v === 1 || v === '1', `n value: ${JSON.stringify(v)}`);
+ const v = r.rows[0].n
+ expect(v === 1 || v === '1', `n value: ${JSON.stringify(v)}`)
},
},
{
name: 'execute INSERT (multi-row)',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const ph2 = dialect.placeholder(2);
- const r = await call('iii-database::execute', {
+ const ph1 = dialect.placeholder(1)
+ const ph2 = dialect.placeholder(2)
+ const r = await call('database::execute', {
db: driver,
sql: `INSERT INTO t (n) VALUES (${ph1}), (${ph2})`,
params: [10, 20],
- });
- expectEqual(r.affected_rows, 2, 'affected_rows after multi-row insert');
+ })
+ expectEqual(r.affected_rows, 2, 'affected_rows after multi-row insert')
},
},
{
name: 'query SELECT after insert',
async run({ driver, call }) {
- const r = await call('iii-database::query', {
+ const r = await call('database::query', {
db: driver,
sql: 'SELECT n FROM t ORDER BY id',
- });
- expectEqual(r.row_count, 2, 'two rows returned');
- const ns = r.rows.map((row: any) => Number(row.n));
- expectEqual(ns, [10, 20], 'row values');
+ })
+ expectEqual(r.row_count, 2, 'two rows returned')
+ const ns = r.rows.map((row: any) => Number(row.n))
+ expectEqual(ns, [10, 20], 'row values')
},
},
{
name: 'prepareStatement + runStatement',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const prep = await call('iii-database::prepareStatement', {
+ const ph1 = dialect.placeholder(1)
+ const prep = await call('database::prepareStatement', {
db: driver,
sql: `SELECT n FROM t WHERE n = ${ph1}`,
- });
- const handleId = prep.handle?.id;
- expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present');
+ })
+ const handleId = prep.handle?.id
+ expect(typeof handleId === 'string' && handleId.length > 0, 'handle id present')
- const r1 = await call('iii-database::runStatement', { handle_id: handleId, params: [10] });
- expectEqual(r1.row_count, 1, 'first runStatement row_count');
- expectEqual(Number(r1.rows[0].n), 10, 'first runStatement value');
+ const r1 = await call('database::runStatement', { handle_id: handleId, params: [10] })
+ expectEqual(r1.row_count, 1, 'first runStatement row_count')
+ expectEqual(Number(r1.rows[0].n), 10, 'first runStatement value')
- const r2 = await call('iii-database::runStatement', { handle_id: handleId, params: [20] });
- expectEqual(r2.row_count, 1, 'second runStatement row_count');
- expectEqual(Number(r2.rows[0].n), 20, 'second runStatement value');
+ const r2 = await call('database::runStatement', { handle_id: handleId, params: [20] })
+ expectEqual(r2.row_count, 1, 'second runStatement row_count')
+ expectEqual(Number(r2.rows[0].n), 20, 'second runStatement value')
},
},
{
name: 'transaction commit',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const r = await call('iii-database::transaction', {
+ const ph1 = dialect.placeholder(1)
+ const r = await call('database::transaction', {
db: driver,
statements: [
{ sql: `UPDATE t SET n = n + 1 WHERE n = ${ph1}`, params: [10] },
{ sql: `UPDATE t SET n = n + 1 WHERE n = ${ph1}`, params: [20] },
],
- });
- expectEqual(r.committed, true, 'committed');
- const verify = await call('iii-database::query', { db: driver, sql: 'SELECT n FROM t ORDER BY id' });
- const ns = verify.rows.map((row: any) => Number(row.n));
- expectEqual(ns, [11, 21], 'post-commit values');
+ })
+ expectEqual(r.committed, true, 'committed')
+ const verify = await call('database::query', { db: driver, sql: 'SELECT n FROM t ORDER BY id' })
+ const ns = verify.rows.map((row: any) => Number(row.n))
+ expectEqual(ns, [11, 21], 'post-commit values')
},
},
{
name: 'transaction rollback',
async run({ driver, dialect, call }) {
- const ph1 = dialect.placeholder(1);
- const before = await call('iii-database::query', { db: driver, sql: 'SELECT COUNT(*) AS c FROM t' });
- const beforeCount = Number(before.rows[0].c);
+ const ph1 = dialect.placeholder(1)
+ const before = await call('database::query', { db: driver, sql: 'SELECT COUNT(*) AS c FROM t' })
+ const beforeCount = Number(before.rows[0].c)
- const r = await call('iii-database::transaction', {
+ const r = await call('database::transaction', {
db: driver,
statements: [
{ sql: `INSERT INTO t (n) VALUES (${ph1})`, params: [999] },
// Second statement violates NOT NULL — forces rollback.
{ sql: `INSERT INTO t (n) VALUES (${ph1})`, params: [null] },
],
- });
- expectEqual(r.committed, false, 'committed=false');
- expectEqual(r.failed_index, 1, 'failed_index=1');
- expect(typeof r.error === 'object' && r.error !== null, 'structured error object');
+ })
+ expectEqual(r.committed, false, 'committed=false')
+ expectEqual(r.failed_index, 1, 'failed_index=1')
+ expect(typeof r.error === 'object' && r.error !== null, 'structured error object')
- const after = await call('iii-database::query', { db: driver, sql: 'SELECT COUNT(*) AS c FROM t' });
- expectEqual(Number(after.rows[0].c), beforeCount, 'row count unchanged after rollback');
+ const after = await call('database::query', { db: driver, sql: 'SELECT COUNT(*) AS c FROM t' })
+ expectEqual(Number(after.rows[0].c), beforeCount, 'row count unchanged after rollback')
},
},
-];
+]
diff --git a/iii-database/tests/e2e/workers/harness/src/dialect.test.ts b/database/tests/e2e/workers/harness/src/dialect.test.ts
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/src/dialect.test.ts
rename to database/tests/e2e/workers/harness/src/dialect.test.ts
diff --git a/iii-database/tests/e2e/workers/harness/src/dialect.ts b/database/tests/e2e/workers/harness/src/dialect.ts
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/src/dialect.ts
rename to database/tests/e2e/workers/harness/src/dialect.ts
diff --git a/iii-database/tests/e2e/workers/harness/src/runner.ts b/database/tests/e2e/workers/harness/src/runner.ts
similarity index 66%
rename from iii-database/tests/e2e/workers/harness/src/runner.ts
rename to database/tests/e2e/workers/harness/src/runner.ts
index cfe6c790..52fcc139 100644
--- a/iii-database/tests/e2e/workers/harness/src/runner.ts
+++ b/database/tests/e2e/workers/harness/src/runner.ts
@@ -1,27 +1,22 @@
-import { writeFileSync, mkdirSync } from 'node:fs';
-import { resolve } from 'node:path';
-import type { ISdk } from 'iii-sdk';
-import { DRIVER_KEYS, dialects, type DriverKey } from './dialect.ts';
-import {
- SCHEMA_RESET,
- FUNCTION_CASES,
- type CaseContext,
- type TestCase,
-} from './cases.ts';
-import { BOUNDARY_CASES } from './cases-boundary.ts';
-import { PROTOCOL_CASES } from './cases-protocol.ts';
-import { TRANSACTION_EDGE_CASES } from './cases-transaction.ts';
-import { INTERACTIVE_TX_CASES } from './cases-interactive-tx.ts';
-import { CONCURRENCY_CASES } from './cases-concurrency.ts';
-import { ROW_CHANGE_CASES } from './cases-row-change.ts';
-import { TX_CONTROL_BYPASS_CASES } from './cases-tx-control-bypass.ts';
+import { writeFileSync, mkdirSync } from 'node:fs'
+import { resolve } from 'node:path'
+import type { ISdk } from 'iii-sdk'
+import { DRIVER_KEYS, dialects, type DriverKey } from './dialect.ts'
+import { SCHEMA_RESET, FUNCTION_CASES, type CaseContext, type TestCase } from './cases.ts'
+import { BOUNDARY_CASES } from './cases-boundary.ts'
+import { PROTOCOL_CASES } from './cases-protocol.ts'
+import { TRANSACTION_EDGE_CASES } from './cases-transaction.ts'
+import { INTERACTIVE_TX_CASES } from './cases-interactive-tx.ts'
+import { CONCURRENCY_CASES } from './cases-concurrency.ts'
+import { ROW_CHANGE_CASES } from './cases-row-change.ts'
+import { TX_CONTROL_BYPASS_CASES } from './cases-tx-control-bypass.ts'
interface CaseResult {
- driver: DriverKey;
- case: string;
- status: 'PASS' | 'FAIL';
- error?: string;
- duration_ms: number;
+ driver: DriverKey
+ case: string
+ status: 'PASS' | 'FAIL'
+ error?: string
+ duration_ms: number
}
/**
@@ -35,37 +30,37 @@ interface CaseResult {
* - `bypass-only` — ONLY the bypass repros. Use this to focus on the
* /review findings (post-fix it must be all-PASS).
*/
-export type RunnerMode = 'full' | 'no-bypass' | 'bypass-only';
+export type RunnerMode = 'full' | 'no-bypass' | 'bypass-only'
export interface RunnerOptions {
- iii: ISdk;
- reportPath: string;
- filterDriver?: DriverKey;
- mode?: RunnerMode;
+ iii: ISdk
+ reportPath: string
+ filterDriver?: DriverKey
+ mode?: RunnerMode
}
export class Runner {
constructor(private opts: RunnerOptions) {}
private async callOnce(functionId: string, payload: unknown): Promise {
- return await this.opts.iii.trigger({ function_id: functionId, payload });
+ return await this.opts.iii.trigger({ function_id: functionId, payload })
}
private async callWithRetry(functionId: string, payload: unknown, attempts = 10): Promise {
- let lastErr: unknown;
+ let lastErr: unknown
for (let i = 0; i < attempts; i++) {
try {
- return await this.callOnce(functionId, payload);
+ return await this.callOnce(functionId, payload)
} catch (e) {
- lastErr = e;
- await new Promise((r) => setTimeout(r, 200));
+ lastErr = e
+ await new Promise((r) => setTimeout(r, 200))
}
}
- throw lastErr;
+ throw lastErr
}
private async runCase(driver: DriverKey, c: TestCase): Promise {
- const start = Date.now();
+ const start = Date.now()
const ctx: CaseContext = {
driver,
dialect: dialects[driver],
@@ -73,20 +68,20 @@ export class Runner {
iii: this.opts.iii,
expectError: async (fn, expectedCode) => {
try {
- await fn();
+ await fn()
} catch (e: any) {
- const msg = e?.message ?? String(e);
+ const msg = e?.message ?? String(e)
if (!msg.includes(expectedCode)) {
- throw new Error(`expected error code "${expectedCode}", got: ${msg}`);
+ throw new Error(`expected error code "${expectedCode}", got: ${msg}`)
}
- return;
+ return
}
- throw new Error(`expected throw with code "${expectedCode}", but call resolved`);
+ throw new Error(`expected throw with code "${expectedCode}", but call resolved`)
},
- };
+ }
try {
- await c.run(ctx);
- return { driver, case: c.name, status: 'PASS', duration_ms: Date.now() - start };
+ await c.run(ctx)
+ return { driver, case: c.name, status: 'PASS', duration_ms: Date.now() - start }
} catch (e: any) {
return {
driver,
@@ -94,24 +89,23 @@ export class Runner {
status: 'FAIL',
error: e?.message ?? String(e),
duration_ms: Date.now() - start,
- };
+ }
}
}
private async waitForDatabaseWorker(driver: DriverKey): Promise {
// Probe with a no-op query until it succeeds; tolerates worker-startup race.
- await this.callWithRetry('iii-database::query', { db: driver, sql: 'SELECT 1' });
+ await this.callWithRetry('database::query', { db: driver, sql: 'SELECT 1' })
}
async runAll(): Promise<{ pass: number; total: number; results: CaseResult[] }> {
- const mode: RunnerMode = this.opts.mode ?? 'full';
- const drivers: DriverKey[] = this.opts.filterDriver ? [this.opts.filterDriver] : [...DRIVER_KEYS];
+ const mode: RunnerMode = this.opts.mode ?? 'full'
+ const drivers: DriverKey[] = this.opts.filterDriver ? [this.opts.filterDriver] : [...DRIVER_KEYS]
// Wait for the database worker to be reachable on the first driver before kicking off.
- await this.waitForDatabaseWorker(drivers[0]);
+ await this.waitForDatabaseWorker(drivers[0])
- const results: CaseResult[] = [];
- const matchesDriver = (driver: DriverKey, c: TestCase) =>
- !c.applies || c.applies.includes(driver);
+ const results: CaseResult[] = []
+ const matchesDriver = (driver: DriverKey, c: TestCase) => !c.applies || c.applies.includes(driver)
// Stream each case result to stdout as it completes, instead of buffering
// until runAll returns. Slow tests (TTL expiry, pool exhaustion) take 5+
@@ -120,34 +114,34 @@ export class Runner {
// Color the PASS/FAIL tag green/red, but only when stdout is a TTY. When
// run-tests.sh redirects stdout to a log file, isTTY is false and we
// emit plain text (otherwise ANSI escapes show up as garbage in the log).
- const useColor = process.stdout.isTTY === true;
- const GREEN = useColor ? '\x1b[32m' : '';
- const RED = useColor ? '\x1b[31m' : '';
- const RESET = useColor ? '\x1b[0m' : '';
+ const useColor = process.stdout.isTTY === true
+ const GREEN = useColor ? '\x1b[32m' : ''
+ const RED = useColor ? '\x1b[31m' : ''
+ const RESET = useColor ? '\x1b[0m' : ''
const record = (r: CaseResult): CaseResult => {
- const color = r.status === 'PASS' ? GREEN : RED;
- const err = r.error ? ' — ' + r.error : '';
- console.log(`[harness] ${color}${r.status}${RESET} ${r.driver} :: ${r.case} (${r.duration_ms}ms)${err}`);
- results.push(r);
- return r;
- };
+ const color = r.status === 'PASS' ? GREEN : RED
+ const err = r.error ? ' — ' + r.error : ''
+ console.log(`[harness] ${color}${r.status}${RESET} ${r.driver} :: ${r.case} (${r.duration_ms}ms)${err}`)
+ results.push(r)
+ return r
+ }
// The full suite (`full` and `no-bypass`) needs the shared `t` table from
// SCHEMA_RESET. `bypass-only` doesn't, since every bypass case manages its
// own scratch table — skip the reset to avoid letting a stale schema from
// a prior run block the focused repro.
- const includeFullSuite = mode === 'full' || mode === 'no-bypass';
- const includeBypassRepros = mode === 'full' || mode === 'bypass-only';
+ const includeFullSuite = mode === 'full' || mode === 'no-bypass'
+ const includeBypassRepros = mode === 'full' || mode === 'bypass-only'
for (const driver of drivers) {
if (includeFullSuite) {
// Always run the schema reset; not a counted case but failures abort this driver.
- const reset = record(await this.runCase(driver, SCHEMA_RESET));
- if (reset.status === 'FAIL') continue;
+ const reset = record(await this.runCase(driver, SCHEMA_RESET))
+ if (reset.status === 'FAIL') continue
// Function suite.
for (const c of FUNCTION_CASES) {
- record(await this.runCase(driver, c));
+ record(await this.runCase(driver, c))
}
// Boundary, protocol, transaction-edge, interactive-tx, concurrency,
@@ -161,8 +155,8 @@ export class Runner {
...CONCURRENCY_CASES,
...ROW_CHANGE_CASES,
]) {
- if (!matchesDriver(driver, c)) continue;
- record(await this.runCase(driver, c));
+ if (!matchesDriver(driver, c)) continue
+ record(await this.runCase(driver, c))
}
}
@@ -172,21 +166,18 @@ export class Runner {
// together in the per-case summary as the documented coverage for
// the /review findings on this branch.
for (const c of TX_CONTROL_BYPASS_CASES) {
- if (!matchesDriver(driver, c)) continue;
- record(await this.runCase(driver, c));
+ if (!matchesDriver(driver, c)) continue
+ record(await this.runCase(driver, c))
}
}
}
- const counted = results.filter((r) => r.case !== 'schema-reset');
- const pass = counted.filter((r) => r.status === 'PASS').length;
+ const counted = results.filter((r) => r.case !== 'schema-reset')
+ const pass = counted.filter((r) => r.status === 'PASS').length
- mkdirSync(resolve(this.opts.reportPath, '..'), { recursive: true });
- writeFileSync(
- this.opts.reportPath,
- JSON.stringify({ pass, total: counted.length, results, mode }, null, 2),
- );
+ mkdirSync(resolve(this.opts.reportPath, '..'), { recursive: true })
+ writeFileSync(this.opts.reportPath, JSON.stringify({ pass, total: counted.length, results, mode }, null, 2))
- return { pass, total: counted.length, results };
+ return { pass, total: counted.length, results }
}
}
diff --git a/database/tests/e2e/workers/harness/src/test.ts b/database/tests/e2e/workers/harness/src/test.ts
new file mode 100644
index 00000000..4f721721
--- /dev/null
+++ b/database/tests/e2e/workers/harness/src/test.ts
@@ -0,0 +1,17 @@
+import { call } from 'iii-sdk'
+
+await call('database::execute', {
+ db: 'primary',
+ sql: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)',
+})
+
+await call('database::execute', {
+ db: 'primary',
+ sql: 'INSERT INTO users (email) VALUES (?), (?)',
+ params: ['a@x', 'b@x'],
+})
+
+const { rows } = await call('database::query', {
+ db: 'primary',
+ sql: 'SELECT id, email FROM users ORDER BY id',
+})
diff --git a/iii-database/tests/e2e/workers/harness/src/worker.ts b/database/tests/e2e/workers/harness/src/worker.ts
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/src/worker.ts
rename to database/tests/e2e/workers/harness/src/worker.ts
diff --git a/iii-database/tests/e2e/workers/harness/tsconfig.json b/database/tests/e2e/workers/harness/tsconfig.json
similarity index 100%
rename from iii-database/tests/e2e/workers/harness/tsconfig.json
rename to database/tests/e2e/workers/harness/tsconfig.json
diff --git a/iii-database/tests/integration.rs b/database/tests/integration.rs
similarity index 75%
rename from iii-database/tests/integration.rs
rename to database/tests/integration.rs
index dba1429b..d8aa02c7 100644
--- a/iii-database/tests/integration.rs
+++ b/database/tests/integration.rs
@@ -1,16 +1,16 @@
//! Integration: build a local AppState from a YAML config and exercise each
//! function handler end-to-end against an in-memory SQLite database.
-use iii_database::config::WorkerConfig;
-use iii_database::handle::HandleRegistry;
-use iii_database::handlers::execute::ExecuteReq;
-use iii_database::handlers::prepare::PrepareReq;
-use iii_database::handlers::query::QueryReq;
-use iii_database::handlers::run_statement::RunReq;
-use iii_database::handlers::transaction::TxReq;
-use iii_database::handlers::{execute, prepare, query, run_statement, transaction, AppState};
-use iii_database::pool;
-use iii_database::transaction::TxRegistry;
+use database::config::WorkerConfig;
+use database::handle::HandleRegistry;
+use database::handlers::execute::ExecuteReq;
+use database::handlers::prepare::PrepareReq;
+use database::handlers::query::QueryReq;
+use database::handlers::run_statement::RunReq;
+use database::handlers::transaction::TxReq;
+use database::handlers::{execute, prepare, query, run_statement, transaction, AppState};
+use database::pool;
+use database::transaction::TxRegistry;
use iii_sdk::{Logger, RegisterFunction};
use serde_json::json;
use std::collections::HashMap;
@@ -128,7 +128,7 @@ async fn end_to_end_query_execute_prepare_run_transaction() {
#[test]
fn binary_name_matches_manifest() {
- assert_eq!(iii_database::worker_name(), "iii-database");
+ assert_eq!(database::worker_name(), "database");
}
/// Regression: every RPC function must register through the typed
@@ -176,39 +176,39 @@ fn registered_functions_carry_request_and_response_schemas() {
unreachable!()
}
async fn _bt(
- _: iii_database::handlers::begin_transaction::BeginTxReq,
- ) -> Result {
+ _: database::handlers::begin_transaction::BeginTxReq,
+ ) -> Result {
unreachable!()
}
async fn _tq(
- _: iii_database::handlers::transaction_query::TxQueryReq,
+ _: database::handlers::transaction_query::TxQueryReq,
) -> Result {
unreachable!()
}
async fn _te(
- _: iii_database::handlers::transaction_execute::TxExecuteReq,
- ) -> Result {
+ _: database::handlers::transaction_execute::TxExecuteReq,
+ ) -> Result {
unreachable!()
}
async fn _ct(
- _: iii_database::handlers::commit_transaction::CommitTxReq,
- ) -> Result {
+ _: database::handlers::commit_transaction::CommitTxReq,
+ ) -> Result {
unreachable!()
}
async fn _rt(
- _: iii_database::handlers::rollback_transaction::RollbackTxReq,
- ) -> Result {
+ _: database::handlers::rollback_transaction::RollbackTxReq,
+ ) -> Result {
unreachable!()
}
- assert_schemas("iii-database::query", _q);
- assert_schemas("iii-database::execute", _e);
- assert_schemas("iii-database::prepareStatement", _p);
- assert_schemas("iii-database::runStatement", _r);
- assert_schemas("iii-database::transaction", _t);
- assert_schemas("iii-database::beginTransaction", _bt);
- assert_schemas("iii-database::transactionQuery", _tq);
- assert_schemas("iii-database::transactionExecute", _te);
- assert_schemas("iii-database::commitTransaction", _ct);
- assert_schemas("iii-database::rollbackTransaction", _rt);
+ assert_schemas("database::query", _q);
+ assert_schemas("database::execute", _e);
+ assert_schemas("database::prepareStatement", _p);
+ assert_schemas("database::runStatement", _r);
+ assert_schemas("database::transaction", _t);
+ assert_schemas("database::beginTransaction", _bt);
+ assert_schemas("database::transactionQuery", _tq);
+ assert_schemas("database::transactionExecute", _te);
+ assert_schemas("database::commitTransaction", _ct);
+ assert_schemas("database::rollbackTransaction", _rt);
}
diff --git a/iii-database/tests/value_coercion.rs b/database/tests/value_coercion.rs
similarity index 95%
rename from iii-database/tests/value_coercion.rs
rename to database/tests/value_coercion.rs
index 0872fd08..a82f27c5 100644
--- a/iii-database/tests/value_coercion.rs
+++ b/database/tests/value_coercion.rs
@@ -1,6 +1,6 @@
//! Cross-cutting coercion tests — every JSON shape, both directions.
-use iii_database::value::{JsonParam, RowValue};
+use database::value::{JsonParam, RowValue};
use serde_json::json;
#[test]
diff --git a/iii-database/skills/index.md b/iii-database/skills/index.md
deleted file mode 100644
index aceaa6b2..00000000
--- a/iii-database/skills/index.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-type: index
-title: iii-database
----
-
-# iii-database
-
-Connect to PostgreSQL, MySQL, and SQLite from the iii engine. Run read-only
-queries, write statements, atomic transactions, and prepared-statement
-sequences over a managed per-database connection pool. Every callable
-surface lives under the single `iii-database::*` namespace; SQLite is the
-recommended starting point because it needs no server, just a file.
-
-The worker resolves the driver from each database's URL scheme (`sqlite:`,
-`postgres://`, `postgresql://`, `mysql://`). For the `databases:` config
-block, TLS modes, error-code reference, and the per-driver compatibility
-table (e.g. `returning:` is a no-op on MySQL; SQLite degrades
-`read_committed` / `repeatable_read` to `serializable`), see
-[the README](../README.md).
-
-## How-tos
-
-### `iii-database::*`
-
-- [`iii-database::query`](iii://iii-database/query) — read-only SQL; returns `{ rows, row_count, columns }` for SELECT-style statements.
-- [`iii-database::execute`](iii://iii-database/execute) — write SQL (INSERT/UPDATE/DELETE/DDL); returns `{ affected_rows, last_insert_id, returned_rows }`.
-- [`iii-database::prepareStatement` + `iii-database::runStatement`](iii://iii-database/prepared-statements) — prepare-once, run-many parameterized SQL; the prepare step pins a pool connection until TTL expiry, so always run before re-issuing the same statement many times.
-- [`iii-database::transaction`](iii://iii-database/transaction) — atomic batch sequence; one call, pass every statement together, rolls back on first failure with a `failed_index` pointer at the offending step.
-- [`iii-database::beginTransaction` + `transactionQuery` / `transactionExecute` + `commitTransaction` / `rollbackTransaction`](iii://iii-database/interactive-transaction) — stateful interactive transaction with a configurable timeout-driven auto-rollback. Use this when you need read-your-writes across several round-trips inside a single transaction.
-
-`iii-database::row-change` (Postgres logical replication via `pgoutput`) is
-registered as a trigger type but is **not yet functional in v1.0.0**:
-`register_trigger` returns `UNSUPPORTED` while the streaming decode loop
-waits on an upstream `tokio-postgres` replication API release. Operators
-can pre-provision slots and publications now; see the **Triggers** section
-of [the README](../README.md) for current status and cleanup commands.
diff --git a/iii-database/tests/e2e/reports/.gitkeep b/iii-database/tests/e2e/reports/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/iii-database/tests/e2e/workers/harness/src/test.ts b/iii-database/tests/e2e/workers/harness/src/test.ts
deleted file mode 100644
index a45789e9..00000000
--- a/iii-database/tests/e2e/workers/harness/src/test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { call } from 'iii-sdk'
-
-await call('iii-database::execute', {
- db: 'primary',
- sql: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)'
-})
-
-await call('iii-database::execute', {
- db: 'primary',
- sql: 'INSERT INTO users (email) VALUES (?), (?)',
- params: ['a@x', 'b@x']
-})
-
-const { rows } = await call('iii-database::query', {
- db: 'primary',
- sql: 'SELECT id, email FROM users ORDER BY id'
-})
\ No newline at end of file
diff --git a/shell/tests/e2e/README.md b/shell/tests/e2e/README.md
index 23df1553..f80d1b4d 100644
--- a/shell/tests/e2e/README.md
+++ b/shell/tests/e2e/README.md
@@ -5,7 +5,7 @@ functions (`shell::exec`, `shell::exec_bg`, `shell::kill`, `shell::status`,
`shell::list`), every safety guardrail (allowlist, denylist, timeout, output
truncation, env scrubbing), and the background-job lifecycle in one command.
-Modeled on `iii-database/tests/e2e/`.
+Modeled on `database/tests/e2e/`.
## Prerequisites
diff --git a/storage/src/error.rs b/storage/src/error.rs
index 921bc28d..ee4e4b6a 100644
--- a/storage/src/error.rs
+++ b/storage/src/error.rs
@@ -1,5 +1,5 @@
//! Discriminated, stable error codes returned to the engine. Mirrors
-//! `iii-database/src/error.rs`. The `code` field is the contract; the rest
+//! `database/src/error.rs`. The `code` field is the contract; the rest
//! is diagnostic.
use crate::backend::BackendError;
@@ -77,7 +77,7 @@ pub enum StorageError {
impl StorageError {
/// Convert into the JSON string body that handlers return as their `Err`.
- /// Mirrors `iii-database`'s `err_to_str`.
+ /// Mirrors `database`'s `err_to_str`.
pub fn to_wire_string(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
format!(
diff --git a/storage/src/handlers/mod.rs b/storage/src/handlers/mod.rs
index 1a912c4a..51df982b 100644
--- a/storage/src/handlers/mod.rs
+++ b/storage/src/handlers/mod.rs
@@ -31,7 +31,7 @@ impl AppState {
}
}
-/// JSON-string error body, mirroring iii-database's err_to_str.
+/// JSON-string error body, mirroring database's err_to_str.
pub fn err_to_str(e: StorageError) -> String {
e.to_wire_string()
}
diff --git a/storage/src/triggers/dispatcher.rs b/storage/src/triggers/dispatcher.rs
index 8684376e..d31eabbe 100644
--- a/storage/src/triggers/dispatcher.rs
+++ b/storage/src/triggers/dispatcher.rs
@@ -77,7 +77,7 @@ impl EventDispatcher for EngineDispatcher {
.await
{
Ok(Ok(value)) => {
- // Match iii-database's contract: `null` from a void-returning
+ // Match database's contract: `null` from a void-returning
// function counts as ack=true; otherwise the function may
// return `{ack: bool}` to control redelivery explicitly.
if value.is_null() {
diff --git a/storage/tests/e2e/config.yaml b/storage/tests/e2e/config.yaml
index 25c6f14d..a961c21c 100644
--- a/storage/tests/e2e/config.yaml
+++ b/storage/tests/e2e/config.yaml
@@ -8,7 +8,7 @@
# The storage worker config is inlined under its worker entry. The
# engine serializes this `config:` value to a temp YAML and threads
# `--config ` through to the spawned binary (same delivery path
-# iii-database uses for its e2e config).
+# database uses for its e2e config).
#
# Triggers are registered DYNAMICALLY by the harness via the SDK
# (`registerTrigger` in runner.ts) so the harness can route dispatches to
diff --git a/storage/tests/e2e/run-tests.sh b/storage/tests/e2e/run-tests.sh
index 760d085e..3505f8c5 100755
--- a/storage/tests/e2e/run-tests.sh
+++ b/storage/tests/e2e/run-tests.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# E2E orchestrator for the storage worker, modelled on
-# iii-database/tests/e2e/run-tests.sh. Builds the worker binary, starts the
+# database/tests/e2e/run-tests.sh. Builds the worker binary, starts the
# engine, launches the TS harness, greps for the HARNESS_DONE sentinel,
# pretty-prints the report, and propagates pass/fail as the exit code.
#