Skip to content

refactor(linux)!: switch the Linux app to driver contract v2 - #3358

Merged
datlechin merged 37 commits into
linuxfrom
refactor/linux-driver-contract-v2
Oct 13, 2026
Merged

datlechin merged 37 commits into
linuxfrom
refactor/linux-driver-contract-v2

Conversation

@datlechin

@datlechin datlechin commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Why

The Linux app ran on the first driver contract: host and port connect options, one shared DatabaseService and an interim russh tunnel. SSH ignored ~/.ssh/config and agents, nothing asked for a password the keyring lacked, and imported connections could not connect.

Change

  • Every driver is on contract v2: lossless values, per-engine dialects and editor sessions. PostgreSQL runs on tokio-postgres, SQL Server on tiberius 0.13 from crates.io.
  • SSH runs through the system OpenSSH client, with password prompts and keyring credentials, and sessions live in a new session crate.
  • The connect dialog has a Security group and a SQLite file chooser. The SQL editor runs one statement at a time, with a results list.

Verified

Each crate's unit and docker suites pass, SQL Server's included. A local run reproduced the CI crash: a debug-build stack overflow leaked SQL Server containers until the runner ran out of memory.

Not in this PR

@datlechin datlechin added the platform-linux About the Linux app label Oct 12, 2026
A dialog torn down without a response made the binding's choose
callback abort the process. The presenter now waits on the response
and closed signals itself, so a forced close reads as a cancel.
The connect dialog is a gtk-rs subclass with CompositeTemplate .ui files
in the GResource. It checks the form and emits a ConnectSubmission
(request and PendingSave); the window connects, saves the entry and
shows a Cancel button on the Connecting toast. Only Test connects from
the dialog, and it closes the connection in the same task.

- Security group: Encryption over the driver's TLS modes (Verify full
  by default), CA certificate, client certificate and key, TLS server
  name. The PEM files are read on the blocking pool with a generation,
  so Connect waits for the newest check.
- SQLite file row with Choose and New Database through the portal.
- SSH: agent, private key, password and keyboard-interactive; host
  aliases, port, user and jump hosts may come from the SSH config.
- Remember password, read-only subtitle by enforcement, TLS remedies
  on the Test toast and on the failed-connect alert, which reopens the
  dialog on what was submitted.
- PendingSave gains forget, so turning Remember off removes the old
  password from the keyring.
- Server codes are named per engine in error text.
…list

The SQL editor is a gtk-rs subclass with a CompositeTemplate. Each Run
goes through the tab's own EditorSession one ScriptPlan batch at a
time: one statement on semicolon engines, a whole GO batch (repeated
for GO n) on SQL Server. A failed statement stops the script unless
the engine runs batches independently, and the rest are shown as not
run.

- Results list: one line per result set, command (rows affected),
  notice, error (with Go to Error when the server gives a position),
  warning, stop, timeout or not-run statement, in an OverlaySplitView
  that folds below 560sp with a toggle to bring it back.
- Run sends the selection when there is one (Run Selection).
- The transaction state from the last run is shown above the results,
  and closing a tab with a transaction open asks first.
- An unterminated string or comment adds a warning and is still sent.
- A lost editor session asks the window's monitor to probe.
- replace_all_undoable and CursorAnchor make Format, a replaced query
  (with an Undo toast) and file loads one undo step; dropped files are
  read asynchronously and non-UTF-8 files are refused.
Every engine has its own SqlDialect now, so the driver-id keyed
EngineDialect, dialect_for and grammar_for go. The legacy DDL builder
keeps its own identifier quoting until per-engine DDL dialects replace
it, and the app and core tests use the PostgreSQL dialect or the core
test dialect. core's MySQL COLUMN_TYPE parser goes too: the MySQL
driver has its own scanner.

- rust_decimal leaves the workspace and sqlx's features; deny.toml bans
  it, as decimals are SqlDecimal.
- CI runs the tablepro-session docker tests in the ubuntu:26.04 image
  with the host's Docker socket, since storage needs GLib 2.88.
- docs/testing.md documents the conformance and TLS conformance suites
  and drops the v1 examples.
- linux/CHANGELOG.md gains the user-visible entries for this work.
@datlechin

datlechin commented Oct 12, 2026 •

Copy link
Copy Markdown
Member Author

Handoff: state of this PR on 2026-10-12, for the next machine

Work moves to another machine. This comment and the ones after it hold everything the next agent needs; nothing lives only on the old machine. Branch head: 89a9b2b4f.

Start here

git fetch origin
git switch refactor/linux-driver-contract-v2   # tracks origin, head 89a9b2b4f

Then read, in order: this comment, the plan comment (plan.md + problem.md), and the five lane reports. The lane reports were the local, gitignored .analysis/refactor-linux-driver-contract-v2/ tree. To restore it, save each comment body into that path under the file name in its title.

Scope and decisions already made

  • Plan: linux/docs/plan-2026-09-14-audit-remediation.md, Steps 30 to 66, in four groups, one PR each, base linux:
    1. Steps 30-39: driver contract v2, lossless values, dialects, OpenSSH transport, editor sessions, write/undo ledger.
    2. Steps 40-45: application shell without Relm4, D-Bus uniqueness, one window per session, credential prompts.
    3. Steps 46-55: grid, export, history, find.
    4. Steps 56-66: connection editing, status pages, notifications, HIG, accessibility, CI, packaging, docs.
  • Group 1 is two PRs: 1a = Step 30 = this PR, 1b = Steps 31-39.
  • Where the plan and linux/docs/dependency-review-2026-09-14.md disagree, the dependency review wins:
    • no private forks;
    • Relm4 replaced by gtk-rs subclasses with CompositeTemplate .ui files and gio actions, in every file a task rewrites;
    • SSH only through the system OpenSSH client.
  • Group 1 calls:
    • W1-12's credential prompter is in Step 30.
    • Keyring write failure keeps the shipped rule: save the connection without the secret and show a toast. W4-06's keyring_failure_saves_no_entry is dropped.
    • SQLite decodes by storage class. The column kind presents BOOLEAN and DATE.
    • Session lives in crates/session, and channels are tokio::sync.
    • tiberius 0.13 comes from crates.io.
    • ClickHouse has one transport-backed connector, with lz4 off.
    • ColumnKind, ColumnType and CatalogType take the FB-C5/C6 shape.
    • The dialects live in the driver crates, with a grammar() accessor.
  • SavedConnection.use_tls became tls: TlsConfig with no migration, as the plan says. An old connections.json without tls does not load: regenerate test connections in the new format.
  • Merging: once this PR is green and reviewed, squash-merge it into linux yourself, then start 1b from the merged linux. linux merges into main only near the first Linux release.

Commit map (oldest first)

Commit Content
a0c47bc tiberius 0.13 from crates.io, no [patch]
cc89cf9 ColumnKind / ColumnType / CatalogType payloads (W3-05)
b2222ca Connection, DatabaseDriver and ConnectOptions v2, TableDefinition family, ForeignKeyMode (W2-11, W3-18, W3-19)
f2a9fe7 one push_value binding path, build_page/build_count/build_export, plan_save, grammar() + ddl() (W3-06)
4eccf18 test-support v2 fakes, FaultTransport, connection and TLS conformance macros, fixture knobs (W9-07, W9-08)
67d5cf1 SQLite on v2 (W2-14)
8d23871 lexer keeps 0.5 as one numeric token
f65d647 PostgreSQL on tokio-postgres 0.7.18 + deadpool-postgres 0.14.2 + tokio-postgres-rustls 0.14.0 (W2-12)
1f51cab SQL Server on v2 (W2-15)
6ecd6ac ClickHouse on v2, query_raw with {pN:Type} (W2-16)
f7e12fe MySQL/MariaDB on v2, no relay (W2-13)
a786fc2 core follow-ups: DdlDialect rename/drop-default, ParamType::Bits, Statement::wrapped, DriverError::FileIo, ServerInfo.patch, stream_read conformance case
15425a0 SSH through system OpenSSH (ControlMaster, -O forward Unix sockets, askpass to CredentialPrompter); crates/session; russh removed (W1-11, W4-06)
0c6b6bb, 776ca75 app moved onto the session crate
84c147a credential prompts end without choose_future (crash fix)
a91ce63 POTFILES for the session and prompt files
5989999 secrets step isolated from the developer's keyring (also on linux via #3391)
5811951 connect dialog as a template subclass: Security group, SQLite file chooser, SSH rows (W6-13, W2-17)
2d4975f editor runs scripts per statement on EditorSession with a results list; replace is undoable (W6-12)
89a9b2b v1 dialect stand-ins and rust_decimal deleted, deny.toml ban, CI docker job for tablepro-session, docs/testing.md conformance sections, CHANGELOG

Verified on Linux (Ubuntu 26.04 arm64, GNOME 50), before 89a9b2b

Package Unit Docker Other
tablepro-core 511
test-support 36
test-fixtures 26
sqlite 106 11 conformance cases inside unit
postgres 80 84 15 conformance + 6 TLS
mysql 55 89 MySQL and MariaDB conformance + TLS
clickhouse 58 45
mssql 104 CI only no arm64 image; 53 docker tests compile
ssh 55 13
session 114 3
storage 136 secrets 4
app (tablepro) 329 all Meson suites 1681/1681; lint, l10n, data PASS

Not yet verified: 89a9b2b4f. The disk filled during its Meson run, so build, test, lint, l10n and data never finished on it. The CI run for it (Build Linux) is the first full check.

What is left in this PR (1a)

  1. Get CI green on 89a9b2b4f (workflow Build Linux):
    • Watch the docker jobs. The most likely failure is SQL Server conformance, which runs with unsupported_types: true. The 53 MSSQL docker tests have never run anywhere but CI.
    • Watch the new tablepro-session docker job. It runs in an ubuntu:26.04 container with the host Docker socket, because storage needs GLib 2.88.
  2. Finish the 14b leftovers: tls_remedy and script_diagnostic_message are only partly wired into the UI (W6-11, W2-17).
  3. Run the app against PostgreSQL, MySQL, ClickHouse and SQLite, with connections in the new format (tls field):
    • connect;
    • the password prompt;
    • the Security group;
    • SSH through an alias;
    • the editor results list;
    • undo after Format.
  4. Review:
    • one independent review of the whole diff;
    • one security pass on credentials (keyring, askpass, prompter), SQL building (binding, identifier quoting, dialects) and SSH (ControlMaster socket paths and permissions, host key prompts).
  5. Rewrite the PR body to .github/pull_request_template.md, mark the PR ready, keep CI green, then squash-merge into linux.

Known gaps to carry into 1b or later

  • PostgreSQL spec tests not written yet:
    • ctid_window_explain
    • cursor_row_limit_reads_under_1mib
    • row_limit_server_stopped_select_list_generate_series
    • begin_insert_big_select_commit_persists
    • statement_pool_editor_cursor_falls_back
  • sqlx 0.9.0 cannot set TCP_NODELAY, so FC-D3 is unmet for MySQL and SQLite.
  • Unattended reconnects cancel the askpass prompt. The first reconnect after Retry is attended.
  • The History and Saved Queries dialogs still use Relm4 commands (Group 2 removes Relm4).
  • test-support still lacks:
    • the 50k-row streaming case;
    • the ClickHouse mutations check;
    • a "black hole after N bytes" fault transport.
  • The legacy DDL builder still quotes identifiers itself, until W3-12's per-engine DdlDialect plans in Step 35.
  • ADR numbering: W2-19's ADR takes number 0016.
  • The CSV import fix (ui/app/import.rs polled a sqlx future on the GTK thread) and the LIBEXECDIR fallback for tablepro-askpass are done. Confirm both in the review.

Next PRs

  • 1b, Steps 31-39, from the merged linux. The per-task gap analysis, with the review amendments applied, is in the lane reports below.

    Step Tasks
    31 W2-19, W4-07, W6-14
    32 W3-17, W6-15
    33 W4-08, W4-09
    34 W4-10
    35 W3-12, W5-01, W9-09
    36 W3-13, W4-11
    37 W4-12
    38 W3-14, W5-02
    39 W3-15, W5-03, W6-17
  • Group 2 (Steps 40-45), Group 3 (46-55) and Group 4 (56-66) follow, one PR each.

  • chore(claude-md): finish the fix-issue split on main #3355 (base main) is still open. It carries two late fix-issue skill commits to main; linux already has them.

How to work on it

Linux verification needs Linux:

  • GTK 4.22, libadwaita 1.9, GtkSourceView 5.18 and GLib 2.88, which is the GNOME 50 floor;
  • Rust stable;
  • Meson;
  • Docker.

The verify.sh Linux backend runs only on Linux. On a Mac, use an arm64 Ubuntu 26.04 VM, or push and read the Build Linux workflow. Do not try to build the GTK app natively on macOS.

V=.claude/skills/fix-issue/scripts/verify.sh
$V --platform linux setup                 # once per tree, and after a merge from linux
$V --platform linux build
$V --platform linux test <package> [filter]   # tablepro-core, tablepro-postgres, tablepro-session, tablepro ...
$V --platform linux test                  # every Meson suite, as CI's fast job
$V --platform linux lint                  # rustfmt, clippy with and without default features
$V --platform linux l10n
$V --platform linux data
$V --platform linux docker <package>      # the real engines, needs Docker
$V --platform linux secrets               # Secret Service tests in a throwaway keyring

By hand, from linux/:

  • meson setup _build -Dprofile=development
  • meson compile -C _build
  • meson devenv -C _build ./crates/app/tablepro

Rules for this work

  • Never force-push or rewrite published history. The PR is squash-merged, so each intermediate commit need not build.
  • No AI attribution lines in commits or PR text.
  • Run the Secret Service tests only through verify.sh --platform linux secrets. That step uses isolated XDG directories and a throwaway D-Bus session. Running gnome-keyring-daemon against the real session re-keys the developer's login keyring. That happened once, and 5989999ce / fix(linux): keep the secrets step off the developer's login keyring #3391 fix it.
  • Run --ignored tests only through the docker and secrets steps.
  • Commit scope linux or linux-<area>. Linux user-visible changes go in linux/CHANGELOG.md, never the root one.
  • Writing style: no em dashes. scripts/check-banned-words.sh --base origin/linux must stay clean; it is clean at 89a9b2b4f.
  • Large disk use: one _build with its Cargo target takes up to about 70 GB. Run cargo clean inside _build when space runs low.

Working preferences of the maintainer

These lived only in the old machine's agent memory:

  • Reply to the maintainer in Vietnamese. Everything committed to the repo stays in English.
  • Use the standard, widely used convention for a thing, never a custom or hacky mechanism. When there is a choice, name the convention and its source, then recommend one.
  • Analyze before a redesign, then show a short plan for approval. The maintainer answers tersely ("ok", numbered replies).
  • Keep workflows small (under 5 agents) when token budget matters.
  • Work through the fix-issue skill with its Linux profile (--platform linux).
  • Track progress in the tree's .analysis/<slug>/plan.md, a local file outside git.

Databases for manual app runs

The tests start their own containers through crates/test-fixtures. For running the app by hand, the old machine used one compose file outside the repo with:

  • PostgreSQL 18 on 5432;
  • MySQL 8.4 on 3306;
  • ClickHouse 25.8 on 8123;
  • a shop.sqlite file.

Each server had user and password tablepro and database shop. Recreate that locally. SQL Server has no arm64 image.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/plan.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

plan.md

Plan: PR 1a, Step 30 (driver contract v2), base linux

Progress log

  • 2026-10-12: gap analysis done (lanes/*.md). Decisions below agreed with the user.
  • 2026-10-12: [1] done, a0c47bc (tiberius 0.13; mssql + fixtures tests and workspace lint pass). Draft PR refactor(linux)!: switch the Linux app to driver contract v2 #3358.
  • 2026-10-12: [2] done (W3-05 reconciliation; core 475/475, clippy clean). Drivers and app stop compiling until their rewrites.
  • 2026-10-12: [3]+[4] done (ForeignKeyMode, TableDefinition family, Connection/DatabaseDriver/ConnectOptions v2, v1 query.rs and read_only.rs deleted; core 486/486). No trust field on NetworkTarget: drivers build trust from tls (FC-D8).
  • 2026-10-12: [5] done (one push_value path, build_page/count/export, plan_save, edit API, xmin row id, grammar() + ddl(); core 510/510). engine_dialect.rs stays until drivers own their dialects.
  • 2026-10-12: [6] done (v2 fakes, FaultTransport, connection + TLS conformance macros, fixture options; test-support 34, fixtures 25, 7 docker fixture tests). No FakeConnector until Session exists. Missing: the 50k-row streaming case, ClickHouse mutations check, black-hole-after-N-bytes.
  • 2026-10-12: [7] SQLite done (104/104 incl. 11 conformance cases; dialect in crate; storage-class decode). Its Cargo.lock change rides with [8]. Gaps it found: no DriverError I/O variant; core lexer splits 0.5 into three tokens; no stream_read conformance case.
  • 2026-10-12: [8] PostgreSQL done (tokio-postgres 0.7.18, deadpool-postgres 0.14.2, tokio-postgres-rustls 0.14.0; unit 80/80, docker 81/81 incl. 14 conformance + 6 TLS). Missing: PostgresFixture with a PkiAlgorithm (blocks 2 channel-binding tests); core ParamType has no Bits; Statement::new is pub(super). Unwritten spec tests listed in the agent report: ctid_window_explain, cursor_row_limit_reads_under_1mib, row_limit_server_stopped_select_list_generate_series, begin_insert_big_select_commit_persists, statement_pool_editor_cursor_falls_back.
  • [9] MySQL, [10] ClickHouse, [11] SQL Server ran in parallel (deadpool and clickhouse-types added to the workspace first); usage limits stopped them twice, resumed at 19:20.
  • 2026-10-12: [9] MySQL/MariaDB done (unit 53/53, docker 87/87 incl. 13+14 conformance and 6+6 TLS; no relay). All five drivers are on v2.
  • 2026-10-12: follow-ups done, a786fc2 (DdlDialect rename/drop-default, ParamType::Bits, Statement::wrapped, DriverError::FileIo, ServerInfo.patch, 15th conformance case stream_read, PG channel-binding tests, MySQL trusting_function_creators, tiberius and clickhouse features trimmed, russh gone from the workspace).
  • 2026-10-12: [12]+[13] done, 15425a0 (ssh 55 + docker 13; session 114 + docker 3; storage 136 + secrets 4). SavedConnection.use_tls -> tls with no migration. Unattended reconnects still cancel askpass; the first reconnect after Retry is Attended.
  • 2026-10-12: [14a] done (84c147a, a91ce63): app builds and runs on v2 + session; GtkCredentialPrompter; import.rs fixed; verified build, test tablepro 329/329, lint, all suites 1681/1681, l10n, data. Old dev connections.json no longer loads (no tls, no migration by plan).
  • 2026-10-12: secrets step rewrote the developer's real login keyring with "ci\n" (no XDG isolation). Repaired with the user's consent (password now "ci"; user resets it to the login password in seahorse). Fixed in 5989999 and on linux via fix(linux): keep the secrets step off the developer's login keyring #3391.
  • 2026-10-12: [14b]+[15] running in one agent with local milestone commits (connect dialog v2 + W2-17, editor on EditorSession + undoable replace, then v1 deletions, CI matrix, testing.md, CHANGELOG).
  • 2026-10-13: [14a] partial app work committed as 0c6b6bb after the session ended (em dashes replaced: fks action_text -> "not reported", index_key_summary -> "no keys"); cargo clean freed 47.7 GB; agent resumed.
  • 2026-10-12: [14a] app adoption running (build + tests on the new stack with today's UI, GtkCredentialPrompter, import.rs fix). Next: [14b] connect dialog v2 and editor on EditorSession as gtk-rs subclasses; [15] delete v1 leftovers (engine_dialect, mysql_column_type, rust_decimal, sqlx rust_decimal feature), CI docker matrix gains tablepro-session, CHANGELOG, docs/testing.md.
  • Follow-ups before [12] (core and workspace cleanup, one commit): delete core engine_dialect.rs and sql_syntax::mysql_column_type once nothing uses them; DdlDialect methods for the SQL Server default drop and rename; classify_default; ParamType::Bits; Statement::new visibility; DriverError I/O variant; ServerInfo patch version; a stream_read conformance case; PostgresFixture PkiAlgorithm; MySQL fixture log_bin_trust_function_creators; workspace features: tiberius without rust_decimal, clickhouse without its rustls features; deny.toml wrappers for the driver crates.
  • 2026-10-12: [10] ClickHouse done (unit 57/57, docker 44/44 incl. 11 conformance + 6 TLS). Follow-ups: drop rustls-tls-aws-lc and rustls-tls-webpki-roots from the workspace clickhouse features; deny.toml wrappers; core has no classify_default; SSH-forward test waits for the ssh rewrite.
  • 2026-10-12: [11] SQL Server done (98/98 unit; clippy default, no-default and kerberos clean; 53 docker tests compile, CI only). Follow-ups: drop rust_decimal from tiberius features in the workspace; core DdlDialect needs the default-constraint drop and rename methods; deny.toml wrappers; conformance runs with unsupported_types true (most likely CI failure).
  • 2026-10-12: [14a] app on Session + core v2 with today's UI (0c6b6bb, 776ca75, 84c147a, a91ce63): build, test tablepro 329/329, lint, Meson 5/5 suites (1681 cases), l10n, data pass; app launches. Next: W6-13 connect dialog, W6-12 editor, W2-17 rows, tls_remedy.

Root cause

The app and every driver still run on the v1 contract (host/port ConnectOptions, v1 Connection, DatabaseService, russh tunnel). The v2 pieces were added beside it and nothing consumes them, so SSH ignores ssh_config and agents, nothing asks for a missing password, and the lossless/dialect work cannot reach the UI.

Refactor, not patch

The v1 model cannot express transports, editor sessions, write ledgers or exact transaction state without special cases; the plan makes Step 30 one atomic switch. On this branch v1 is replaced in place; the PR is squash-merged, so linux gets one commit. Each crate is verified on its own as it reaches v2.

Decisions (user, 2026-10-12)

  • Review amendments win over the plan text (lanes/*.md list the stale spec text).
  • W1-12 credential prompter is in this PR. Keyring write failure: save the entry without the secret and toast.
  • SQLite decodes by storage class. Session lives in crates/session. tokio::sync channels. tiberius 0.13 from crates.io.
  • ClickHouse: one transport-backed connector, lz4 off. ColumnKind/ColumnType/CatalogType to FB-C5/C6. Dialects move to the driver crates with a grammar accessor.

Order (each line is one or more commits; the crate's tests pass before the next starts)

  1. Dependencies: tiberius 0.13 crates.io (drop [patch], empty deny allow-git, command_timeout(None)); workspace deadpool 0.13.
  2. Core W3-05 reconciliation: kind_parts.rs, ColumnKind/ColumnType/CatalogType payloads, EditContext; fix the 20 matching files.
  3. Core W3-19 + W3-18 types: ForeignKeyMode/Check, WriteBatch.foreign_keys (keep table), TableDefinition family.
  4. Core W2-11: Connection v2 (stream_read, fetch_table_definition), DatabaseDriver v2 (capabilities, connect(options, cancel)), ConnectOptions v2 (ConnectTarget, NetworkTarget, Credentials, WorkspaceSettings, ConnectEnvironment without private_socket_dir), delete query.rs/read_only.rs/MAX_QUERY_ROWS; OwnedCalls and outcome tests.
  5. Core W3-06: SqlDialect::ddl() + grammar(), one push_value path, build_page/count/export, paging_mode, PageSpec.layout, plan_save, edit API, xmin row-id part.
  6. test-support v2: FakeConnection/Driver/EditorSession/Connector, conformance traits and cases (W9-07), TLS cases (W9-08), FaultTransport; fixture knobs (PG version, MySQL root/network, sshd start_in, PgBouncer pool, P-384/Ed25519 keys).
  7. SQLite (W2-14 + W3-07/08/09/18/19 SQLite parts): explicit open, create-or-replace, one workspace connection, progress-handler cancel, autocommit interim, sqlparser CreateIndex.
  8. PostgreSQL (W2-12): tokio-postgres 0.7.18 + postgres-types 0.2.14 + postgres-protocol 0.6.12 + deadpool-postgres 0.14.2 + tokio-postgres-rustls 0.14.0; codecs, cursor flight, status tracker + SAVEPOINT probe, no prepare, COPY FROM STDIN refused; delete sqlx-postgres.
  9. MySQL/MariaDB (W2-13): route-based (TCP or the OpenSSH socket), no relay, deadpool manager, KILL QUERY.
  10. ClickHouse (W2-16): query_raw with {pN:Type} params from the dialect, transport connector, read_until, clickhouse-types 0.1.3.
  11. SQL Server (W2-15): cancel_query + discard poisoned, CAST money, trust_cert_ca + hostname_in_certificate, Kerberos on spawn_blocking (CI only).
  12. SSH + credentials (W1-11, W1-12 core): SshServices in lib.rs, askpass to a CredentialPrompter, KeyringCredentials, LIBEXECDIR fallback to the binary's directory.
  13. Session (W4-06) in crates/session: link and lease, runner, connector, pending session, health, registry; then the app adoption and every call site (incl. ui/app/import.rs, ran_statements.rs).
  14. App surfaces: W6-11 ErrorCategory, W6-13 connect dialog as a gtk-rs subclass (+ W2-17 rows), W6-12 editor on EditorSession with a results list, W1-12 prompter dialog.
  15. Delete v1: database_service, connection_service, connection_monitor, russh_tunnel.rs, russh, sqlx-postgres, rust_decimal; deny.toml bans; CI docker matrix gains the session package.

Tests (fail before, pass after)

W9-07 conformance per engine, W9-08 TLS per engine, the specs' named unit tests per task; docker tests for PG/MySQL/ClickHouse/SSH locally, SQL Server in CI.

Also in scope

CSV import future on the GTK thread; LIBEXECDIR fallback. linux/CHANGELOG.md entries; docs/testing.md conformance sections; ADR numbering (W2-19's ADR takes 0016).

  • 2026-10-13: [14b]+[15] committed 5811951 (connect dialog), 2d4975f (editor), 89a9b2b (v1 deletes, CI session job, docs, CHANGELOG); disk filled during the Meson run, cargo clean freed 71 GiB; re-running verify.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/problem.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

problem.md

Group 1: Linux remediation plan Steps 30-39 (driver contract v2)

What happens

The Linux app still runs on the v1 driver contract: DatabaseService, the old Value, ConnectOptions with host and port, and an interim russh tunnel (tablepro_ssh::russh_tunnel). The v2 types, the OpenSSH transport and the askpass bridge were added beside it and nothing uses them. Visible results: SSH refuses agent and keyboard-interactive auth and ignores ~/.ssh/config; a saved connection without a stored password cannot connect and nothing asks for one; imported connections cannot be used.

What should happen

Steps 30-39 of linux/docs/plan-2026-09-14-audit-remediation.md (42 tasks, list below), with every amendment in linux/docs/dependency-review-2026-09-14.md "Plan amendments" applied where they disagree: no private forks; PostgreSQL on crates.io tokio-postgres + deadpool-postgres + tokio-postgres-rustls with a SQL-cursor row limit and per-statement transaction status; MySQL over the OpenSSH forward socket with no relay; tiberius on the upstream rev; ClickHouse query_raw and a Unix-socket connector; SSH only through the system OpenSSH client; Relm4 replaced by gtk-rs subclasses in every file these tasks rewrite.

Tasks

30: W1-11 W2-11 W2-12 W2-13 W2-14 W2-15 W2-16 W2-17 W3-05 W3-06 W3-07 W3-08 W3-09 W3-10 W3-11 W3-18 W3-19 W4-06 W6-11 W6-12 W6-13 W9-07 W9-08
31: W2-19 W4-07 W6-14 | 32: W3-17 W6-15 | 33: W4-08 W4-09 | 34: W4-10 | 35: W3-12 W5-01 W9-09
36: W3-13 W4-11 | 37: W4-12 | 38: W3-14 W5-02 | 39: W3-15 W5-03 W6-17

Environment

Ubuntu 26.04 arm64 (Asahi), GNOME 50 Wayland. Docker reachable; sample PostgreSQL, MySQL and ClickHouse in ~/tablepro-dev-db. SQL Server has no arm64 image: MSSQL docker tests run only in CI.
Base: origin/linux plus #3354 (Linux verify backend). One PR into linux, merged by us when green and reviewed.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/core.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

lanes/core.md

Lane: core (driver contract v2, value model, dialects, catalog, DDL)

Tasks: W2-11 W3-05 W3-06 W3-07 W3-08 W3-10 W3-11 W3-18 W3-19 (step 30), W2-19 (31), W3-17 (32), W3-12 (35), W3-13 (36), W3-14 (38), W3-15 (39).

Base inspected: worktree HEAD, which contains origin/linux (merge-base = origin/linux tip 06794b8; git diff origin/linux HEAD -- linux touches only CLAUDE.md, CONTRIBUTING.md, app/tests/startup.rs, docs/testing.md). Every path below is relative to linux/ unless absolute.

Labels: confirmed = I opened the file/line or crate source; inferred = my reading, not proven by a test or build.


0. Summary of what already landed (the additive v2 commits)

These commits put v2 types into crates/core beside v1, nothing deleted (confirmed by commit messages and file contents):

Commit What it added
1da0822 lossless value component types (value/*)
ad3d221 column/* (ColumnType, ColumnKind, ReadForm, SqlTypeExpr), sql_syntax/*, ddl/referential_action.rs, WideInteger on num-bigint
74672a7 statement/* (Statement, BoundParam, ParamType, StatementBuilder, RowGuard, GuardedStatement, SqlBuildError), column/column_info.rs, query_result.rs, foreign_key_info.rs, edit/cell_input.rs
84e2590 meta/* (TableRef, SchemaInfo, ObjectScope, ObjectInfo/ObjectKind, IndexInfo/IndexKey, RowIdentity, EngineRowId/Part, page types, TableEstimate), browse/{split_page,row_key_layout,browse_page,browse_sql_error}, dml/key_component, save/{change_set,change_origin,save_plan}, dialect/{bind_target,like_form,keyset_direction,placeholder,...}
b2c75ae edit/parse_literal_text.rs (parse_literal_text, parse_filter_text)
745d78a SqlDialect trait, dml/{build_insert,build_update,build_delete,predicate}, dialect/test_dialect.rs, dialect/engine_dialect.rs (EngineDialect keyed by driver id)
f4260f4 dialect-driven build_filter/operators_for in filter.rs
e861c99 Value switched: drivers return value::Value (lossless), QueryResult{columns: Arc<[ResultColumn]>}; old query::Value gone
14dd128 DriverError v2 + ErrorCategory + ServerDiagnostics (drivers classify)
f4a80e2 OwnedCalls, StopRequest/StopReason, EngineStop/CallResource/StopDelivery/StopScope, WriteLedger, SessionFate
7a4ee6e EditorSession + RunOptions + ResultSink/ResultEvent/RunSummary, DriverCapabilities, LinkWatch, UserSql, RowLimit, TransactionState, WriteBatch, WriteReport, CommitToken
92aea71 W3-17: export split into export/*, value_text at column precision, JSON raw number tokens

Commits after the plan that the plan does not know about, and that the switch must carry (confirmed): 9e00c03 column comments (ColumnInfo.comment, comment DDL in sql_ddl.rs), 3bcd8df StructureOp::RenameColumn (sql_ddl.rs:268, 1161, 1281), ec75581 SQL INSERT export (export/sql.rs, uses dialect_for), d91c70a CSV import (core/src/import/csv_import.rs, app/src/ui/app/import.rs on execute_in_transaction + dialect_for), e508f68 XLSX export.

v2 types present in core today (confirmed)

CallOptions (call_options.rs:11-28), DriverCapabilities + EndpointKind/ReadOnlyEnforcement/ScriptExecution/CommitResolutionSupport/EstimateQuality (capabilities.rs:8-90), DriverError v2 (error.rs:23-126, no FileExists, exhaustive category() 132-153), ErrorCategory (error_category.rs), ServerDiagnostics/ServerCode, ReadOnlyRefusal, LossPhase, TimeoutPhase, EditorSession/Id/State/Options (editor_session.rs:11-75), RunOptions, ResultSink/SinkFlow, ResultEvent incl. ColumnsResolved (result_event.rs:37-60), RunEnd/RunSummary/Truncation/ServerNotice, RowLimit (EDITOR_DEFAULT 10,000), UserSql, TransactionState, ServerInfo, LinkWatch/LinkState, OwnedCalls (owned_calls.rs:37 new, 47 run_read, 131 run_write, 209 shutdown), StopRequest/StopReason, EngineStop/CallResource/StopDelivery/StopScope (engine_stop.rs), WriteLedger/WriteAtomicity (write_ledger.rs:38-124), SessionFate, WriteBatch/WriteKind (write_batch.rs:10-25, with table, without foreign_keys), WriteReport/StatementOutcome, CommitToken/CommitResolution, meta::*, statement::*, column::*, value::*, browse::{split_page, RowKeyLayout, BrowsePage, BrowseSqlError}, dml::*, save::{ChangeSet, RowInsert, RowUpdate, ChangeOrigin, SavePlan}, dialect::*, edit::{CellInput, EditParseError, ReadOnlyReason, parse_literal_text, parse_filter_text}, plus W2-09's transport/TLS/endpoint types (NetworkEndpoint, FileEndpoint, TlsConfig, LivenessPolicy, Transport, TransportError, ...).

v2 types absent (confirmed by an empty workspace grep for each name)

Connection v2 trait, DatabaseDriver v2, ConnectTarget, NetworkTarget, Credentials enum, ConnectEnvironment, WorkspaceSettings, APPLICATION_NAME, stream_read/ReadStatement/StreamSummary, PageRequest, SortKey, PageSpec, build_page, build_count, paging_mode, build_export, plan_save, validate_raw_predicate, edit_text/parse_edit_text/empty_input_meaning/is_unchanged, EditContext, SqlDialect::ddl(), DdlDialect, TableDefinition/ColumnDefinition/ColumnAttributes, ForeignKeyMode/ForeignKeyCheck, fetch_table_definition, classify_default, check_runtime_version, WORKSPACE_SQL_MODE_STATEMENT, ConformanceEngine, OperationKind, current_dialect, literal_of (public), source_offsets, strip_comments.

v1 types the switch deletes, with every consumer (confirmed by grep over crates/)

v1 item Defined Consumers
Connection v1 (list_tables, fetch_rows, query, query_params, execute, execute_params, execute_in_transaction, ping, close(Box)) core/src/connection.rs:64-118 all 5 drivers' lib.rs; core read_only.rs; test-support fake_connection.rs, call_counters.rs, connection_script.rs; app services/connection_service.rs, connection_monitor.rs (ping), ui/app/{mod,browse,row_ops,structure,import,connection,workspace_tabs}.rs, ui/browse_tab.rs, ui/connect_dialog.rs, services/change_tracker.rs; driver tests (all 5 integration.rs, sqlite/tests/csv_import.rs)
ConnectOptions v1 (host, port, use_tls, service_endpoint, service_address, Default) connection.rs:25-62 5 drivers, app connection_service.rs, connect_dialog.rs; service_endpoint/service_address also in core transport.rs, net/src/direct_tcp.rs, ssh/src/forward.rs, mssql lib.rs
AuthMode connection.rs:15-23 stays (persisted, serde) but must move out of connection.rs; used by capabilities.rs, config_error.rs, storage connections.rs, app (7 files)
DatabaseDriver v1 (default_port, is_file_based, ddl_is_transactional, reports_rows_affected, supports_integrated_auth, connect -> Box) driver.rs:7-49 5 drivers; test-support fake_driver.rs; app connect_dialog.rs (default_port, is_file_based, supports_integrated_auth), connection_service.rs, ui/app/structure.rs:255-258 (ddl_is_transactional), ui/app/row_ops.rs:40-44 (reports_rows_affected)
TableInfo, ExecResult, v1 IndexInfo (columns: Vec), v1 ForeignKeyInfo (String actions) query.rs:3-41, re-exported at lib.rs:95 drivers; read_only.rs; sql_ddl.rs:18; app ui/app/{mod,structure,connection,workspace_tabs}.rs, structure_tab/{mod,indexes,fks}.rs, structure_tab_dialogs.rs, connect_dialog.rs, sidebar_row.rs; test-support
MAX_QUERY_ROWS lib.rs:99-101 5 drivers, clickhouse/tests/integration.rs
ReadOnlyConnection read_only.rs:11-170 app services/connection_service.rs
sql_dialect.rs (quote_ident(driver_id), placeholder_for, build_update, build_order_and_pagination, build_single_cell_update, build_full_row_update, build_insert_from_draft, v1 BuildSqlError) core/src/sql_dialect.rs:1-631 core engine_dialect.rs, sql_ddl.rs:19; clickhouse lib.rs:15 + tests; mssql lib.rs; app change_tracker.rs:35, ui/app/browse.rs:55-98,180, ui/error_text.rs:2
EngineDialect, dialect_for(driver_id), grammar_for(driver_id), parse_for_column (interim, not in plan) dialect/engine_dialect.rs:21-304 app ui/app/{browse,import,row_ops}.rs, ui/export_dialog.rs:382, ui/editor/mod.rs:675,697 (grammar_for); core export/sql.rs tests; sqlite/tests/csv_import.rs
classify_type_name, has_dynamic_storage column/classify.rs all 5 drivers' column_type_of; engine_dialect.rs:228; app change_tracker.rs, browse_tab.rs, grid.rs:2191 (tests)
normalize_pg_default / normalize_mssql_default / normalize_default_value postgres lib.rs:525, mssql lib.rs:631, sqlite lib.rs:521 own driver
sql_ddl.rs (driver_id builders, validate_safe_*, mssql_drop_default_constraint dynamic SQL at 347-360, supported_fk_actions, StructureOp, diff_to_ops, materialize_ops) core/src/sql_ddl.rs:1-2609 app services/structure_tracker.rs:23,55, ui/structure_tab/{mod,columns}.rs, structure_tab_dialogs.rs:24,197, ui/app/{mod,structure}.rs; pg/mysql/mssql tests/integration.rs
export::render_in_clause/InClause/in_clause_literal export/in_clause.rs:1-74 app ui/grid.rs:1624
export::value_to_text (kept as an internal of value_text; public callers go) export/value_text.rs drivers' bind fallbacks (pg 503, mysql 504, sqlite 501, mssql 475, clickhouse 758), sql_ddl.rs:169, engine_dialect.rs:127, app grid.rs:2131

1. Cross-cutting findings that decide the lane's shape

F1. ColumnKind / ColumnType / CatalogType diverge from FB-C5/C6 (confirmed; decision needed)

  • Landed ColumnKind is flat: Boolean, Integer(IntegerKind), Decimal, Float(FloatKind), Text(TextKind), Binary, Date, Time, Timestamp, Interval, Uuid, Json, Enumeration, Set, BitString, Network, Geometry, Array, Other (column/kind.rs:6-30), IntegerKind is a 10-variant enum (kind.rs:32-44).
  • FB-C5 (details.md:1852-1891) requires payloads: Bool(BoolStorage), IntegerKind { width, signed } with W24/W256, Decimal { precision, scale }, Money { scale }, Text { length }, Enum { labels, multiple }, Bytes { length }, Json { binary }, Time { fractional_digits, with_offset, range }, Timestamp { fractional_digits, zone }, BitString { length, varying }, Network(NetworkFamily), Xml, Spatial, Array { element }, Variant, Composite.
  • Landed ColumnType has private fields and catalog: CatalogType::{Unknown, Oid(u32), Named(SqlTypeExpr)} (column_type.rs:6-28); FB-C6 (details.md:1909-1917) has catalog: Option<CatalogType { declaration, cast_target }>.
  • Precision is recovered today by parsing the type name (fractional_digits, decimal_scale, type_arguments at column_type.rs:95-135).
  • Specs that cannot be met without the FB-C5/C6 payloads: W3-07 ServerText $n::<cast_target> and MissingCatalogType; W3-08 tests tinyint(1) -> Bool(Integer), money -> Money{Some(4)}, Spatial, enum labels in enumsortorder, int4range -> Other ServerText, bit(10) width; W3-10 DraftType::Catalog splices catalog.declaration, MissingCatalogDeclaration; W3-13 "CHARACTER SET only for textual kinds, SRID only for Spatial, ON UPDATE only for Timestamp"; W3-17 "ColumnKind::{Time,Timestamp}.fractional_digits"; edit ReadOnlyReason::{Spatial, Composite, Variant} already exists (edit_parse_error.rs:58-66) with no ColumnKind to produce them.
  • Recommendation: W3-05 brings ColumnKind/ColumnType/CatalogType to FB-C5/C6 inside the switch (196 ColumnKind:: references in 20 files, 40 .kind() calls, 22 CatalogType:: sites; confirmed by grep). Keeping kind() as an accessor is fine; catalog must carry declaration and cast_target.

F2. The dialect lives in core, keyed by driver id (confirmed; contradicts FB-D10 and W3-07)

  • EngineDialect { driver_id, grammar } with dialect_for(driver_id) falling back to PostgreSQL (dialect/engine_dialect.rs:21-58) implements all five engines through driver_id string matches and calls v1 sql_dialect::{quote_ident, placeholder_for, build_update, build_order_and_pagination} (engine_dialect.rs:4, 93, 107, 162, 194).
  • It is wrong against the normative C12 table in several places: ESCAPE '\\' everywhere (engine_dialect.rs:156, default at sql_dialect.rs:90) instead of !; ClickHouse gets bound ?-style placeholders instead of CAST(<literal> AS <decl>); no E''/_utf8mb4 X''/N'' literals; Bytes always Unrepresentable (engine_dialect.rs:126); MSSQL probe COUNT(*)/TOP 2 not COUNT_BIG(*) ... TOP (2) ... WITH (UPDLOCK, HOLDLOCK); no read_form projections; row-id projection ctid instead of "ctid"::text AS "__tablepro_row_id".
  • W3-07 must delete engine_dialect.rs, dialect_for and grammar_for, and the editor's grammar choice (app ui/editor/mod.rs:675-698) needs another source. Proposal (inferred): SqlDialect::grammar() -> SqlGrammar, or a grammar field in DriverCapabilities so the editor can lex before a connection exists.

F3. Binding paths are duplicated (confirmed)

  • StatementBuilder::push_value(&dyn PlaceholderStyle, value, ParamType) (statement/statement_builder.rs:40-53) is used only in tests (row_guard.rs:66-70; PlaceholderStyle implemented only in a test at statement_builder.rs:72).
  • dml and filter assemble SQL and params by hand with a first_ordinal (dml/predicate.rs:11-50, filter.rs:235-270).
  • W3-06 requires StatementBuilder { push_value(dialect, value, BindTarget) } and build_filter(&mut StatementBuilder, ...) -> Result<bool, _>. Unify on SqlDialect::placeholder (which can inline ClickHouse CASTs) and delete PlaceholderStyle. Ordinal: FB-C10 says the ordinal is the count already emitted; engine_dialect.rs:107 does ordinal.saturating_sub(1). Fix it once in the builder.

F4. The conflict list adds scope to W2-11 and W3-06 (confirmed: plan.md:1068-1069)

  • W2-11 adds Connection::stream_read with ReadStatement and StreamSummary (XW1).
  • W3-06 adds build_export, PageSpec.layout, browse::paging_mode, COUNT_BIG for SQL Server (XW3/13/15), and the core edit API edit_text, parse_edit_text, empty_input_meaning, is_unchanged with a non-#[non_exhaustive] EditParseError (XW9).
  • None of these exist; they are not in the per-task specs, so they are easy to miss.

F5. Review amendments that touch this lane

  • W3-07, W3-11: ClickHouse identifiers and values go through clickhouse::sql::Identifier/Query::param, or a drift test. Crate facts: the escape set is pub(crate) (clickhouse-0.15.2 src/sql/mod.rs:11 pub(crate) mod escape; escape.rs:19-29), but Identifier is public (sql/mod.rs:8, bind.rs:23-31) and Query::sql_display() is public (query.rs:45-48) and renders bound args immediately (sql/mod.rs:83-103). So an offline drift test is feasible: Client::default().query("?").bind(Identifier(name)).sql_display().to_string() == ClickhouseDialect.quote_identifier(name).
  • PostgreSQL goes through postgres_protocol::escape::{escape_identifier, escape_literal}. postgres-protocol 0.6.12: escape_literal emits E'...' (leading space) when the input has a backslash (docs.rs src escape/mod.rs:13-16, 62-65). This matches W3-07's E'' golden; the golden must expect the leading space. It is not in Cargo.lock; W2-12 adds it (review W3-09 amendment).
  • Use one core hex helper (exists: core/src/hex.rs encode_lower). ClickHouse hex_encode in its lib.rs goes.
  • W2-16, FC-D5: Client::query_raw replaces escape_bind_markers (clickhouse lib.rs:787). W3-07's "escape_bind_markers stays" is stale; delete it with bind_placeholders, literal and escape_str (lib.rs:612, 728, 777). clickhouse-types is 0.1.3 (lock and registry), not 0.1.2.
  • FB-D3, W3-03: WideInteger on num-bigint. Done (value/wide_integer.rs:4-10, 86-104: ASCII -?[0-9]+ check and 78-char cap before BigInt::from_str). The check is needed because num-bigint 0.4.8 accepts + and _ (biguint/convert.rs:213-235, bigint/convert.rs:29). The lock has num-bigint 0.4.8 (core) and 0.5.1 (russh). Per the review, 0.4 is correct once russh leaves under FC-D17.
  • SqlDecimal stays custom (value/decimal.rs, MAX_INTEGER_DIGITS 131,072 at :42).
  • FD-D19, W8-10: ban rust_decimal in deny.toml "after the switch". Not banned yet (deny.toml [bans] lines 38-50). Still declared in the workspace Cargo.toml:42, the sqlx feature (:38), the tiberius feature (:39), app/Cargo.toml:43 (no rust_decimal:: use in app src, so likely unused; inferred), postgres Cargo.toml:18 (to_pg_decimal lib.rs:791), and mysql/mssql dev-deps.
  • Query/DDL building: keep per-engine dialects; insta snapshot tests for golden SQL; avoid sea-query. insta is not in the lock. Latest is 1.49.0 (crates.io, 2026-10-03), which is under two weeks old, so take 1.48.0 (2026-06-11), the version the review cites.
  • W2-14 / mssql default: sqlparser MsSqlDialect parse_expr for SQL Server defaults (W3-08 classify_default).
  • FC-D2/FC-D6 (relay removed): ConnectEnvironment.private_socket_dir in W2-11 is stale. The OpenSSH forward socket belongs to tablepro-ssh (ADR 0013). Keep only client_version (inferred).
  • FC-D8 (per-engine trust): the trust shape in NetworkTarget { transport, credentials, database, tls, trust } must follow the per-engine trust decision (platform-verifier for PG/ClickHouse; trust_cert_ca for MSSQL; sqlx roots + CA for MySQL). The trust field is probably a TrustSource handle the drivers interpret per engine (inferred).
  • FC-D1/W2-12: "Editor columns text-typed until Canceling connection does not actually cancel #1358". ResultEvent::ColumnsResolved already exists, so no core change.
  • Relm4 replacement (FA-D1 amendment, problem.md): every app file these tasks rewrite moves to gtk-rs subclasses. In this lane that means the structure tab (StructureTab is a SimpleComponent, ui/structure_tab/mod.rs:495) for W3-10/W3-12/W3-14, and BrowseTab (SimpleComponent, browse_tab.rs:1342) plus grid for W3-11. W3-11 only edits call sites, so leave the widget migration to the app lane and the step that rewrites the widget (W3-12 for the structure tab).

F6. ADR numbering is stale (confirmed)

The plan assigns 0011 to W3-12 and 0013 to W2-19 (plan.md:1073). The repo already has 0013-openssh-client-transport.md and 0015-gnome-50-platform-floor.md (docs/decisions/README.md index). The details file also names 0010-driver-contract-v2.md (W2-19, details.md:8541) and 0010-ddl-plans-and-table-definitions.md (W3-12, details.md:9611). Free numbers: 0008, 0010, 0011, 0012, 0014. Proposal: W3-12 keeps 0011. W2-19 takes 0010 (W1-01's openssh-config ADR is gone under FC-D17, because 0013 covers it) or 0016. The owner should choose.

F7. META extension missing: PostgreSQL xmin (confirmed)

details.md:8591 requires PgDialect engine_row_id to project ctid, xmin and the DML key to add xmin = $n::xid. EngineRowIdPart has only PostgresTableOid, PostgresCtid, SqliteRowid (meta/row_identity.rs:34-38), and EngineRowId::parts() (:44-50) has no xmin part. W3-06/W3-07 must add EngineRowIdPart::PostgresXmin (bound as ServerText(xid)), or the owner must drop the requirement.


2. Per-task reports

W2-11 · Core driver contract v2 (Connection, DriverError, EditorSession, writes, metadata, OwnedCalls, WriteLedger)

Status: partial.

Done (confirmed):

  • DriverError v2 exactly per ERR minus FileExists, with an exhaustive category() (error.rs:23-153).
  • ErrorCategory, ServerDiagnostics, ReadOnlyRefusal, LossPhase, TimeoutPhase.
  • CallOptions with a single new (call_options.rs:16-18). It uses Option<tokio::time::Instant> where CONN says Option<Duration>. Instant is the better carrier for propagation; record the divergence.
  • DriverCapabilities with every enum (capabilities.rs), ServerInfo, LinkWatch (mark_lost, state, lost future: link_watch.rs:21-29).
  • StopReason/StopRequest, StopScope/StopDelivery/EngineStop/CallResource, OwnedCalls run_read/run_write/shutdown, WriteLedger with stop_permitted/interrupted/failed/guard_failed, SessionFate.
  • EDITOR types incl. ColumnsResolved; RowLimit::EDITOR_DEFAULT; UserSql.
  • WriteKind/WriteBatch{kind, table, steps}, GuardedStatement/RowGuard, WriteReport, CommitToken/CommitResolution/StatementOutcome (no serde).
  • All META types.
  • core deps tokio [rt, time] and tokio-util [rt] (core/Cargo.toml:25-26).

Remaining work, core:

  • crates/core/src/connection.rs: replace with the CONN trait. server_info, dialect() -> Arc<dyn SqlDialect>, read_only, link_state, lost() -> WaitForCancellationFutureOwned, probe, list_schemas, list_objects, fetch_columns(&TableRef), fetch_indexes, fetch_foreign_keys, row_identity, table_estimate, query_bound(&Statement, RowLimit), count_rows, write_batch, resolve_commit, open_editor_session, close(&self). Plus stream_read(ReadStatement, sink) -> StreamSummary (F4) and fetch_table_definition (W3-18). Move AuthMode to auth_mode.rs.
  • crates/core/src/driver.rs: DRIVER trait (capabilities() -> &'static DriverCapabilities, connect(options, cancel) -> Arc<dyn Connection>).
  • New: connect_options.rs, connect_target.rs, network_target.rs, credentials (add Credentials { Password { username, password: SecretString }, Integrated } beside the existing credentials/ prompt module), connect_environment.rs (client_version only, F5), workspace_settings.rs (read_only, statement_timeout, lock_timeout; workspace(read_only) = 60 s / 10 s), APPLICATION_NAME.
  • Optional, to follow the spec's file list: split editor_session_options.rs, result_event.rs/run_summary.rs, stop_delivery.rs, call_resource.rs and object_scope.rs out of the files where they now live. This is cosmetic; skip unless reviewers ask.
  • Delete crates/core/src/read_only.rs, crates/core/src/query.rs (TableInfo, v1 IndexInfo, v1 ForeignKeyInfo, ExecResult), MAX_QUERY_ROWS and the v1 re-exports at lib.rs:95-102. Re-export the v2 meta::IndexInfo and foreign_key_info::ForeignKeyInfo at the root.
  • Tests missing against the spec list: owned_calls has 8 tests (owned_calls.rs:430-664) under different names. dropping_caller_future_still_calls_stop, connection_scope_waits_for_stop_before_release, statement_scope_releases_immediately and in_band_delivery_cancels_stop_request_and_never_calls_stop have no obvious counterpart (inferred from the names and the InBand-only fixtures at 498/525/598/667). write_ledger::outcome_table (full matrix) is not present as a table test (write_ledger.rs:239-373 are case tests). error_category::every_variant_has_a_category, row_limit::zero_is_none (a near equivalent exists, row_limit.rs:31), object_kind::browsable_and_structure_editable_table (near equivalent in object_info.rs) and link_watch::mark_lost_resolves_lost_future (near equivalent link_watch.rs:48) need checking or renaming.

Remaining work, test-support:

  • crates/test-support/src/conformance.rs (new): ConformanceEngine + the 8 scenarios.
  • Rewrite fake_connection.rs, fake_driver.rs, call_counters.rs and connection_script.rs to v2. Add a FakeDialect (W3-07 / conflict list plan.md:1075).
  • crates/test-support/Cargo.toml.

Out of lane, but blocking the switch: every driver (W2-12..W2-16) and the app (W4-06, W6-*).

  • Dependencies: none new. tokio and tokio-util are already present (tokio 1.53.1, tokio-util 0.7.19 in Cargo.lock; drop_guard at tokio-util cancellation_token.rs:275, cancelled_owned at :267, TaskTracker at task/task_tracker.rs:158).
  • Tests the spec requires: the 11 owned_calls cases, write_ledger::outcome_table, guard_failed_after_applied_is_partially_applied (exists as a_guard_mismatch_after_a_statement_landed_says_so, write_ledger.rs:360), error_category::every_variant_has_a_category, row_limit::zero_is_none, object_kind::browsable_and_structure_editable_table, link_watch::mark_lost_resolves_lost_future, and the conformance scenarios (run per driver by W9-07).
  • Depends on: W2-09 and W2-10 (landed: 969b0a6, 70835e1), W3-05, W4-01 (landed efcf0f5). Every driver task and W4-06 consume it.
  • Stale or amended spec text: ConnectEnvironment.private_socket_dir (relay removed, F5); FileExists already absent; CallOptions.deadline type; "deadpool Object::take" applies only to deadpool-postgres and the custom mysql/mssql managers (FC-D18 amendment); conformance probe_never_waits_for_pool_slot holds for SQLite through the single workspace connection.
  • Size: core and test-support about 1,800-2,300 changed lines (Connection/Driver about 250; connect options about 300; stream_read about 150; deletions about 260; missing tests about 400; conformance module about 600; fakes about 350). Driver and app adoption are counted in other lanes.

W3-05 · Lossless value, column metadata and statement types (contract switch, core model)

Status: partial (mostly done).

Done (confirmed):

  • Value with all 22 variants and boxed Other/Undecodable (value/mod.rs:27-51), is_null, variant_name, as_exact_u64, no serde.
  • The size test asserts ≤ 32 bytes (mod.rs:103-105), stricter than the spec's 40.
  • ValueIdentity + same_value (value/identity.rs; NaN, -0 and offset test at :98).
  • ColumnInfo (with an extra comment), ResultColumn, QueryResult{Arc<[ResultColumn]>, rows, truncated} (query_result.rs:11-18).
  • ColumnDefault {None, Literal, Expression} (column/default.rs), ForeignKeyInfo with Option<ReferentialAction> (foreign_key_info.rs:13-22), CellInput.
  • Statement, BoundParam::new rejecting mismatches and Undecodable, ParamType::accepts with Null always accepted, GuardedStatement/RowGuard, SqlBuildError, BindError/LiteralError.
  • The export lossless rules (value_text.rs tests value_to_text_keeps_fraction_offset_and_full_scale, json json_non_finite_float_is_string).
  • rust_decimal is gone from core (no entry in core/Cargo.toml). The drivers already emit v2 Value (e861c99).

Remaining:

  • F1 reconciliation: bring column/kind.rs, column/column_type.rs (public fields or accessors, catalog: Option<CatalogType { declaration, cast_target }>) and column/read_form.rs to FB-C5/C6. Add kind_parts.rs (BoolStorage, IntegerWidth, FloatWidth, TextLength, TimeRange, TemporalZone, NetworkFamily). Update fractional_digits/decimal_scale to read the kind payloads. Then fix the 20 files that match on ColumnKind.
  • Decide on ColumnInfo.comment, a post-plan field (9e00c03, column_info.rs:20-23) that W3-18 duplicates as ColumnAttributes.comment. Proposal (inferred): move it into ColumnAttributes in W3-18 and keep ColumnInfo per FB-C8. Otherwise the comment DDL in sql_ddl.rs needs both.
  • EditContext { InsertDraft, UpdateRow } (W3-04 item, absent).
  • Delete the v1 query.rs types (shared with W2-11).
  • Remove rust_decimal from the workspace once W2-12, W2-13 and W2-15 drop it, and add the deny ban (F5).
  • test-support column, type and value builders (none exist).
  • Dependencies: remove rust_decimal 1 (workspace Cargo.toml:42, the sqlx and tiberius feature lists :38-39, app/Cargo.toml:43, postgres/mysql/mssql). num-bigint 0.4.8 stays.
  • Tests the spec requires: value_size_is_at_most_40_bytes (exists as ≤32), as_exact_u64_table (exists, mod.rs:108), the identity tests (exist, combined), bound_param_rejects_text_for_uuid_and_undecodable (exists), param_type_accepts_null_for_every_variant (exists), statement_builder_ordinals_are_continuous (exists), value_to_text_keeps_fraction_offset_and_full_scale (exists), json_non_finite_float_is_string (exists). New: ColumnKind helper table for the FB-C5 semantics (is_textual includes Xml/Other, accepts_empty_string includes Enum with a "" label).
  • Depends on: W3-03 and W3-04 (landed, but W3-04's ColumnKind shape is not FB-C5), W4-01.
  • Stale: "size ≤ 40" (32 holds); the "export adaptation" is done; render_in_clause moves out in W3-11.
  • Size: about 300 lines without F1. With F1, about 900-1,300 lines across core, the drivers' column_type_of and app grid/filter_strip.

W3-06 · SqlDialect trait, row-identity keys and dialect-driven core SQL builders

Status: partial.

Done (confirmed):

  • SqlDialect trait with capabilities, quote_identifier, qualify, projection, sort_expression, count_all, placeholder, literal, escape_like_text, like_predicate, row_window, row_value_after, engine_row_id, engine_row_id_column, update/delete/insert_default_row/key_probe, parse_type and parse_expression (dialect/sql_dialect.rs:12-113).
  • DialectCapabilities, BindTarget incl. EngineRowId, Placeholder, LikeForm/LikeCase, KeysetDirection, BindError, LiteralError.
  • TestDialect (#[cfg(test)], dialect/test_dialect.rs).
  • key_components (dml/key_component.rs:11-29, Unordered -> ReadOnlyRows); build_insert/build_update/build_delete/build_key_probe with IS NULL and UndecodableKey; BuildSqlError with all 8 variants (dml/build_sql_error.rs).
  • RowKeyLayout, BrowsePage, split_page with ShapeMismatch and None keys (browse/split_page.rs).
  • ChangeSet/RowUpdate/ChangeOrigin/SavePlan<B>.
  • build_filter with Parse/Bind/OperatorNotAllowed, operators_for(ColumnKind) (filter.rs:107-270); parse_literal_text/parse_filter_text.

Remaining:

  • SqlDialect::ddl() -> Option<&dyn DdlDialect> (returns None until W3-12; missing from sql_dialect.rs).
  • Grammar accessor (F2).
  • like_predicate default ESCAPE char (spec engines use !).
  • StatementBuilder::push_value(dialect, value, BindTarget); delete PlaceholderStyle (F3).
  • filter.rs -> filter/{mod,filter_set,build_filter,operators_for,build_filter_error}.rs. build_filter(&mut StatementBuilder, ...) -> Result<bool, _>. Delete the v1 leftovers (extra_is_blank filter.rs:102). Add validate_raw_predicate(dialect, text) (sqlparser parse_expr + EOF; details.md:8590) and call it from build_page.
  • browse/{page_request,sort_key,page_spec,build_page,build_count}.rs + paging_mode + build_export + PageSpec.layout (F4), keyset via row_value_after, ctid windows, OFFSET with identity tiebreak, COUNT_BIG for MSSQL. Change split_page to take &PageSpec (it takes visible_columns, &RowKeyLayout today, split_page.rs:11-15).
  • save/plan_save.rs: inserts, updates, deletes; guard table; WriteBatch{.., foreign_keys: Enforced} (needs W3-19). SavePlan is generic SavePlan<B> today; pin it to WriteBatch.
  • edit/{parse_edit_text, edit_text, empty_input_meaning, is_unchanged} + EditContext (F4/XW9). EditParseError must not be #[non_exhaustive]; it isn't marked today (edit_parse_error.rs:13).
  • Delete crates/core/src/sql_dialect.rs (631 lines) once W3-11 and the drivers stop using it.
  • Add EngineRowIdPart::PostgresXmin (F7) if the owner keeps it.
  • Dependencies: none (sqlparser 0.63.0 present).
  • Tests the spec requires that are not yet present: build_update_engine_row_id_uses_part_columns_and_targets (the closest is an_engine_row_id_needs_a_dialect_that_has_one), build_insert_all_default_uses_default_row_or_errors (only the default-row half exists), key_components_unordered_is_read_only, all 8 browse build tests, the 3 save tests, parse_filter_text_integer_bounds_i8_to_u64, parse_literal_text_numeric_scale_exceeded. Present: the filter tests (filter.rs:456-589), split_page_* (4 of 4), build_update_null_key..., build_update_undecodable_key..., build_insert_omits_default_cells_and_generated_columns.
  • Depends on: W2-11, W3-05, W3-19 (ForeignKeyMode).
  • Stale: the spec says filter::classify is deleted (it already is); dialect/mod.rs doc says "the dialect trait itself lands with the per-engine implementations" (mod.rs:1-4), which is stale.
  • Size: about 1,600-2,000 added and about 700 deleted (sql_dialect.rs plus v1 filter remnants).

W3-07 · Per-engine SqlDialect implementations

Status: not started (the interim core EngineDialect is a placeholder to delete, F2).

Remaining:

  • New: crates/drivers/postgres/src/dialect.rs + param_oid.rs; crates/drivers/{mysql,sqlite,mssql,clickhouse}/src/dialect.rs. Each is built from ServerInfo at physical connect (PostgresDialect{server_version_num}, MySqlDialect{version, flavour}, SqliteDialect{version}, MssqlDialect{major_version}, ClickhouseDialect{version}) per C12.
  • PG: quote_identifier = postgres_protocol::escape::escape_identifier; literal via escape_literal (F5).
  • ClickHouse: identifier escape with the offline drift test against Identifier + sql_display (F5).
  • Delete: core dialect/engine_dialect.rs (431 lines), dialect_for, grammar_for, parse_for_column; the per-driver quote_ident copies (pg lib.rs:512, mysql :513, sqlite :510, mssql :683, clickhouse via core quote_ident); clickhouse bind_placeholders (lib.rs:612), literal (:728), escape_str (:777), escape_bind_markers (:787, per the review, with query_raw in W2-16), hex_encode.
  • App: export_dialog.rs:382, row_ops.rs:131 and import.rs:195 take the dialect from the session or lease (with W3-11 and W4-06). editor/mod.rs:675-698 takes the grammar from the new accessor.
  • test-support FakeDialect.
  • Tests move to tests/integration/<topic>.rs (plan.md:1078); new sqlite/tests/dialect.rs.
  • Dependencies: postgres-protocol 0.6.12 (direct in the postgres driver; also added by W2-12/W3-09); clickhouse-types 0.1.3 direct in clickhouse (already in the lock transitively); dev insta 1.48.0 in each driver that keeps golden SQL (review).
  • Tests the spec requires: the per-engine golden tests (PG NULL->ParamType, enum/array/char/money casts, text-into-uuid BindError, $1::tid/$2::oid, E''; MySQL _utf8mb4 X'6162635c', FROM_UNIXTIME(1743465600.500000), row-id None/BindError; SQLite rowid ?; MSSQL NULL varbinary, [ escape; ClickHouse CAST DateTime64 and unhex; row_value_after per engine; literal round trip through each sqlparser dialect). Integration: like_escape_matches_literal_underscore_and_percent on 6 engines, insensitive_filter_matches_mixed_case (MySQL utf8mb4_bin, SQLite), clickhouse_ilike_is_case_insensitive. Add the ClickHouse identifier drift test.
  • Depends on: W2-12 (PG connection builds the dialect), W3-06, W8-10 (landed, c3af59a tiberius pin); F1 for cast_target and ServerText.
  • Stale or amended: "escape_bind_markers stays" -> delete (review W2-16); clickhouse-types "0.1.2" -> 0.1.3; PG escaping -> postgres_protocol::escape; ClickHouse escape -> crate Identifier or drift test; exact_row_counts for MSSQL false per C12 (EngineDialect has it true, engine_dialect.rs:88).
  • Size: about 2,500-3,000 changed lines (5 dialects with goldens about 1,800, integration about 350, deletions about 900).

W3-08 · Classify columns and defaults from each engine's structured catalog

Status: not started. The drivers already query pg_catalog, information_schema and system.columns (pg lib.rs:90-114, mysql lib.rs:99-146, clickhouse lib.rs:188, sqlite PRAGMA table_xinfo lib.rs:93), but classify through the string classifier classify_type_name (column_type_of at pg :766, mysql :662, sqlite :465, mssql :797, clickhouse :1100) and the normalize_*default helpers.

Remaining:

  • New catalog.rs + typed row structs per driver: pg_typmod.rs, pg_column_row.rs; mysql_column_row.rs, mysql_column_type.rs (the deterministic scanner), column_type_scan_error.rs; sqlite_column_row.rs, runtime_version.rs; mssql_column_row.rs; clickhouse_column_row.rs.
  • Shared classify_default per engine using sql_syntax (needs public literal_of, strip_comments and parse_single_expression, which W3-04 specified and only ValidatedExpression.literal provides now: parse_expression.rs:54-62).
  • Delete core column/classify.rs (classify_type_name, has_dynamic_storage), core sql_syntax/mysql_column_type.rs (it uses sqlparser for modifiers, contrary to W3-08's review fix "no sqlparser for modifiers"), the drivers' column_type_of and normalize_*default (pg :525-560, mssql :631-680, sqlite :521).
  • ClickHouse uses DataTypeNode::new (clickhouse-types-0.1.3 data_types.rs:118). Enum labels come back as HashMap<i16, String> (:85), so sort by value to keep declaration order (inferred requirement).
  • Dependencies: clickhouse-types 0.1.3 direct (review); postgres-types 0.2.14 for Type::from_oid (added by W2-12).
  • Tests: the PG, MySQL and other unit tests listed in the spec (16), plus integration tests on PG 13/18, MySQL 8.4/MariaDB 11.4, MSSQL, ClickHouse and SQLite (catalog.rs).
  • Depends on: W2-11, W2-12, W3-04, W3-05 (F1 payloads are mandatory here), W3-06.
  • Stale: "MySQL scanner in driver" vs the core mysql_column_type that landed. The "SQLite floor 3.37.0" stands (sqlx 0.9 already needs it per the plan's risk list).
  • Size: about 2,200-2,700 changed lines.

W3-18 · Table definitions with column attributes, inbound foreign keys and SQLite dependents

Status: not started. core/src/ddl/ holds only referential_action.rs (ddl/mod.rs:1-3). ForeignKeyInfo::references() exists (foreign_key_info.rs:24-30) as a client-side inbound test, but no driver lists inbound keys.

Remaining:

  • New core files: ddl/{table_definition, column_definition, column_attributes, collation_name, generated_column, identity_style, column_visibility, column_key_roles, primary_key_constraint, inbound_foreign_key, table_source, schema_object_source, unmodeled_attribute}.rs.
  • Connection::fetch_table_definition (the CONN amendment, landing with W2-11).
  • Per-engine readers: drivers/*/src/catalog/table_definition.rs, mysql/src/catalog/mysql_extra.rs, sqlite/src/catalog/dependents.rs. dependents.rs uses the sql_syntax token stream, so it needs a public tokenizer helper; source_offsets does not exist yet.
  • Execution in the 5 connection.rs files (W2 lanes); a scripted FakeConnection.
  • ClickHouse is_in_sorting_key/is_in_partition_key replace split_key_expression (clickhouse lib.rs:563, review).
  • ColumnInfo.comment disposition (W3-05).
  • Dependencies: none beyond sqlparser 0.63.0 (Tokenizer::new/with_unescape/tokenize_with_location at sqlparser-0.63.0 src/tokenizer.rs:894, 932, 944).
  • Tests: unit mysql_extra_parses_all_known_tokens_and_reports_unknown, sqlite_dependents_include_views_and_foreign_triggers_transitively, sqlite_dependents_ignore_name_inside_string_literal. Integration: fetch_table_definition_reports_pk_name_collation_comment (5 engines), ..._lists_inbound_foreign_keys (PG, MySQL, MSSQL, SQLite), MSSQL default_constraint_name_is_read, SQLite dependents_and_source_are_read.
  • Depends on: W2-11, W3-05, W3-08, W4-01.
  • Size: about 1,800-2,200 changed lines.

W3-19 · Foreign-key suspension in WriteBatch, SQLite write transactions, MySQL workspace sql_mode

Status: not started. WriteBatch has no foreign_keys (write_batch.rs:21-25). There is no BEGIN IMMEDIATE or foreign_key_check in the sqlite driver and no sql_mode statement in the mysql driver (greps empty).

Remaining:

  • New core foreign_key_mode.rs (ForeignKeyMode { Enforced, SuspendedThenVerified { checks } }) and foreign_key_check.rs. Add the field to WriteBatch and WriteBatch::new.
  • New sqlite/src/foreign_key_verification.rs (multiset diff, WITHOUT ROWID by count).
  • SQLite write_batch protocol (in W2-14's connection.rs).
  • New mysql/src/workspace_session.rs with WORKSPACE_SQL_MODE_STATEMENT, run on each new workspace pool connection (W2-13).
  • Unsupported { feature: "foreign key suspension" } in PG, MySQL, MSSQL and ClickHouse, and in the FakeConnection.
  • Tests sqlite/tests/write_batch_foreign_keys.rs and MySQL integration.
  • Dependencies: none (sqlx 0.9.0 begin_with, already locked: sqlx-core 0.9.0).
  • Tests: units introduced_violations_ignore_baseline_rows_across_rename, without_rowid_violations_compare_by_count; SQLite integration (4); non_sqlite_drivers_reject_suspension (×5); MySQL 8.4/MariaDB 11.4 workspace_session_strips_no_backslash_escapes.
  • Depends on: W2-11, W3-05. Blocks W3-06 (plan_save fills Enforced), W3-13 and W3-16.
  • Note: MySQL comment DDL already doubles backslashes as a defence (9e00c03). Once this sql_mode statement runs, that becomes deterministic rather than a guess.
  • Size: about 800-950 changed lines.

W3-10 · Structure DDL on table definitions, typed defaults, structural validation, static SQL Server default drop

Status: not started. sql_ddl.rs is still v1. It takes driver_id, imports query::{ForeignKeyInfo, IndexInfo} (sql_ddl.rs:17-19), keeps validate_safe_type/validate_safe_default/FORBIDDEN_CONTROL_CHARS/MAX_TYPE_LEN/MAX_DEFAULT_LEN (:63-115) and the dynamic DECLARE ... EXEC(...) default drop (:347-360). It has gained post-plan comment DDL (:338-440) and RenameColumn (:268).

Remaining:

  • crates/core/src/sql_ddl.rs: builders take (driver_id, &dyn SqlDialect) and quote through the dialect.
  • DraftColumn { original: Option<ColumnDefinition>, type_text: DraftType::{Catalog, Typed}, default: ColumnDefault }; InvalidType/InvalidDefault/Literal(LiteralError).
  • Static DROP CONSTRAINT [<name>] from attributes.default_constraint_name; sp_rename with N'' literals; FK actions &'static [ReferentialAction]; build_create_index on IndexInfo.keys and predicate.
  • Carry the post-plan comment DDL onto the dialect: MSSQL extended-property literals become N'' through dialect.literal.
  • App: ui/structure_tab/{mod,columns}.rs, ui/structure_tab_dialogs.rs, services/structure_tracker.rs, ui/app/structure.rs. Load TableDefinition via session.run(OperationKind::Metadata, ...). Default ComboRow (None / Value / SQL expression) + EntryRow. Validate Type through dialect.parse_type. Error class, subtitle and Property::Description. Take the dialect from Session::current_dialect().
  • Per-engine golden tests in each driver's src/dialect.rs.
  • Dependencies: none.
  • Tests: core typed_type_is_validated_structurally, catalog_declaration_is_not_reparsed; 8 driver goldens; MSSQL alter_default_on_column_with_named_constraint (CI only, no arm64 image); #[gtk::test] default_combo_produces_literal_and_expression, invalid_expression_marks_error_and_blocks_save.
  • Depends on: W3-07, W3-08, W3-18, W4-06 (Session::run, OperationKind, current_dialect: app lane).
  • Stale or amended: the line numbers (sql_ddl.rs:289-300 is now 347-360). Comment handling is not in the spec. The Relm4 question: StructureTab is a relm4 SimpleComponent (structure_tab/mod.rs:495). Recommend leaving the gtk-rs subclass conversion to W3-12, which rewrites the same files at step 35, rather than converting twice.
  • Size: about 1,300-1,700 changed lines (core about 700, app about 400, goldens about 400).

W3-11 · App SQL generation, row keys and copied SQL on dialects and column kinds

Status: partial (small).

Already done (confirmed): browse_tab.rs has no classify_type/TypeKind, and parse_input_for_column calls edit::parse_literal_text (browse_tab.rs:2679-2689). filter_strip.rs maps operators_for(kind) (filter_strip.rs:73-74). RowKey::Persisted already holds ValueIdentity (KeyValue = ValueIdentity, change_tracker.rs:73).

Remaining:

  • crates/core/src/sql_copy/{mod,in_clause,render_in_clause,render_insert}.rs (new). Delete export/in_clause.rs.
  • Reconcile the post-plan export/sql.rs render_sql_insert (unqualified dialect.quote_identifier(table) at sql.rs:25; writes /* omitted */ NULL on LiteralError) with sql_copy::render_insert(dialect, &TableRef, &[ColumnInfo], row). Share one renderer: qualified, generated columns skipped, the error policy per caller.
  • services/change_tracker.rs: change_set() -> ChangeSet replaces materialize (:524-694); delete the v1 imports (:35), RowKey::from_pk_values/KeyValue (:47-73); drafts as CellInput.
  • ui/row_object.rs: add key: RefCell<Option<RowKey>>.
  • ui/app/row_ops.rs: plan_save + lease.write_batch; delete reports_rows_affected (:40-44), compute_concurrency_warning (:156-220) and dialect_for (:131); add the RowGuardFailed messages.
  • ui/app/browse.rs: build_page + query_bound + split_page inside the op; delete fetch_rows/query_params/v1 quoting (:55-104, :167-191).
  • ui/grid.rs: delete is_*_type/classify_editor_kind/column_accepts_empty (:742-889); CellEditorKind from ColumnKind; delete pk_col_indices (:81) and the tracked_key pk scan (:126-131); new render_in_clause signature (:1624).
  • ui/browse_tab.rs: delete the pk scans (:368, :789, :919, :1203, :2105) and build_persisted_row_key (:2641).
  • ui/error_text.rs: switch from sql_dialect::BuildSqlError (:2-17) to the dml/filter/bind/literal errors.
  • ui/app/mod.rs: delete current_driver_id/driver_id() (:116, :617).
  • ui/app/workspace_tabs.rs.
  • Post-plan CSV import ui/app/import.rs:123-231: build a WriteBatch(RowChanges) of GuardedStatement::unguarded inserts through the lease dialect and write_batch, replacing execute_in_transaction + dialect_for. Not in any task spec; assign it here.
  • ui/export_dialog.rs:382.
  • Integration tests: PG, MySQL, sqlite/tests/row_identity.rs.
  • Dependencies: none.
  • Tests: the app units (change_tracker ×3, row_ops ×2, filter_strip ×2, grid ×1), core sql_copy ×3, driver goldens ×5, integration ×7 (spec list).
  • Depends on: W2-11, W3-06, W3-07, W3-09 (other lane), W3-18, W4-06 (app lane: Session, lease, OperationKind::Save).
  • Stale: all the app line numbers in the spec; parse_input_for_column and filter_strip are already done; CSV import and SQL export are not mentioned.
  • Size: about 2,000-2,500 changed lines.

W2-19 · Document driver contract v2, transports, trust, pooler requirements and per-engine behaviour (step 31)

Status: not started. docs/adding-drivers.md (239 lines) and docs/error-handling.md (131) describe v1. docs/drivers/ does not exist. No contract ADR exists.

Remaining:

  • Rewrite docs/adding-drivers.md and docs/error-handling.md; add a driver-layer section to ARCHITECTURE.md.
  • New docs/drivers/{postgresql,mysql,sqlite,sql-server,clickhouse}.md and the ADR (number per F6).
  • Content must follow the review, not the plan:
    • No forks.
    • tokio-postgres, postgres-types and postgres-protocol from crates.io; deadpool-postgres with RecyclingMethod::Verified.
    • Cursor row limit (DECLARE/FETCH, SAVEPOINT-wrapped) and per-statement transaction status with the SAVEPOINT probe (and its server-log ERROR line); COPY FROM STDIN refused; editor columns text-typed until rust-postgres#1358.
    • MySQL over the OpenSSH forward socket with no relay; tiberius on the tiberius-rs main pin with upstream cancel_query; ClickHouse query_raw and the Unix-socket connector.
    • SSH via system OpenSSH, with the Flatpak limits.
    • Per-engine trust sets (platform-verifier for PG/ClickHouse, trust_cert_ca for MSSQL, sqlx roots + CA for MySQL; freedesktop-sdk missing resize cursor #1905).
    • ARCHITECTURE "tablepro-net, Transport, relay, trust" loses "relay".
  • Update docs/decisions/README.md.
  • Dependencies: none.
  • Tests: none. lychee offline link check (review W9-04 amendment) where configured.
  • Depends on: W2-12..W2-17.
  • Stale: forks, relay, PgBouncer text that mentions the text portal; "ADR 0010 on the tokio-postgres move, the forks" becomes "no forks, upstream pins and removal gates" (review W9-18 also lists that ADR).
  • Size: about 900-1,300 lines of Markdown.

W3-17 · Export values at column precision with exact JSON numbers (step 32)

Status: done in substance (commit 92aea71).

  • export/{mod,csv_options,value_text,csv,tsv,json,json_field_names,markdown}.rs exist, plus post-plan html/xml/xlsx/sql.
  • value_text(&Value, &ColumnType) and cell_text, with fraction and scale from the column (value_text.rs tests).
  • JSON through serialize_map and RawValue::from_string (json.rs:2, 20, 64, 93); row_to_json (:119); RFC 3339 timestamps.
  • The workspace serde_json has raw_value (Cargo.toml:29).
  • App uses the new signatures: grid.rs:1563-1586 and :1657, browse_tab.rs:2367, export_dialog.rs:376.

Residual:

  • (a) value_text derives digits by parsing the type name (column_type.rs:95-135). After F1 it must read ColumnKind::{Time,Timestamp}.fractional_digits and Decimal{scale}.
  • (b) Public export::value_to_text still has non-export callers (grid.rs:2131 display text, drivers' bind fallbacks, sql_ddl.rs:169, engine_dialect.rs:127). Make it crate-private once W3-07, W3-09 and W3-10 remove them.
  • (c) Spec test names differ (json_emits_raw_number_tokens_for_u64_max_and_decimal_40_20 covers the same ground as a_number_wider_than_a_double_keeps_its_digits; csv_formula_rule_unchanged_after_split should be checked).
  • Dependencies: none.
  • Depends on: W3-11 (plan). In practice it landed before the switch, so only (a) and (b) follow.
  • Size: about 60-150 changed lines.

W3-12 · Generate DDL through per-engine DdlDialect plans (step 35)

Status: not started.

Remaining:

  • Delete core/src/sql_ddl.rs (2,609 lines incl. tests).
  • New core/src/ddl/{ddl_dialect,ddl_capabilities,column_capabilities,permission,refusal_reason,table_draft,draft_column,draft_type,draft_problem,table_changes,column_alteration,changed_attributes,diff_table,ddl_plan,drop_table_options,build_ddl_error,ddl_operation}.rs.
  • New drivers/*/src/ddl.rs (5), with the golden tests moved there from dialect.rs; SqlDialect::ddl() returns each.
  • App: services/structure_tracker.rs (PendingStructure), ui/structure_tab/{mod,columns,fks,indexes}.rs, structure_tab_dialogs.rs, ui/app/{structure,mod}.rs. Capabilities drive sensitivity and tooltips; New Table hides without create_table (app/mod.rs sidebar "+"); Show Create Table uses create_table_script. Delete driver_types/default_type_for/driver_can_* (columns.rs:19-115, fks.rs:11) and supported_fk_actions (dialogs :197).
  • This is where StructureTab should become a gtk-rs subclass with CompositeTemplate (review Relm4 amendment; F5).
  • Carry the post-plan comment and rename behaviour into TableChanges. W3-14 owns rename ordering, so W3-12 must at least not regress 3bcd8df's rename (renames first) and 9e00c03's COMMENT handling.
  • Docs: docs/adding-drivers.md DdlDialect section; ADR 0011 (free). W9-09's DDL conformance suite ships in the same commit.
  • Dependencies: none.
  • Tests: core ×3, per-engine goldens ×7 groups, #[gtk::test] ×3, structure tracker ×1 (spec list).
  • Depends on: W3-10, W3-11, W3-18, W4-06, W4-07, W4-10 (app lane, steps 31/34).
  • Stale: "NoChange" deletion and rename land in W3-14, but RenameColumn already exists in v1. Write TableChanges with renamed_columns from the start, or rename becomes a regression between steps 35 and 38 (inferred).
  • Size: about 3,200-4,000 changed lines plus about 2,600 deleted; plus about 1,500 if the Relm4 conversion of the structure tab folds in.

W3-13 · Preserve every column attribute when MySQL, PostgreSQL and SQL Server redefine a column (step 36)

Status: not started. The post-plan comment work already restates MySQL COMMENT in MODIFY (9e00c03 message; sql_ddl.rs:403-440). That is a partial precursor in v1 shape.

Remaining:

  • drivers/mysql/src/ddl.rs + ddl/mysql_column_definition.rs (render(original, draft): CHARACTER SET, COLLATE, GENERATED, NULL, DEFAULT, ON UPDATE, AUTO_INCREMENT, INVISIBLE, SRID, COMMENT with ' and \ doubled).
  • Unmodeled attributes or an odd collation name -> Refused(UnmodeledAttribute).
  • drivers/postgres/src/ddl.rs: TYPE ... COLLATE ... USING.
  • drivers/mssql/src/ddl.rs: restate COLLATE, NULL and SPARSE.
  • Integration tests in the three drivers' tests/integration/.
  • Dependencies: none.
  • Tests: 5 units, 5 integration (spec list). The MSSQL ones run only in CI.
  • Depends on: W3-12, W3-18, W3-19 (sql_mode), and F1 kinds for textual, Spatial and Timestamp gating.
  • Size: about 600-800 changed lines.

W3-14 · Rename columns, change primary keys and toggle auto-increment (step 38)

Status: partial (rename only, v1 shape). StructureOp::RenameColumn with its own phase before alters (sql_ddl.rs:268, 1161, 1281; commit 3bcd8df). There is no rename ordering for swaps, no PK change and no auto-increment toggle.

Remaining:

  • New core/src/ddl/{column_rename,order_renames,primary_key_change}.rs; edit table_changes.rs, diff_table.rs, changed_attributes.rs.
  • The 5 ddl.rs files: PG RENAME first, PK change, identity on with setval(pg_get_serial_sequence(...)), identity off; MySQL RENAME/CHANGE, PK; MSSQL sp_rename N'[s].[t].[c]' with ] doubled, PK order; SQLite native RENAME; ClickHouse RENAME for non-key columns.
  • Delete NoChange.
  • App: ui/structure_tab/{columns,mod}.rs (Name entry editable by capability; PK on forces NOT NULL).
  • sqlite/tests/ddl.rs (new) + integration tests.
  • Dependencies: dev proptest 1.11 already in the workspace (Cargo.toml:49) for non_empty_changes_never_plan_empty.
  • Tests: core ×6, goldens ×5, integration ×6+, #[gtk::test] ×1 (spec list).
  • Depends on: W3-13.
  • Size: about 1,000-1,300 changed lines.

W3-15 · Drop and re-add SQL Server default constraints around column type changes (step 39)

Status: not started. The dynamic DECLARE/EXEC drop still exists (sql_ddl.rs:347-360). W3-10 makes it static and W3-15 adds the re-add ordering.

Remaining:

  • crates/drivers/mssql/src/ddl.rs: per-column drop, ALTER, re-add (ADD CONSTRAINT [name] DEFAULT (...) FOR [c], or unnamed when there was no name); global ordering after FK/PK drops and before PK/FK adds.
  • crates/drivers/mssql/tests/integration/....
  • Dependencies: none.
  • Tests: units ×5, SQL Server 2022 integration ×3. CI only: no arm64 image (problem.md).
  • Depends on: W3-14.
  • Size: about 250-350 changed lines.

3. Implementation order (this lane)

Inside the step-30 stacked branch (one squashed refactor(linux)! commit at the end):

  1. W3-05 reconciliation (F1): ColumnKind, ColumnType and CatalogType to FB-C5/C6; EditContext; decide ColumnInfo.comment. Everything below matches on these.
  2. W3-19 core types (ForeignKeyMode, ForeignKeyCheck, WriteBatch.foreign_keys). Tiny, and W3-06's plan_save needs it.
  3. W2-11 core contract: Connection v2 (incl. stream_read, fetch_table_definition signature), DatabaseDriver v2, ConnectOptions v2 family, AuthMode move, v1 deletions, missing OwnedCalls and ledger tests; test-support fakes, FakeDialect stub and the conformance module.
  4. W3-06: ddl() and grammar accessors, StatementBuilder unification (F3), filter split + validate_raw_predicate, browse build_page/build_count/paging_mode/build_export/PageSpec.layout, plan_save, edit API (XW9), xmin part (F7). Delete sql_dialect.rs at the end of the branch.
  5. W3-18 core model (TableDefinition and friends), so the drivers can implement fetch_table_definition while they are rewritten.
  6. W3-07 dialects, W3-08 catalogs, W3-18 readers, W3-19 driver protocol: one driver at a time, alongside that driver's W2-1x rewrite (PostgreSQL first, because W3-07 and W3-08 depend on W2-12; then SQLite, MySQL, ClickHouse, SQL Server). Delete engine_dialect.rs and classify.rs when the last driver switches.
  7. W3-11 then W3-10 on top of W4-06's Session, lease and current_dialect.
  8. Tip gates: clippy -D warnings, the unit and gtk suites, the docker matrix (MSSQL in CI), conformance.

After the switch:

  1. W2-19 (step 31): docs and ADR.
  2. W3-17 residual (step 32): kind-based precision; value_to_text made private.
  3. W3-12 (step 35, with W9-09; StructureTab to a gtk-rs subclass here).
  4. W3-13 (step 36). 13. W3-14 (step 38). 14. W3-15 (step 39).

4. Open questions and risks that could block it

  1. FB-C5/C6 vs the landed ColumnKind/ColumnType (F1). Without a decision, W3-07, W3-08, W3-10, W3-13 and W3-17 cannot meet their specs. If the owner keeps the flat shape, those specs must be rewritten (no cast_target means no $n::<cast> for ServerText, and no Money, Spatial or Bool(Integer)).
  2. Dialect location and grammar source (F2). Moving the dialects into the driver crates means the editor needs the grammar before a connection exists. That needs a DriverCapabilities field or a SqlDialect::grammar(), plus a fallback for the picker.
  3. Hidden scope from the conflict list (F4): stream_read, build_export, PageSpec.layout, paging_mode, COUNT_BIG and the four edit functions sit in no task spec body, and the W5 lane (BrowsePager, export streaming) depends on them.
  4. Post-plan features on the v1 API: CSV import, SQL INSERT export, XLSX, column comments and RenameColumn. Their behaviour must survive the switch, and no task lists their files. Assigned here to W3-11 (import, SQL export), W3-05/W3-18 (comment) and W3-12/W3-14 (rename).
  5. xmin in EngineRowId (F7): required by details.md:8591 and absent from the code and from W3-06's own spec. The owner should confirm it.
  6. ADR numbers (F6): 0013 is taken. Assign W2-19's ADR before step 31.
  7. Relm4: the structure tab and browse tab are Relm4 components. Folding the gtk-rs migration into W3-10 and W3-11 would double the step-30 diff. Proposal: the structure tab converts in W3-12 (step 35), and browse/grid stay with the app lane's own rewrite steps. Coordinate with the app lane.
  8. Cross-lane gates: W3-10 and W3-11 need W4-06 (Session::run, OperationKind, current_dialect, leases). W3-07 and W3-08 need the W2-12 PostgreSQL driver skeleton (connect_physical builds the dialect from server_version_num). W3-12 needs W4-07 and W4-10.
  9. Testing constraints: SQL Server has no arm64 image, so the W3-07, W3-08, W3-10, W3-13, W3-14 and W3-15 MSSQL integration tests run only in CI. Golden SQL via insta adds a new dev-dependency (1.48.0, because 1.49.0 is under two weeks old).
  10. rust_decimal removal needs W2-12 (pg to_pg_decimal), W2-13 and W2-15 to finish first, and then the deny.toml ban (W8 lane).
  11. One atomic commit: this lane alone is about 15,000-19,000 changed lines inside step 30 (W2-11 about 2.1k, W3-05 about 1.1k, W3-06 about 2.5k, W3-07 about 2.8k, W3-08 about 2.5k, W3-18 about 2.0k, W3-19 about 0.9k, W3-10 about 1.5k, W3-11 about 2.3k). Bisect and review risk is high; the stacked-branch mitigation (plan.md:1084) is mandatory.

5. New and removed dependencies (this lane)

Change Crate Version Where Source
add (driver) postgres-protocol 0.6.12 drivers/postgres (shared with W2-12/W3-09) docs.rs postgres-protocol 0.6.12 escape module
add direct clickhouse-types 0.1.3 drivers/clickhouse Cargo.lock; registry clickhouse-types-0.1.3 data_types.rs:34,118
add dev insta 1.48.0 each driver with golden SQL (and core if desired) crates.io API: 1.48.0 2026-06-11, 1.49.0 2026-10-03
keep num-bigint 0.4.8 core Cargo.lock; num-bigint-0.4.8
keep sqlparser 0.63.0 core Cargo.lock
keep tokio 1.53.1, tokio-util 0.7.19 core Cargo.lock
remove rust_decimal 1 workspace, sqlx/tiberius features, app, drivers workspace Cargo.toml:38-42
remove (code) core sql_dialect.rs, sql_ddl.rs, read_only.rs, query.rs, dialect/engine_dialect.rs, column/classify.rs, export/in_clause.rs, sql_syntax/mysql_column_type.rs

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/postgres.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

lanes/postgres.md

Lane: postgres

Tasks: W2-12, W3-09 (PostgreSQL part), W9-07 (PostgreSQL side), W9-08 (PostgreSQL side).
Tree read: chore/linux-fix-issue-profile at 737f5e2. It equals origin/linux (06794b8) plus 8 Linux fix-issue profile commits that do not touch linux/crates (git diff --stat HEAD origin/linux -- linux shows only crates/app/tests/startup.rs and docs/testing.md).
Labels: confirmed means I opened the file, line or URL. inferred means it is reasoning, or a claim I took from the review without re-opening its source.

1. Where the PostgreSQL driver stands today

  • The crate is still the v1 sqlx driver. crates/drivers/postgres/Cargo.toml:18 has rust_decimal and :21 has sqlx = { features = ["postgres", "bit-vec", "tls-rustls-aws-lc-rs"] }. Its only source file is src/lib.rs (1060 lines). (confirmed)
  • It connects by host and port. lib.rs:41-61 builds PgConnectOptions::new().host(&opts.host).port(opts.port), a PgPoolOptions with max_connections(4) and ssl_mode Require or Disable from use_tls. No Transport is involved. (confirmed)
  • It implements the v1 Connection trait (lib.rs:68-361): list_tables, fetch_rows (format! SQL at 182-186), query, query_params, execute, execute_params, execute_in_transaction, ping. In core, connection.rs:25-37 (v1 ConnectOptions with host and port), connection.rs:64-118 (v1 Connection) and driver.rs:7-48 (v1 DatabaseDriver::connect(ConnectOptions) -> Box<dyn Connection>) are unchanged. (confirmed)
  • Parts of the value model already landed on the sqlx path through commits e861c99 and 14dd128. (confirmed) They are:
    • binary NUMERIC to exact text (lib.rs:856-911), with unit tests at 937-1033;
    • JSON and JSONB raw bytes with the version byte (801-823);
    • bit strings (932-935);
    • float4 kept as Float32 (401);
    • Undecodable instead of Null for unknown types (450-460, 777-784);
    • SQLSTATE mapping for 28P01/28000, 25006, 57014, 55P03 and 53300 (624-637), with full ServerDiagnostics (639-656);
    • TLS errors through tablepro_net::tls::classify (666-670).
  • Gaps against the target value model:
    • NUMERIC NaN and ±Infinity become Value::Float64 (921-928), although SqlDecimal::nan() and infinity() exist (core/src/value/decimal.rs:45-55). (confirmed)
    • date and timestamp decode through sqlx chrono, so only Temporal::Finite exists and there are no infinity sentinels (406-430). (confirmed)
    • time 24:00:00 goes through NaiveTime (409-411). (confirmed)
    • Binding uses sqlx plus rust_decimal (466-510, 791-794). (confirmed)
  • Tests:
    • tests/integration.rs (341 lines, sqlx-era v1 API).
    • tests/tls.rs uses sqlx directly (:5, :31-40) and must go when sqlx leaves the crate.
    • tests/tls_provider.rs is a rustls provider check.
    • tests/smoke_local.rs was already deleted by 869df6f. (confirmed)
  • Nothing from the target stack is in linux/Cargo.lock or in _build/cargo-home/registry/src: tokio-postgres, postgres-types, postgres-protocol, deadpool, deadpool-postgres and tokio-postgres-rustls are all absent. sqlx-postgres 0.9.0 is in the lock. (confirmed by grep of Cargo.lock and the registry listing)
  • The workspace still declares rust_decimal and the sqlx rust_decimal feature (linux/Cargo.toml:38,42). (confirmed)

Supporting pieces that already exist and that the new driver consumes (all confirmed)

Piece Where
Transport trait core/src/transport.rs:14-26: open() -> Box<dyn TransportStream>, explain_closed_stream()
TransportStream core/src/transport.rs:9: AsyncRead + AsyncWrite + Send + Unpin + 'static
Direct TCP transport net/src/direct_tcp.rs:40-59, sequential lookup plus dial; net/src/socket_options.rs:7-15: nodelay, keepalive, TCP_USER_TIMEOUT
OpenSSH forward transport ssh/src/forward.rs:88-122: TransportRoute::UnixSocket, open() = UnixStream::connect(socket) under connect_timeout
TLS client config net/src/tls/client_config.rs:8-38: build_client_config(&TlsConfig) -> Option<Arc<ClientConfig>>, platform verifier with extra roots, Require = UnverifiedServerCertVerifier, client identity
TLS classification net/src/tls/classify.rs:5-36
OwnedCalls::run_read and run_write core/src/owned_calls.rs:47,131; release and discard logic at 307-322; abandon flag set at 298
EngineStop and CallResource core/src/engine_stop.rs:33,50
WriteLedger core/src/write_ledger.rs
EditorSession trait core/src/editor_session.rs:55-75
ResultEvent, including ColumnsResolved core/src/result_event.rs:37-60
DriverCapabilities core/src/capabilities.rs:60-73
CallOptions core/src/call_options.rs:12,17; the deadline is a tokio::time::Instant, not the plan's Duration
ServerInfo core/src/server_info.rs:7-12: {product, version, major, minor}, not FC-C8's version_num/current_database/default_schema
Statement, BoundParam, ParamType core/src/statement/*; ParamType has no Bits and no OID (param_type.rs:10-35)
Generic dialect core/src/dialect/engine_dialect.rs: dialect_for("postgres"), ctid and tableoid row ids; there is no PgDialect yet
Script planner core/src/sql_syntax/script/*: ScriptPlan::build(text, SqlGrammar::PostgreSql, LexicalSettings { backslash_escapes }) (script_plan.rs:23, lexical_settings.rs:5-14), with corpus tests for dollar quotes, a$b$, E'' strings, standard_conforming_strings off, nested comments, BEGIN ATOMIC and CASE…END (core/tests/script_corpus.rs:31-147)
PostgreSQL fixtures PostgresFixture (postgres:17-alpine, fixture_image.rs), HbaMode::{Password, HostSslOnly, ClientCertificate} (hba_mode.rs:2-26)
PgBouncer fixture PgBouncerFixture (edoburu/pgbouncer v1.25.2-p0, max_prepared_statements = 0, ignore_startup_parameters = extra_float_digits,options; pgbouncer_fixture.rs:52-77)
Test PKI TestPki, RSA-SHA256 only (test_pki.rs:26,33); PkiFile::UnrelatedCa exists (pki_file.rs:4)
CI .github/workflows/build-linux.yml:157-200 runs cargo nextest run -p tablepro-driver-postgres --run-ignored only per package, so new tests/integration/* modules need no workflow edit; linux/.config/nextest.toml:2,10 serialises the package

Missing and needed: core ConnectOptions v2, ConnectTarget and NetworkTarget (grep finds none in core); CONN v2 Connection; FaultTransport (not in crates/test-support/src); the W9-07 conformance module; xmin in EngineRowIdPart (core/src/meta/row_identity.rs:24-38 has only tableoid, ctid and rowid). (confirmed)

2. Crate facts, checked against crates.io and docs.rs

Crate Version (exists, not yanked) Source
tokio-postgres 0.7.18 (2026-06-12, rust_version 1.85), newest crates.io/api/v1/crates/tokio-postgres/versions
postgres-types 0.2.14 (2026-06-12), newest crates.io/api/v1/crates/postgres-types/versions
postgres-protocol 0.6.12 (2026-06-12), newest crates.io/api/v1/crates/postgres-protocol/versions
deadpool-postgres 0.14.2 (2026-08-26, rust_version 1.85), newest crates.io/api/v1/crates/deadpool-postgres/versions
deadpool 0.13.1 (2026-08-26), newest; 0.12.4 is yanked crates.io/api/v1/crates/deadpool/versions
tokio-postgres-rustls 0.14.0 (2026-05-21), newest crates.io/api/v1/crates/tokio-postgres-rustls/versions

API facts (all confirmed at the URL given):

  • Config::connect_raw<S, T>(&self, stream: S, tls: T) -> Result<(Client, Connection<S, T::Stream>), Error>, where S: AsyncRead + AsyncWrite + Unpin and T: TlsConnect<S>. Source: docs.rs tokio_postgres/config/struct.Config.html.
    • The doc says every setting other than user, password, dbname, options and application_name is ignored.
    • The source still reads config.ssl_mode and ssl_negotiation (connect_raw.rs:95-96, 119-120) and config.channel_binding (:219, :274, :291-292), so setting them on Config works.
    • BackendKeyData is kept (:349-352), and the startup ReadyForQuery status is dropped (:362).
  • Client (docs.rs struct.Client.html) has simple_query_raw, batch_execute, query_typed_raw, execute_typed, cancel_token, is_closed, check_connection and prepare. It has no transaction-status accessor.
  • SimpleColumn exposes only name(), and its new is pub(crate) (simple_query.rs:18-31).
  • In SimpleQueryStream, ReadyForQuery(_) => None drops the status. CopyIn, CopyOut and CopyData fall through to Err(unexpected_message) (simple_query.rs:85-112).
  • SimpleQueryMessage has Row, CommandComplete and RowDescription and is #[non_exhaustive] (docs.rs enum.SimpleQueryMessage.html).
  • Responses::poll_next turns an ErrorResponse into Err(Error::db). The stream keeps going to ReadyForQuery (client.rs:41-60).
  • cancel_token() copies ssl_mode, ssl_negotiation, process_id and secret_key (client.rs:721-730). CancelToken::cancel_query_raw<S, T>(&self, stream: S, tls: T) is Clone, Send and Sync (docs.rs struct.CancelToken.html).
  • query_typed sends Parse("") Bind("") Describe Execute(0) Sync as one request (query.rs:75-86). It calls get_type for every column (:102).
  • get_type returns early for Type::from_oid built-ins. For anything else it prepares a typeinfo statement named s{n} from a process-wide counter (prepare.rs:59,66,135-144,195-210). This is the PgBouncer hazard behind the read_form rule.
  • Connection::parameter (connection.rs:296-299) and poll_message (:320-338) exist.
    • Notices are AsyncMessage::Notice (:112-114).
    • ParameterStatus updates parameters (:124-129).
    • The Future impl only logs notices through log (:348-355).
  • tokio_postgres::Error has no public constructor and no From impls. Its methods are as_db_error, code, is_closed and into_source (docs.rs error/struct.Error.html).
  • ToSql::encode_format(&self, &Type) -> Format is a provided method, so text-format parameters are possible (docs.rs postgres-types trait.ToSql.html).
  • postgres_types::Date<T> and Timestamp<T> are {PosInfinity, NegInfinity, Value(T)} (docs.rs enum.Date.html).
  • In postgres-types chrono_04.rs, NaiveDateTime and NaiveDate use checked_add_signed (lines 21, 119). NaiveTime uses + (line 142), so 24:00:00 wraps to midnight. Source: raw.githubusercontent.com rust-postgres tag postgres-types-v0.2.14.
  • postgres_protocol::types has array, range, inet, macaddr, varbit, point, box, path, hstore, ltree, lquery, ltxtquery, lsn, date, time, timestamp, uuid and scalar *_from_sql/*_to_sql. It has no numeric and no interval (docs.rs postgres_protocol/types/index.html).
  • deadpool-postgres 0.14.2 (docs.rs src/deadpool_postgres/lib.rs.html, config.rs.html, trait.Connect.html):
    • pub trait Connect: Sync + Send { fn connect(&self, &PgConfig) -> Pin<Box<dyn Future<Output = Result<(PgClient, JoinHandle<()>), Error>> + Send + '_>> }.
    • Manager::from_connect(pg_config, impl Connect + 'static, ManagerConfig) (lib.rs:118-127).
    • recycle checks is_closed(), then runs simple_query(recycling_method.query()) (lib.rs:155-170). It cannot see the transaction status.
    • RecyclingMethod::{Fast (default), Verified => "", Clean => DISCARD…, Custom(String)} (config.rs:308-373).
    • ClientWrapper derefs to Client. Its Drop aborts the connection task (lib.rs:231-313), so a taken object closes without sending Terminate.
    • Normal dependencies: deadpool ^0.13.1 [managed] and tokio-postgres ^0.7.14 with default features on non-wasm (crates.io /deadpool-postgres/0.14.2/dependencies).
  • deadpool 0.13.1:
    • Object::take(this) -> M::Type (docs.rs managed/struct.Object.html).
    • Pool::timeout_get(&Timeouts) (managed/struct.Pool.html).
    • PoolError::{Timeout(TimeoutType), Backend(E), Closed, NoRuntimeSpecified, PostCreateHook} (managed/enum.PoolError.html).
    • PoolBuilder has max_size, wait_timeout, create_timeout, recycle_timeout, runtime, pre_recycle, post_recycle and post_create (managed/struct.PoolBuilder.html).
    • deadpool-postgres re-exports Pool, PoolError, Object, Timeouts, Runtime and Hook as aliases (docs.rs index), so no direct deadpool dependency is needed. That last point is inferred.
  • tokio-postgres-rustls 0.14.0 (docs.rs src/tokio_postgres_rustls/lib.rs.html):
    • MakeRustlsConnect::new(config: ClientConfig) takes the config by value, not an Arc (L199).
    • MakeTlsConnect<S> requires S: AsyncRead + AsyncWrite + Unpin + Send + 'static (L248-261).
    • RustlsConnect sits in a private module. It is reachable only as <MakeRustlsConnect as MakeTlsConnect<S>>::TlsConnect through make_tls_connect(&mut self, hostname) (L54-77, L256).
    • Channel binding maps SHA-1, SHA-256 and ECDSA-SHA256 to SHA-256, SHA-384 to SHA-384, SHA-512 to SHA-512, and everything else (Ed25519) to none (L109-142). md5WithRSA, ecdsa-with-SHA1 and ecdsa-with-SHA512 are missing, as the review says.
    • Dependencies: rustls ^0.23 with no default features, sha2 ^0.11, x509-cert ^0.2, tokio-rustls ^0.26, tokio-postgres ^0.7 [runtime] (crates.io /tokio-postgres-rustls/0.14.0/dependencies).
  • tokio-postgres 0.7.18 has 18 normal dependencies, including socket2 0.6 [all], tokio-util 0.7 [codec], rand 0.10 and whoami 2. Features: default = runtime; array-impls, with-chrono-0_4, with-uuid-1 and with-serde_json-1 forward to postgres-types (crates.io /0.7.18/dependencies, docs.rs crate/tokio-postgres/0.7.18/features). Nothing there pulls ring or OpenSSL (inferred from the list).

3. How the driver takes a transport stream instead of a host and port

  • Every physical connection is let stream = target.transport.open().await? (Box<dyn TransportStream>, core/src/transport.rs:21), followed by config.connect_raw(stream, tls). This holds for workspace pool members, editor sessions, the standalone probe, stream_read and every CancelRequest.
    • The stream is TCP from DirectTcpTransport, already tuned with nodelay, keepalive and TCP_USER_TIMEOUT by net/src/socket_options.rs:7-15.
    • Or it is the OpenSSH forward's UnixStream from SshForwardTransport::open (ssh/src/forward.rs:103-122).
    • Box<dyn TransportStream> meets MakeTlsConnect's Unpin + Send + 'static bound, because TransportStream requires those (confirmed, transport.rs:9).
  • tokio-postgres Config::host, port, connect_timeout, keepalives and tcp_user_timeout are never set. connect_timeout and socket options belong to the transport. login_timeout wraps connect_raw (the transport open() already has connect_timeout).
  • The TLS server name is NetworkTarget::tls_server_name() (tls.server_name, else transport.service_endpoint().host()). For an SSH Unix-socket forward this is the database host, not the socket path. It is passed to make_tls_connect(hostname). (inferred from FC-C1 and the tokio-postgres-rustls API)
  • Cancel must use CancelToken::cancel_query_raw(transport.open().await?, fresh_tls_connector), never cancel_query. connect_raw has no socket config to redial with, so cancel_query cannot work. (inferred: socket_config is filled only by Config::connect, client.rs:723-724.)
  • On an I/O or closed error, ask transport.explain_closed_stream() (transport.rs:25; implemented for SSH at forward.rs:128-143) before mapping to ConnectionLost, so a dead ssh master is reported as an SSH failure.
  • The app today still builds v1 ConnectOptions { host, port } through the russh tunnel (app/src/services/connection_service.rs:43,207, russh_tunnel imports). Turning RequestTarget into NetworkTarget { transport: Arc<dyn Transport>, … } is W1-11, W2-11 and W4-06 work, not this lane. The PostgreSQL driver only reads ConnectTarget::Network.

4. W2-12 · PostgreSQL driver on tokio-postgres

Status: not started. The crate is the v1 sqlx driver (section 1). These pieces can be carried over:

  • the SQLSTATE table and the diagnostics extraction (lib.rs:624-656, rewritten against tokio_postgres::error::DbError);
  • normalize_pg_default and the related helpers (525-571), if W3-08's catalog still wants them;
  • the binary NUMERIC renderer (856-911, which moves under W3-09).

Every other prerequisite type exists in core (section 1). The CONN v2 trait and ConnectOptions v2 do not.

Amended design (the review wins over the spec)

  1. Crates. crates.io tokio-postgres 0.7.18, postgres-types 0.2.14 and postgres-protocol 0.6.12. No [patch] and no fork (review "Forks"; amendment FC-D1/W2-01..03). Nothing changes in deny.toml allow-git, which lists only tiberius today (deny.toml:55).

  2. Pool (amendment FC-D18/W2-12). Use deadpool_postgres::Manager::from_connect(Config, PgConnect, ManagerConfig { recycling_method: RecyclingMethod::Verified }) and Pool::builder(m).max_size(WORKSPACE_POOL_MAX_SIZE = 4).wait_timeout(pool_wait).create_timeout(connect + login).recycle_timeout(probe_timeout).runtime(Runtime::Tokio1).

    • PgConnect::connect runs transport.open(), then connect_raw(stream, PgTls), then spawns the Connection on the connection's TaskTracker, and returns (Client, JoinHandle).
    • PgPooled and pg_manager.rs are deleted.
    • A thin PgCall { object: deadpool_postgres::Object, abandon: Arc<AtomicBool> } still implements core CallResource, because that trait is core's (engine_stop.rs:50).
    • discard is Object::take(object) followed by drop, which aborts the connection task.
    • PgCall's own Drop must also take the object when abandon is set. When the grace runs out, OwnedCalls sets the flag (owned_calls.rs:298) and then drops the work future, which still holds the object. Without the take, deadpool would recycle it. Verified only runs "" (config.rs:369), which succeeds even inside a half-run write transaction. (inferred from the confirmed code paths)
    • Flights with no COMMIT or ROLLBACK acknowledgement are taken, not returned.
    • Timeout(Wait) maps to Busy. Timeout(Create) keeps the phase reached.
  3. TLS (amendment FC-D23/W2-10). A small PgTls wrapper implements tokio_postgres::tls::TlsConnect<Box<dyn TransportStream>>.

    • It delegates to the MakeRustlsConnect::new((*arc_config).clone()).make_tls_connect(server_name) connector.
    • It records handshake_started when connect is called, which only happens after the server answers 'S'.
    • For TlsMode::Disable the Config gets SslMode::Disable, and connect is never called. The wrapper type still has to exist (an Option inner, or NoTls in an enum stream).
    • Require and VerifyFull become SslMode::Require. Verification lives in the ClientConfig from tablepro_net::tls::build_client_config (client_config.rs:8-38).
    • channel_binding(Prefer) is set on Config.
    • No x509-cert or channel_binding.rs of our own; tokio-postgres-rustls brings x509-cert 0.2 transitively.
    • Placement is an open question (section 9). I recommend the driver crate (src/tls.rs) so tablepro-net stays engine-agnostic. The W2-10 spec put the connector in net.
  4. App SQL flights (FC-D21, unchanged). futures::join!(batch_execute("START TRANSACTION READ ONLY; SET LOCAL statement_timeout = <ms>; SET LOCAL lock_timeout = <ms>"), query_typed_raw/execute_typed, batch_execute("COMMIT")).

    • The milliseconds come from deadline.saturating_duration_since(now), because CallOptions carries an Instant (call_options.rs:12).
    • New rule: never render 0 for a call deadline, because statement_timeout = 0 disables the bound in PostgreSQL. Clamp to at least 1 ms, or return Timeout before sending. (inferred, PostgreSQL semantics)
    • No Client::prepare anywhere.
    • The 10,000-row enum/citext flight test stays as the read_form guard (FC-D18 amendment). The prepare_typed mitigation is deleted.
  5. Editor sessions (amendments FC-D1/FC-D19/FC-C24). The psql model stays: split client-side and send one statement per message.

    • Splitting. Use core ScriptPlan::build(text, SqlGrammar::PostgreSql, LexicalSettings { backslash_escapes: standard_conforming_strings == "off" }). Read the setting from the actor-owned Connection::parameter("standard_conforming_strings") before each statement. The driver's own script_split.rs is deleted under amendment W6-05/FC-C23 ("one core ScriptLexer/ScriptPlan"). Core already passes the W2-12 splitter cases (core/tests/script_corpus.rs:31-147).
    • Exact transaction status, with no fork. A transaction_tracker.rs derives Idle, InTransaction or Failed from the statement class plus success or failure:
      • BEGIN or START moves to InTransaction.
      • Any error in a block moves to Failed.
      • COMMIT, END or ROLLBACK moves to Idle, and COMMIT in a failed block is a rollback.
      • ROLLBACK TO SAVEPOINT in a failed block moves back to InTransaction.
      • COMMIT AND CHAIN and ROLLBACK AND CHAIN stay in InTransaction.
      • PREPARE TRANSACTION moves to Idle.
      • Undetermined covers CALL, DO, multi-statement text and anything unclassified. It triggers a probe: SAVEPOINT tablepro_probe then RELEASE. Success means InTransaction, 25P01 means Idle and 25P02 means Failed.
      • These transitions are inferred from the review's reading of xact.c and utility.c, and need tests.
      • The "Unknown" interim is rejected, because read-only enforcement depends on exact status.
    • Row limit. The SQL cursor flight replaces query_text_portal. It applies only to row sources: SELECT, VALUES, TABLE, WITH and a leading (.
      • Idle: BEGIN, DECLARE tablepro_cursor NO SCROLL CURSOR FOR\n<sql>, FETCH FORWARD <limit+1> FROM tablepro_cursor, CLOSE, COMMIT.
      • InTransaction: SAVEPOINT, DECLARE, FETCH, CLOSE, then RELEASE, or ROLLBACK TO SAVEPOINT on refusal.
      • DECLARE and FETCH go as separate messages, so a trailing -- comment cannot swallow the FETCH.
      • Error positions are corrected by subtracting the prefix length.
      • A DECLARE refusal with 0A000 (data-modifying WITH) or 42601 (SELECT INTO) falls back to simple_query_raw. That is safe because DECLARE has not executed anything.
      • Failed sessions and transaction-control statements use simple_query_raw. Rows past the limit are counted but not decoded (ReadPastLimit).
    • Read-only per transaction, unchanged from the spec except that it composes with the cursor:
      • Idle plus a row source: the read-only wrapper and the cursor are one flight, START TRANSACTION READ ONLY … COMMIT.
      • Idle plus TransactionStart: run the statement, then SET TRANSACTION READ ONLY.
      • TwoPhase: ReadOnly(ClientGuard).
      • No session SET is ever sent. Deadlines go through CancelRequest.
    • COPY. COPY … FROM STDIN is refused before sending. The upstream stream yields Err(unexpected_message) on CopyInResponse (simple_query.rs:112), and the server would wait for CopyData forever. COPY TO STDOUT output cannot be displayed until upstream exposes CopyData. Whether to refuse it or drain it with a notice is an open question.
    • Column types. Editor result columns are text-typed until rust-postgres Canceling connection does not actually cancel #1358 ships, because SimpleColumn exposes only name() (simple_query.rs:18-31).
      • The spec's whole type-name path is deleted: the Weak<PgConnectionInner> pg_type lookup, type_names.rs, and ColumnsResolved for this engine. Core's ResultEvent::ColumnsResolved (result_event.rs:43) stays unused by PostgreSQL.
      • Values are Value::Text or Value::Null from SimpleQueryRow.
    • Actor. The actor owns Client, Connection and Arc<dyn Transport>. It polls Connection::poll_message itself, so notices become ResultEvent::Notice.
  6. stream_read (XW1, step-30 addition to CONN, details 12254-12264). It runs on a standalone physical connection, never a pool slot.

    • ReadStatement::Generated uses START TRANSACTION READ ONLY; SET LOCAL statement_timeout = 0, then query_typed_raw with RawCell decode as rows arrive.
    • ReadStatement::User must use simple_query_raw (text), not query_typed_raw. Otherwise non-builtin result OIDs make tokio-postgres prepare its named typeinfo statement (prepare.rs:144), which breaks the pooler rule. (inferred)
  7. Metadata, write_batch and resolve_commit follow the spec unchanged: pg_namespace and pg_class kinds, pg_get_indexdef keys, pg_index_column_has_property, the PK, unique-NOT-NULL, ctid and tableoid ladder, pg_relation_size / block_size, pg_current_xact_id()::text / txid_current(), and pg_xact_status.

    • The ctid identities need xmin. Core EngineRowIdPart has no xmin today (row_identity.rs:33-38), which is the META/DIALECT extension owned by FC and W3.
    • ServerInfo must be filled as {product: "PostgreSQL", version, major, minor} (server_info.rs:7-12). server_version_num is not a ParameterStatus, so it costs one SHOW server_version_num. (inferred)

Remaining work: files

crates/drivers/postgres/:

Action Path Notes
change Cargo.toml See the dependency list below
rewrite src/lib.rs Module declarations and pub use driver::PgDriver; pub const WORKSPACE_POOL_MAX_SIZE: usize = 4; (W9-07 "part 2 of 2")
new src/driver.rs DatabaseDriver v2: capabilities(), connect(options, cancel)
new src/capabilities.rs Network 5432, [Password], all TLS modes with CA, identity and server name, SessionDefault, Transactional/Transactional, NativeBatch, ServerTransactionStatus, Statistics, the 8 object kinds, database_required false
new src/physical.rs connect_physical(target, liveness, cancel): open, Config, connect_raw under login_timeout raced against cancel, ServerInfo query
new src/tls.rs PgTls wrapper over tokio-postgres-rustls with handshake_started (replaces net postgres_connector.rs and channel_binding.rs, which were never created)
new src/pool.rs PgConnect: deadpool_postgres::Connect, pool builder, PgCall CallResource with Drop-take on abandon, PoolError mapping. Replaces the spec's pg_pooled.rs and pg_manager.rs
new src/connection.rs CONN v2 impl: link state, lost(), probe with timeout_get(wait ZERO) and standalone fallback, close
new src/workspace_call.rs FC-D21 flight, millisecond preamble (non-zero clamp)
new src/write_batch.rs run_write, ledger, guards, xid, COMMIT outcome mapping
new src/commit_resolution.rs pg_xact_status / txid_status
new src/cancel.rs PgStop: EngineStop (OutOfBand(Connection), cancel_query_raw over transport.open(), 57014 acknowledgement)
new src/stream_read.rs XW1
new src/editor_session.rs Actor, run loop, sink backpressure, deadline and cancel
new src/editor_dispatch.rs Pure plan: (class, tracked status, read_only, row-source) to a flight
new src/transaction_tracker.rs Status transitions plus the SAVEPOINT probe (amendment; not in the spec)
new src/cursor_flight.rs DECLARE/FETCH flight, savepoint wrapper, position correction, 0A000/42601 fallback (amendment; not in the spec)
new src/statement_class.rs Leading-keyword class: TransactionStart, TransactionEnd (with AND CHAIN), Savepoint, RollbackToSavepoint, TwoPhase, CopyFromStdin, CopyToStdout, RowSource, Undetermined, Other
new src/errors.rs DbError to DriverError (28P01/28000, 25006, 25001 inside the wrapper, 57014 through OwnedCalls, 55P03, 53300); TLS classify; explain_closed_stream
new src/catalog.rs fetch_columns: FB-D8 domain CTE, enum labels, typmod, read_form. Shared with W3-08
new src/metadata/{mod,objects,indexes,identity,estimate}.rs
delete n/a The spec's script_split.rs, type_names.rs, pg_pooled.rs and pg_manager.rs. decode.rs, raw_cell.rs, pg_param.rs, numeric_wire.rs and array_wire.rs are W3-09's files (section 5)
delete tests/integration.rs, tests/tls.rs tests/tls.rs imports sqlx at line 5
keep or fold tests/tls_provider.rs
new tests/integration/main.rs and topic modules cancellation.rs, editor_session.rs, read_only.rs, write_batch.rs, metadata.rs, pgbouncer.rs, liveness.rs, tls.rs (engine-specific only), decode.rs (W3-09), conformance_engine.rs (W9-07)
no edit needed ../.github/workflows/build-linux.yml Already per-package nextest (build-linux.yml:157-200)
no new file tests/fixtures/pgbouncer.ini PgBouncerFixture replaces it (details 16484)

Outside the crate:

  • crates/test-fixtures/src/pgbouncer_fixture.rs: add a default_pool_size/max_client_conn option for transaction_pool_editor_leaves_no_session_state (pool size 1). The current config has no pool size (pgbouncer_fixture.rs:59-70).
  • crates/test-fixtures/src/test_pki.rs: ECDSA P-384 and Ed25519 server-certificate variants for the channel-binding tests (today RSA only at :26,33). This is W9-02's file, so coordinate with that lane.
  • crates/test-support/src/fault_transport.rs with stall_reads_after_write_containing, owned by W4-01/TEST-SUPPORT, for commit_stall_deadline_commit_outcome_unknown_then_applied.
  • linux/Cargo.toml: add workspace entries for the new crates if they are declared at the workspace level. Remove rust_decimal and the sqlx rust_decimal feature only when the MySQL and MSSQL switches drop them; it is the same commit in step 30.

Dependencies

Change Crate and version Features
add tokio-postgres 0.7.18 default-features = false, ["runtime"]. deadpool-postgres turns default features on anyway
add postgres-types 0.2.14 ["with-chrono-0_4"], for Date<NaiveDate> / Timestamp<NaiveDateTime>; optionally with-uuid-1
add postgres-protocol 0.6.12
add deadpool-postgres 0.14.2 default rt_tokio_1; deadpool 0.13.1 comes transitively
add tokio-postgres-rustls 0.14.0 default-features = false; brings sha2 0.11 and x509-cert 0.2
add rustls (workspace 0.23) normal dependency, to clone the ClientConfig
add tokio rt, time, sync, macros
add tokio-util 0.7.19 rt, for TaskTracker and CancellationToken
add futures-util 0.3.34
add bytes 1.12.1
add tracing
keep tablepro-core, tablepro-net, async-trait, chrono, uuid, secrecy
remove sqlx, rust_decimal, futures (use futures-util), serde_json serde_json goes if JSON stays raw text through JsonText::parse, which is likely
none [patch.crates-io], deny allow-git
check cargo deny Multiple-versions warnings, for example sha2 0.10 and 0.11 if postgres-protocol still uses 0.10. Not verified

Tests the spec requires (amended)

Unit:

  • statement_class::leading_keyword_table, extended with AND CHAIN, ROLLBACK TO, COPY FROM STDIN, COPY TO STDOUT and row-source detection.
  • editor_dispatch::read_only_plan_per_class_and_status.
  • workspace_call::preamble_uses_integer_milliseconds, plus a sub-millisecond deadline that never renders 0.
  • errors::sqlstate_table.
  • capabilities_match_driver_contract.
  • The script_split cases are already covered by core's corpus. Add only a driver test that standard_conforming_strings off maps to backslash_escapes.
  • New: transaction_tracker::transitions_table and cursor_flight::error_position_is_corrected.
  • numeric_wire tests are W3-09's.

integration/tls.rs, engine-specific only after the W9-08 macros:

  • channel_binding_require_with_rsa_sha256_and_ecdsa_p384
  • ed25519_cert_require_fails_prefer_connects
  • server_without_ssl_reports_server_refused_tls
  • Removed by details 16478: verify_full_with_ca_ok, without_ca_unknown_issuer, wrong_server_name_name_mismatch, require_ok.

integration/cancellation.rs:

  • pg_sleep_cancel_returns_cancelled_within_2s_backend_idle
  • deadline_1s_timeout_server_cancelled
  • dropped_caller_future_backend_idle_within_2s
  • connection_scoped_stop_never_reaches_next_call comes from the W9-07 macro.

integration/editor_session.rs:

  • From the spec: begin_update_rollback_persists_state, set_search_path_and_temp_table_persist, create_function_with_dollar_body, create_function_begin_atomic_runs_as_one_statement, vacuum_after_select_in_same_script_succeeds, row_limit_server_stopped_select_list_generate_series, delete_returning_50000_limit_100_deletes_all, begin_insert_big_select_commit_persists_and_state_in_transaction, script_insert_select_insert_both_persist, slow_sink_receives_all_rows_in_order, consumer_gone_cancels_and_session_ready.
  • Renamed: select_inside_begin_uses_portal_row_limit becomes select_inside_begin_uses_savepoint_cursor_row_limit.
  • Deleted: type_created_in_open_transaction_resolved_via_columns_resolved (no editor type OIDs until Canceling connection does not actually cancel #1358).
  • New from the amendments and the dropped W2-03 fork tests:
    • cursor_row_limit_reads_under_1mib, the byte-counting stream from the W2-03 test;
    • cursor_trailing_line_comment_still_fetches;
    • data_modifying_with_falls_back_0a000;
    • select_into_inside_block_falls_back_42601_and_block_survives;
    • copy_from_stdin_refused_before_send;
    • parse_error_leaves_session_idle;
    • call_procedure_with_commit_probes_status;
    • do_block_commit_probes_status;
    • commit_and_chain_stays_in_transaction;
    • savepoint_probe_25p01_idle_25p02_failed.

integration/read_only.rs keeps all 7 spec tests: editor_idle_delete_fails_25006, editor_begin_then_delete_fails_25006, editor_set_default_transaction_read_only_off_does_not_escape, editor_vacuum_refused_as_read_only, editor_commit_prepared_refused_client_guard, write_batch_client_guard, query_bound_nextval_is_25006.

integration/write_batch.rs:

  • Kept: lock_timeout_lock_wait, concurrent_delete_row_guard_failed_rolls_back_all, statement_deadline_before_commit_rolls_back, commit_stall_deadline_commit_outcome_unknown_then_applied (needs FaultTransport), deferred_constraint_commit_error_is_rolled_back.
  • Removed by the W9-07 macros: row_changes_statement_timeout_1s_times_out, schema_changes_unbounded.
  • New: abandoned_write_connection_is_taken_not_recycled.

integration/metadata.rs keeps all 8 spec tests, including stale_ctid_edit_after_vacuum_reuse_row_guard_failed (needs xmin) and ctid_window_explain_shows_tid_range_scan. New from FC-D18: enum_citext_10000_row_flight_never_prepares.

integration/pgbouncer.rs (PgBouncerFixture, v1.25.2-p0):

  • the transaction-pooling round trip;
  • transaction_pool_set_local_timeout_applies;
  • transaction_pool_read_only_editor_delete_fails;
  • transaction_pool_editor_leaves_no_session_state (needs pool size 1);
  • session_pool_editor_state_persists;
  • statement_pool_write_batch_returns_diagnostics;
  • no_named_parse_with_max_prepared_statements_0;
  • new: statement_pool_editor_cursor_falls_back.

integration/liveness.rs:

  • terminate_backend_link_stays_open_after_healthy_probe
  • stopped_container_marks_lost
  • probe_with_saturated_pool_is_ok is removed in favour of W9-07 case 5.

Depends on

  • W2-11: CONN v2 Connection, DRIVER v2, EditorSession wiring, XW1 stream_read/ReadStatement/StreamSummary. Core still has v1 (connection.rs:64-118, driver.rs:7-48).
  • W2-10: done for the dialer, TLS config and classify. The PG TLS connector moves to tokio-postgres-rustls under the amendment.
  • W2-09: done (types present).
  • W3-06: Statement and browse builders, partly present.
  • W3-07: PgDialect and engine_row_id with xmin.
  • W3-08: catalog read_form, shared catalog.rs.
  • W3-09: codecs (section 5).
  • W1-11: NetworkTarget with the OpenSSH transport.
  • W4-01 / TEST-SUPPORT: FaultTransport.
  • W9-02: TestPki ECDSA and Ed25519.
  • W9-07 and W9-08: the shared macros.

Spec text that is stale or amended

  • "tokio-postgres 0.7.18 (fork)", query_text_portal, SimpleColumn::type_oid, SimpleQueryStream::transaction_status, "using the fork's status from the previous ReadyForQuery" and "transaction_state() is the fork status" (details 7704-7729, 7764). These are replaced by the tracker, the SAVEPOINT probe and the cursor flight (amendment FC-D1).
  • "PgManager implements deadpool Manager. Type = PgPooled …; recycle runs simple_query_raw("") … rejects unless status is Idle" (7650-7654) and FC-D18 (3093-3094). Replaced by deadpool-postgres from_connect plus Verified, with take-on-abandon (amendment FC-D18).
  • "deadpool 0.13.1 [managed, rt_tokio_1]" as a direct dependency (7761). Use deadpool-postgres 0.14.2 instead.
  • "Type names … Weak … ColumnsResolved" (7723-7727). Deleted until Canceling connection does not actually cancel #1358.
  • script_split::split mirroring psqlscan.l (7695-7700). Superseded by core ScriptPlan (amendment W6-05/FC-C23; commit 43eea7e).
  • Files pg_pooled.rs, pg_manager.rs, script_split.rs, type_names.rs, tests/smoke_local.rs (deleted) (already gone, 869df6f), tests/fixtures/pgbouncer.ini and the build-linux.yml edit (7637, 7762) are stale.
  • FC-D21 "Editor sessions … SET default_transaction_read_only = on / SET statement_timeout after connect" (3176) and FC-C4 (3357) contradict W2-12's "No session-level SET is ever sent". W2-12 is the later reconciliation and should win. Fix the FC text.
  • PG-DRIVER (5907) and FC-C17 (3808): "fork (FC-C24)", "rust_decimal [db-tokio-postgres]". Stale (amendments FC-D1 and the rust_decimal editorial note).
  • CONN "fn dialect()" and the ServerInfo shape from FC-C8 differ from the current core types (server_info.rs:7-12). CallOptions deadline is an Instant (call_options.rs:12), not Duration as in FC-C8 and CONN.
  • The FC-C17 Cargo list (with-uuid-1, with-serde_json-1, array-impls) is not needed: RawCell plus postgres-protocol do the decoding.

Size

About 3,300 production lines, 700 unit-test lines and 1,600 integration-test lines added, and about 1,400 lines deleted (lib.rs plus the sqlx tests). That is roughly 5,600 changed lines, excluding W3-09's codecs and the shared W9 modules.


5. W3-09 · PostgreSQL part: lossless decode and encode

Status: partial, on the wrong stack.

  • Present on sqlx: the binary NUMERIC to exact-text renderer with tests (lib.rs:856-911, 937-1033), JSON and JSONB raw bytes (801-825), bit strings (932-935), Float32 (401), and Undecodable-not-Null (450-460, 777-784).
  • Missing:
    • NaN and ±Infinity as SqlDecimal (today they become Float64, 921-928);
    • date and timestamp infinity (406-430 only produce Temporal::Finite);
    • time 24:00:00 (409-411);
    • timetz and interval through the custom wire (today they use the sqlx types at 412-422 and 435-441);
    • arrays with lower bounds;
    • inet, cidr and macaddr as Other;
    • RawCell for tokio-postgres;
    • PgParam, the ToSql encode by ParamType. Today sqlx binds through rust_decimal (466-510, 791-794).

Amended design (W3-09/FB-D5)

  • Upstream decoders (all binary in query_typed_raw):
    • postgres_types::{Date<NaiveDate>, Timestamp<NaiveDateTime>, Timestamp<DateTime<Utc>>} for date, timestamp and timestamptz (infinity variants and checked arithmetic; chrono_04.rs:21,119);
    • postgres-protocol bool/int2/int4/int8/float4/float8/text/bytea/uuid/oid_from_sql, array_from_sql, range_from_sql, inet_from_sql, macaddr_from_sql, varbit_from_sql, point/box/path_from_sql and lsn_from_sql.
  • Custom code stays only for:
    • numeric_wire (move lib.rs:856-911 and return SqlDecimal::nan()/infinity());
    • interval_wire (i64 µs, i32 days, i32 months);
    • time (time_from_sql then SqlTime::from_micros, because upstream NaiveTime wraps 24:00:00, chrono_04.rs:142);
    • timetz (i64 µs plus i32 seconds west).
  • "Evaluate text-form numeric first":
    • Decode: binary results are fixed by tokio-postgres (query.rs binds binary result formats; inferred). Keeping the existing tested binary renderer is cheaper than projecting ::text for every numeric column.
    • Encode: ToSql::encode_format exists (confirmed), so PgParam can send Decimal, Interval, Time (24:00) and TimeTz as text with their real type OID (Type::NUMERIC, …). That avoids writing binary encoders. Recommended. (inferred design on a confirmed API)
  • Unreachable decoders. hstore and ltree have non-builtin OIDs, so DIALECT read_form already projects them ::text (details 5721). Their binary decoders can never run in app SQL. Drop them from the review's list, or keep them only for arrays the server sends as builtin. (inferred)
  • read_form interplay with W3-08. The ratified refinement ("ServerText … for built-in OIDs without a W3-09 binary codec (tsvector, point, pg_lsn)", details 9981) shrinks to tsvector, because point and pg_lsn now have upstream codecs. Alternatively keep them ServerText for exact server text. Decide this with the W3-08 lane.
  • Editor path. decode_text "through the fork's type_oid" (9380) is stale. Editor cells are Value::Text or Null until Canceling connection does not actually cancel #1358. Delete decode_text.rs from the file list, or keep it unused until then.

Remaining work: files

Action Path
new crates/drivers/postgres/src/raw_cell.rs: RawCell(Value): FromSql, accepts every type, from_sql_null gives Null
new src/decode.rs: OID dispatch, malformed input gives InvalidEncoding, overflow gives OutOfRange, otherwise UnsupportedType
new src/numeric_wire.rs: moved and fixed
new src/interval_wire.rs
new src/temporal_wire.rs: time and timetz only; date and timestamp go through postgres-types
new src/array_decode.rs: array_from_sql, recursive, lower bound ≠ 1 gives Other [lb:ub]={..}
new src/network_text.rs: inet, cidr and macaddr through postgres-protocol, rendered to canonical text as Other
new src/pg_param.rs: PgParam<'a>(&'a BoundParam): ToSql, accepts true, to_sql_checked!, ParamType to Type map, encode_format text for Decimal, Interval, Time, TimeTz and ServerText
change src/lib.rs
change Cargo.toml (shared with W2-12)
new tests/integration/decode.rs, replacing tests/integration.rs's value_roundtrip_all_types (:75) and binary_only_types_read_back (:238)
drop decode_text.rs from the spec list

Open point for W3-06: ParamType has no Bits variant (param_type.rs:10-35). Value::Bits is accepted under ParamType::Bytes (:59), which PostgreSQL will refuse for a bit or varbit column. Either add a Bits ParamType or bind bits as ServerText(bit(n)) with text format.

Dependencies

Same as W2-12: postgres-types 0.2.14 [with-chrono-0_4] and postgres-protocol 0.6.12. Remove rust_decimal from this crate.

Tests (spec)

Unit:

  • numeric_wire_round_trips
  • numeric_wire_malformed_is_undecodable_not_panic
  • date_timestamp_infinity_sentinels
  • timestamp_overflow_is_out_of_range
  • time_24_00_00
  • interval_wire_round_trip
  • array_lower_bound_2_is_other_text
  • pg_param_null_typed_by_oid
  • Add pg_param_decimal_nan_sent_as_text_numeric.
  • Keep the existing numeric tests (lib.rs:954-1017), adjusted from Float64 NaN to SqlDecimal.

Integration:

  • round_trip_matrix
  • set_null_saves_typed_null
  • char10_key_update_affects_one_row
  • The regression map cites round_trip_matrix (details 16365).

Depends on

W2-12 (RawCell use sites), W3-05 and W3-06 (Value and ParamType, present), W3-08 (read_form), W8-12 (supersedes the "keep PG on sqlx 0.8.6" interim, which the review amended).

Stale or amended text

  • "Scalars through postgres-protocol codecs" is fine.
  • "date and timestamp infinity sentinels with checked_add_signed" is now upstream postgres_types::{Date, Timestamp} (amendment W3-09).
  • "decode_text … through the fork's type_oid" (9380) is stale.
  • FB-C13 still says "bool … use sqlx Decode" and "interval: PgInterval" (2250, 2254). Stale for PostgreSQL.
  • The "Rewrite the rationale that cites sqlx-postgres" (FB-D5, 1272) is an editorial fix.
  • Files crates/drivers/postgres/tests/integration.rs (9365) should become tests/integration/decode.rs.

Size

About 900 production lines (≈130 of them moved), 350 unit-test lines and 350 integration-test lines: roughly 1,600 changed lines.


6. W9-07 · PostgreSQL side: connection conformance

Status: not started.

  • No crates/test-support/src/conformance/ exists. The test-support files are call_counters, close_behaviour, connection_script, fake_connection, fake_driver, fake_prompter and fake_secret_vault.
  • No tests/integration/ tree exists in the PostgreSQL crate.
  • No WORKSPACE_POOL_MAX_SIZE export exists.
    (confirmed)

Remaining work (PostgreSQL side only; the shared module belongs to whichever lane owns W9-07 core)

  • crates/drivers/postgres/tests/integration/conformance_engine.rs (new): PgEngine implementing ConformanceEngine, NetworkConformanceEngine, PooledConformanceEngine, EditorTransactionEngine, TypedConformanceEngine and BoundedDdlEngine.
    • start() uses PostgresFixture::start(HbaMode::Password).
    • connect_options builds NetworkTarget with DirectTcpTransport.
    • faulty_connect_options wraps it in FaultTransport.
    • unbounded_statement_sql is SELECT pg_sleep(3600) /*marker*/.
    • blocking_statement_sql is pg_sleep(hold).
    • running_statement_count_sql counts pg_stat_activity WHERE state='active' AND query LIKE '%marker%' AND pid <> pg_backend_pid().
    • row_source_sql is generate_series.
    • schema_change_exceeding_sql is CREATE TABLE t AS SELECT 1 AS id FROM pg_sleep(3).
    • unsupported_type_cases are point, int4range, a composite and an enum.
    • WORKSPACE_POOL_MAX_SIZE comes from the driver.
  • crates/drivers/postgres/tests/integration/main.rs: connection_conformance_suite! { engine: PgEngine, docker: true, endpoint: network, pool: pooled, editor_transactions: true, unsupported_types: true, bounded_ddl: true }.
  • No CI change: nextest per package plus #[ignore = "requires docker"].

Tests

The 14 cases plus suite_flags_match_driver_capabilities run against PostgreSQL. Case 6 (row limit keeps transaction and later statements) exercises the new cursor flight. Case 13 is "unsupported types never decode as Null".

Depends on

W2-11, W2-12, W4-01 (FaultTransport), W9-02 (fixtures, present), W9-03 (the matrix is now nextest per package, already in CI).

Stale or amended text

  • "unsupported_types (point, int4range, …)" (15981): after the W3-09 amendment, point and int4range have upstream codecs. The case still holds (non-null reads stay non-null), but the name "unsupported" is now loose.
  • build-aux/tests/test_integration_matrix.py (15919, 16005) is deleted by amendment W9-03 (nextest).
  • "deadpool 0.13.1 PoolConfig::default max_size" stands, but the constant now feeds deadpool-postgres's PoolBuilder::max_size.

Size

About 220 lines for the engine plus 30 for main.rs: roughly 250 changed lines on the PostgreSQL side.


7. W9-08 · PostgreSQL side: TLS conformance

Status: not started.

  • tests/tls.rs covers prefer, require and plaintext-refused over sqlx and the v1 API (tests/tls.rs:26-72).
  • The fixtures exist: HbaMode::ClientCertificate with hostssl … cert for tablepro_client (hba_mode.rs:20-25), certificate_credentials() (postgres_fixture.rs:133) and PkiFile::UnrelatedCa (pki_file.rs:4). The served name db.tablepro.test is in TestPki::SERVER_NAMES (test_pki.rs:19).
    (confirmed)

Remaining work

  • crates/drivers/postgres/tests/integration/tls.rs (new):
    • PgTlsEngine implementing TlsConformanceEngine and ClientIdentityEngine.
    • start is PostgresFixture::start(HbaMode::ClientCertificate).
    • ca_file and unrelated_ca_file come from TestPki, and served_name is "db.tablepro.test".
    • plaintext_rejection_category is most likely Authentication: the server answers 28000 "no pg_hba.conf entry … no encryption" for hostnossl reject (inferred; verify).
    • client_identity is tablepro_client with the client cert and key.
  • Invocation: tls_conformance_suite! { engine: PgTlsEngine, client_identity: true }.
  • The engine-specific tests from W2-12 stay beside it: channel binding with RSA-SHA256 and ECDSA-P384 under channel_binding(Require), Ed25519 failing Require but connecting with Prefer, and server_without_ssl_reports_server_refused_tls against HbaMode::Password.
    • These need TestPki ECDSA and Ed25519 server keys (W9-02 file).
    • The Ed25519 result follows tokio-postgres-rustls's _ => None (L125).
    • md5, ecdsa-with-SHA1 and ecdsa-with-SHA512 cannot be tested until the upstream OID PR ships (amendment FC-D23).

Tests

Six cases plus tls_suite_flags_match_driver_capabilities for PostgreSQL.

Depends on

W2-10 (TLS config, present), W2-12 (PgTls wrapper), W9-02 (PKI variants), W9-07 (shared ConformanceFailure and macro module).

Stale text

The engine-specific list "channel binding, ed25519" stands, but the implementation is now tokio-postgres-rustls's mapping, not x509-cert 0.3 (amendment FC-D23).

Size

About 120 lines for the engine plus 60 for the engine-specific cases. TestPki variants, if done here, add about 80. Roughly 200-260 changed lines.


8. Implementation order

  1. Prerequisites, in other lanes or done first on the stacked step-30 branch: W2-11 CONN v2 and ConnectOptions v2 (NetworkTarget); W1-11 OpenSSH transport into targets; W3-06 and W3-07 PgDialect with xmin in EngineRowIdPart; W4-01 FaultTransport; W9-02 TestPki ECDSA and Ed25519 plus the PgBouncer pool-size option.
  2. W3-09 PostgreSQL codecs as pure modules with unit tests: raw_cell, decode, numeric_wire (moved and fixed), interval_wire, temporal_wire, array_decode, network_text, pg_param. They depend only on postgres-types and postgres-protocol, so they can compile on the branch before the trait switch.
  3. W2-12 base: tls.rs (PgTls wrapper), physical.rs, pool.rs (Connect, PgCall, take-on-abandon), errors.rs, capabilities.rs, driver.rs.
  4. W2-12 workspace side: workspace_call.rs, metadata/*, catalog.rs (with W3-08), query_bound/count_rows, stream_read.rs, cancel.rs (PgStop), write_batch.rs, commit_resolution.rs, connection.rs (probe and link loss).
  5. W2-12 editor side: statement_class.rs, transaction_tracker.rs (with the SAVEPOINT probe), cursor_flight.rs, editor_dispatch.rs, editor_session.rs (actor over core ScriptPlan).
  6. W9-07 PostgreSQL engine and W9-08 PostgreSQL TLS engine, then the engine-specific integration modules (cancellation, editor_session, read_only, write_batch, metadata, pgbouncer, liveness, decode).
  7. Delete the sqlx code, tests/integration.rs and tests/tls.rs. Drop sqlx and rust_decimal from the crate, and run cargo deny (bans plus a cargo tree -i rust_decimal check after all drivers switch).

9. Open questions and risks

  1. deadpool-postgres Connect cannot return typed transport errors.
    • The trait's error is tokio_postgres::Error, which has no public constructor (confirmed).
    • A pool-created connection that fails in transport.open() (name resolution, refused, SSH master exited) cannot surface as DriverError::Transport.
    • Options:
      • (a) A failing stream adapter whose write yields io::Error::other(TransportError), so the error comes back through Error::source() / io::Error::get_ref. The connect_raw path wrapping I/O errors that way is inferred.
      • (b) A per-call side slot in PgConnect.
      • (c) Accept a degraded ConnectionLost for later pool members, since the first connection is opened outside the pool with full typing.
    • This needs a decision. It is the one place where "use deadpool-postgres" costs fidelity.
  2. Recycle cannot see the transaction status. Safety rests on two things: take-on-abandon in PgCall::drop, and taking every flight without a COMMIT or ROLLBACK acknowledgement. A missed path returns a connection inside an open transaction to the pool. That needs a regression test (abandoned_write_connection_is_taken_not_recycled).
  3. Exact status without a fork. The tracker is inferred from PostgreSQL rules. CALL and DO with transaction control, COMMIT AND CHAIN and procedures that end in a different state must route through the SAVEPOINT probe. Every Idle probe writes a server ERROR log line (25P01). An upstream status PR is the exit, and it goes on the watch list.
  4. Cursor row limit.
    • It changes plans (cursor_tuple_fraction 0.1) and disables parallel plans (as Execute with a row count also did).
    • It needs a driver-owned BEGIN … COMMIT when Idle.
    • It falls back under PgBouncer statement pooling.
    • It needs exact position correction.
    • COPY TO STDOUT output is lost until upstream exposes CopyData. Refuse it or drain it with a notice?
  5. COPY FROM STDIN detection is client-side. A misclassified COPY would leave the session waiting. The deadline and CancelRequest are the backstop, but whether cancel interrupts COPY IN reads is unverified.
  6. Editor columns are text-typed until rust-postgres Canceling connection does not actually cancel #1358 (unreviewed since 2026-05-29, per the review). This is user-visible: no typed bool, bytea or numeric formatting in editor results.
  7. TLS wrapper placement: the PostgreSQL driver or tablepro-net. The W2-10 spec places it in net, while the amendment only says "wrapped". The driver crate keeps tokio-postgres out of net.
  8. tokio-postgres-rustls is a personal repository with a sole owner. It lacks three signature OIDs, and the upstream PR must be filed. Accepted as a bus-factor-1 exception (amendment FC-D23); it needs an ADR entry.
  9. The statement_timeout 0 hazard when a call deadline is already near: clamp it, or fail early.
  10. Value and ParamType gaps: no Bits ParamType, and no xmin EngineRowIdPart. Both belong to other lanes but block W2-12 and W3-09 tests.
  11. PgBouncerFixture lacks a pool-size option, and TestPki lacks ECDSA and Ed25519. Both are fixture changes in the W9-02 files.
  12. The FC-D21 and FC-C4 editor SET text contradicts W2-12. Resolve it as "no session SET", and fix the FC text so the W2-19 docs do not reintroduce it.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/drivers.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

lanes/drivers.md

Lane: drivers (W2-13, W2-14, W2-15, W2-16, W3-09 MySQL and SQLite parts)

Base read: worktree HEAD 737f5e2ee, which contains origin/linux 06794b85c (git merge-base --is-ancestor passed). Linux-side commits on HEAD past origin/linux touch only crates/app/tests/startup.rs and docs/testing.md.

Labels: [C] means confirmed by a file I opened (path:line) or a URL I fetched. [I] means inferred.

Precedence: the dependency review (linux/docs/dependency-review-2026-09-14.md) wins over the plan. I applied every amendment that names W2-13, W2-14, W2-15, W2-16, W3-09, FC-D2, FC-D3, FC-D5, FC-D6, FC-D16, D12 and W8-10. Where upstream changed after the review (tiberius 0.13.0 on 2026-09-25), the current upstream state wins, and I say so.


0. Findings shared by all five tasks

0.1 What the core contract has today (W2-11 belongs to another lane)

Present, and usable by the drivers [C]:

  • OwnedCalls::run_read / run_write: crates/core/src/owned_calls.rs:47,131
  • EngineStop, StopDelivery, StopScope, CallResource: crates/core/src/engine_stop.rs
  • WriteLedger and WriteAtomicity: crates/core/src/write_ledger.rs:10,24-124
  • WriteBatch { kind, table, steps }: crates/core/src/write_batch.rs:21-25
  • EditorSession, RunOptions, ResultEvent, Truncation, RunSummary, TransactionState, RowLimit, UserSql
  • DriverCapabilities and its enums: crates/core/src/capabilities.rs:59-73
  • DriverError v2 with no FileExists: crates/core/src/error.rs:25-126
  • ServerCode::{MySql, Tds, ClickHouse, Sqlite}: crates/core/src/server_diagnostics.rs:10-15
  • CommitToken::ClickHouseQuery: crates/core/src/commit.rs:15-18
  • FileEndpoint::{open_existing, create_new, for_reopen}: crates/core/src/file_endpoint.rs
  • NetworkEndpoint, Transport, TransportRoute { Tcp, UnixSocket }: crates/core/src/transport_route.rs:6-9
  • TlsConfig, TlsMode { Disable, Require, VerifyFull }
  • Meta types (IndexInfo { keys, predicate }, RowIdentity, TableEstimate, ObjectKind, SchemaInfo, ObjectScope)
  • The lossless Value: crates/core/src/value/mod.rs:28-50
  • Statement, BoundParam, ParamType, GuardedStatement, RowGuard: crates/core/src/statement/
  • SqlDialect and EngineDialect: crates/core/src/dialect/

Missing, and every driver task needs it [C, by grep over crates/core/src]:

  • Connection v2 trait. crates/core/src/connection.rs:65 is still the v1 trait (list_tables, query, execute_in_transaction).
  • DatabaseDriver v2 with capabilities() and connect(options, cancel). crates/core/src/driver.rs:7 is v1.
  • ConnectOptions v2. ConnectTarget, NetworkTarget, Credentials, WorkspaceSettings, ConnectEnvironment and APPLICATION_NAME do not exist. ConnectOptions is still host/port/service_endpoint (connection.rs:26-36).
  • TableDefinition (W3-18) and ForeignKeyMode (W3-19) do not exist.
  • The conformance module (W9-07) does not exist. crates/test-support/src has only fakes.

0.2 Transports each engine must accept

Two transports exist today [C]:

  • DirectTcpTransport: route() returns Tcp(endpoint), and open() returns a socket2-tuned TcpStream (crates/net/src/direct_tcp.rs:42-65, crates/net/src/dial.rs:16-37).
  • SshForwardTransport: route() returns UnixSocket(path) for the OpenSSH -O forward socket, and open() returns a UnixStream (crates/ssh/src/forward.rs:99-122). reroute() opens a new forward (forward.rs:124-126).

No relay exists in crates/net (crates/net/src/lib.rs:1-6), which matches the review amendment for FC-D2 and W2-13.

Engine How it reaches the server TLS name Notes
MySQL (sqlx-mysql 0.9.0) Uses transport.route(), never open(). Tcp(ep) becomes .host(ep.host()).port(ep.port()). UnixSocket(p) becomes .socket(p).host(<service host or server-name override>). sqlx always uses options.host [C sqlx-mysql-0.9.0/src/connection/tls.rs:62]. It dials connect_uds when socket is set [C establish.rs:18-20]. The server-name override only works over SSH (ServerNameOverride::SshForwardOnly). Over TCP the host is both the dial target and the TLS name. Direct TCP has no TCP_NODELAY (0.9.0) and no keepalive, so the health probe is the only liveness check. The KILL QUERY control connection uses the same route.
SQLite FileEndpoint only. No transport. n/a
SQL Server (tiberius) transport.open() gives a boxed tokio stream, then compat_write(), then Client::connect(config, stream). Routing uses transport.reroute(endpoint) once. Config::host(service host) with . mapped to localhost. tiberius 0.13 adds hostname_in_certificate for the override [C, fetched main src/client/config.rs]. Works the same way over TCP and over the SSH Unix socket.
ClickHouse (clickhouse 0.15.2) Client::with_http_client(hyper_util Client<HttpsConnector<C>>) [C clickhouse-0.15.2/src/lib.rs:152, http_client.rs:23-33]. C is a tower_service::Service<Uri> whose call() runs transport.open(). hyper-rustls takes SNI from the URL host, so the URL must name the service host [I]. hyper-util implements Connection for UnixStream and TokioIo<T> [C hyper-util-0.1.20/src/client/legacy/connect/http.rs:583,598]. Its Connect blanket impl needs S::Future: Unpin + Send [C connect/mod.rs:340-344].

0.3 Pools

  • The review keeps deadpool managers for sqlx-mysql (MysqlManager) and tiberius (MssqlManager) only.
  • SQLite stays on SqlitePool with max_connections(1).
  • ClickHouse uses hyper's pool through pool_idle_timeout.
  • deadpool is not in Cargo.lock [C grep]. Latest is 0.13.1 (2026-08-26) [C crates.io].
  • Manager has type Type, type Error, fn create(&self) -> impl Future<Output=Result<..>> + Send, fn recycle(&self, &mut Type, &Metrics) -> impl Future<Output=RecycleResult<..>> + Send and fn detach [C https://docs.rs/deadpool/0.13.1/deadpool/managed/trait.Manager.html].
  • Features: managed (default) and rt_tokio_1 [C docs.rs features page].
  • Add: deadpool = { version = "0.13.1", default-features = false, features = ["managed", "rt_tokio_1"] } to [workspace.dependencies].

0.4 Upstream changes since the review (2026-09-14 to 2026-10-12)

  • tiberius 0.13.0 is on crates.io (2026-09-25) [C https://crates.io/api/v1/crates/tiberius/versions].
    • Main carries Client::cancel_query, an additive Config::trust_cert_ca, trust_cert_ca_bundle, hostname_in_certificate, handshake_timeout (default 15 s) and command_timeout (default 30 s, per round trip; it sets command_desync, after which the connection must not be reused) [C fetched main src/client.rs, src/client/config.rs, src/client/connection.rs].
    • Still missing: money_as_numeric (MONEY decodes to F64, per src/tds/codec/column_data/money.rs) and an injectable rustls ClientConfig. Error::Tls is still String [C fetched v0.13.0 rustls_tls_stream.rs].
    • Crypto provider: CryptoProvider::get_default(), falling back to aws-lc-rs [C same file].
    • Default features are [tds80, winauth, native-tls]. libgssapi is ^0.11 [C https://docs.rs/crate/tiberius/0.13.0/features].
    • The review's trigger "drop the patch when crates.io 0.13 ships" has fired.
  • sqlx is still 0.9.0 [C crates.io]. #4336 (TCP_NODELAY) was merged to main on 2026-08-17 but is not released [C api.github.com pulls/4336]. #4281 (custom transport) is still open, last updated 2026-05-28 [C]. FC-D3 ("first 0.9.x patch with #4336") cannot be met today.
  • clickhouse 0.15.2 is still the latest [C crates.io]. clickhouse-types 0.1.3 and hyper-rustls 0.27.9 / hyper-util 0.1.20 / tower-service 0.3.3 are already locked (Cargo.lock lines 837, 2253, 2282, 5667) [C].

0.5 Workspace items the switch must change (linux/Cargo.toml)

  • Line 37: clickhouse enables rustls-tls-aws-lc and rustls-tls-webpki-roots. W2-16 turns the crate's TLS features off [C].
  • Line 38: sqlx features include rust_decimal and json. Drop rust_decimal at the switch. crates/app (grid.rs) and the PG driver still use rust_decimal, so it leaves in the same commit [C grep].
  • Line 39: tiberius = "0.12" with rust_decimal. Change to "0.13", default-features = false, features ["tds73", "rustls", "chrono"].
  • Lines 96-102: delete [patch.crates-io] tiberius.
  • deny.toml:58: allow-git = [tiberius-rs] becomes empty [C].
  • deny.toml:42-53: the ban on rust_decimal is not there yet (FD-D19 adds it after the switch) [C].
  • deny.toml:52: the tablepro-test-fixtures wrappers list only tablepro-driver-postgres. Add mysql, mssql, clickhouse and sqlite when they take the fixtures as a dev-dependency [C].

0.6 CI

  • .github/workflows/build-linux.yml:157-198 already has one Docker job per package (mysql, mssql, clickhouse) running cargo nextest run -p <pkg> --run-ignored only [C].
  • Following the review amendment for W9-03 (no generated matrix), W2-13, W2-15 and W2-16 need no workflow edit. MariaDB runs inside the mysql package through MysqlFixture::start(MysqlFlavour::Mariadb) (crates/test-fixtures/src/mysql_flavour.rs:5-17).
  • SQLite tests are not Docker tests. They run in the fast job, which already installs libsqlite3-dev, clang and libclang-dev (build-linux.yml:75-78) [C].

W2-13 · MySQL and MariaDB driver

Status: not started. [C]

  • crates/drivers/mysql/src/lib.rs (713 lines) is still the v1 Connection.
  • Connect builds a sqlx Pool with max_connections(4), host and port, and Required/Disabled TLS only (lib.rs:37-57).
  • Workspace reads, metadata and transactions go through the pool (lib.rs:65-344).
  • There is no KILL QUERY, no editor session, no session statements and no engine-aware writes.

Pieces that already exist and W2-13 reuses [C]:

  • tablepro-net is already a dependency (mysql/Cargo.toml:14).
  • tablepro_net::tls::{mysql_ca_bundle_pem, client_identity_pem, classify} (crates/net/src/tls/mysql_bundle.rs:7-64, classify.rs:6-18).
  • The error-number table is mostly right: 1045, 1290|1792, 1317, 1205, 3024|1969, 1040 (lib.rs:579-597).
  • TIME through MySqlTime (lib.rs:474-482).
  • MysqlFixture and MysqlFlavour (test-fixtures).
  • The MySQL column-type parser crates/core/src/sql_syntax/mysql_column_type.rs.

Design after the amendments (review W2-13, FC-D2, FC-D6):

  • No PrivateSocketRelay. physical::connect_physical(target, route, timezone, liveness, cancel) builds MySqlConnectOptions from transport.route() as described in 0.2.
  • Options carried over from the spec [C APIs at sqlx-mysql-0.9.0/src/options/mod.rs]:
    • ssl_mode Disabled/Required/VerifyIdentity (:174)
    • ssl_ca_from_pem (:204), ssl_client_cert_from_pem / ssl_client_key_from_pem (:244/:284)
    • statement_cache_capacity(0) (:295)
    • pipes_as_concat(false) (:331), no_engine_substitution(false) (:366)
    • timezone (:387), set_names(false) (:413)
  • Keep W1-11's disable_statement_logging() (W1-11 owns it; per the plan's reconcile note at plan.md:1077).
  • MySqlConnection comes from MySqlConnection::connect_with inside a deadpool MysqlManager, not from sqlx Pool.
  • The connection id comes from SELECT CONNECTION_ID(), because the handshake id is pub(crate) [C protocol/connect/handshake.rs:20].
  • KILL QUERY runs on a lazily opened control connection over the same route.
  • Rows-matched semantics: CLIENT_FOUND_ROWS is set in 0.9.0 [C connection/stream.rs:38].

Remaining work

  • Create under crates/drivers/mysql/src/:
    • driver.rs, capabilities.rs, flavour.rs
    • physical.rs (route-based, no relay)
    • session_statements.rs (plus W3-19's workspace_session.rs sql_mode statement)
    • mysql_manager.rs, connection.rs, cancel.rs, table_engine.rs, write_batch.rs, editor_session.rs, catalog.rs, metadata.rs, errors.rs
    • W3-09 modules (see below)
  • Change lib.rs: drop the v1 body and re-export the driver.
  • Delete crates/drivers/mysql/tests/integration.rs (388 lines). Create tests/integration/{main.rs, flavour_suite.rs, tls.rs, row_limit.rs, editor_session.rs, non_transactional.rs}, plus W3-09's decode cases as tests/integration/decode.rs (reconcile plan.md:1078).
  • crates/drivers/mysql/Cargo.toml:
    • Add deadpool 0.13.1 (managed, rt_tokio_1), tokio (rt, time, sync, macros not needed), tokio-util 0.7 (rt) and uuid if session ids are needed.
    • Remove dev-dependency rust_decimal.
    • Add dev-dependencies tablepro-test-fixtures and tablepro-test-support (conformance).
    • sqlx features stay ["mysql", "mysql-rsa", "tls-rustls-aws-lc-rs"] (mysql/Cargo.toml:21).
  • linux/Cargo.toml: drop sqlx rust_decimal (shared with W2-12 and the app). Add deadpool.
  • deny.toml: add the mysql driver to the test-fixtures wrappers.
  • No build-linux.yml edit (0.6).

Dependency changes (exact): add deadpool 0.13.1. Remove rust_decimal (dev). sqlx stays 0.9.0, because there is no 0.9.x release with #4336. tablepro-net is already present.

Tests required (spec plus amendments):

  • Unit:
    • flavour::detects_mariadb_from_version_string
    • session_statements::mysql_and_mariadb_text
    • table_engine::transactions_column_mapping
    • errors::code_table_per_flavour
    • capabilities_match_driver_contract
  • flavour_suite.rs on mysql:8.4 and mariadb:11.4: the 14 cases in the spec (details:7954-7968), including the conformance scenarios with MySqlEngine.
  • non_transactional.rs: 5 cases.
  • row_limit.rs: 6 cases.
  • editor_session.rs: 3 cases.
  • tls.rs:
    • verify_identity_through_relay_with_server_name becomes verify_identity_through_ssh_socket_with_server_name (review: point at the ssh socket, using OpenSshFixture).
    • unknown_issuer_classified
    • disabled_against_require_secure_transport
    • added by the review: disabled_mode_uses_rsa_not_cleartext and tls_upgrade_happens_over_socket, both against the OpenSSH forward socket.
  • Carried in from other tasks into this crate:
    • W1-11 mysql_emits_no_sqlx_query_events
    • W3-19 workspace_session_strips_no_backslash_escapes and non_sqlite_drivers_reject_suspension
    • W3-18 fetch_table_definition_*
    • W9-16 name read_only_insert_and_create_1792
    • existing coverage caching_sha2_without_tls_uses_rsa (current tests/integration.rs:60)

Depends on:

  • W2-11 (trait, ConnectOptions v2; missing)
  • W2-10 (done in its amended form)
  • W3-06 and W3-07 (dialect; EngineDialect in core)
  • W3-08 (catalog classification)
  • W3-18 (fetch_table_definition)
  • W3-19 (sql_mode, ForeignKeyMode)
  • W1-11 (statement logging; SSH transport wired into the app)
  • W9-07 and W9-08 (conformance)
  • W4-06 (app switch)

Spec text amended or stale:

  • Every relay line is removed by the review: "One PrivateSocketRelay per MysqlConnection", socket(relay.socket_path()), "control connection through the relay", and the API tablepro-net PrivateSocketRelay. It becomes route()-based options.
  • "replaces mysql/src/lib.rs:15-468": the file is now 713 lines.
  • "sqlx sets CLIENT_FOUND_ROWS (verified in 0.8.6 stream.rs:46)": now 0.9.0 stream.rs:38.
  • FC-D3 (take the 0.9.x patch with #4336) is unmeetable. Record a dated watch-list entry.
  • W1-11 lists crates/drivers/mysql/src/connect_options.rs while W2-13 names physical.rs. Pick one file.
  • The spec's tests/integration.rs (deleted) conflicts with W3-09 and W3-11 editing that file. The reconcile (plan.md:1078) moves those cases into tests/integration/.

Size: about 4,300 lines added and 1,100 deleted, about 5,400 changed. That is roughly 2,600 src and 1,700 tests, not counting W3-09's roughly 600.


W2-14 · SQLite driver

Status: not started. [C]

  • crates/drivers/sqlite/src/lib.rs (785 lines) builds a URL string, maps :memory: (lib.rs:45-49) and sets create_if_missing(true) (lib.rs:53). It uses a 4-connection pool (lib.rs:54-57).
  • Indexes come from index_list / index_info only, with no expressions, DESC or predicates (lib.rs:248-307).
  • CANTOPEN always becomes FileAccessDenied (lib.rs:539-541).
  • There is no interrupt, editor session, BEGIN IMMEDIATE or atomic create.
  • libsqlite3-sys is a dev-dependency only (sqlite/Cargo.toml:21-22).
  • A partial W3-09 is in place (storage-class decode, lib.rs:387-419).

Design after the amendments (review W2-14, FC-D16, FD-D11):

  • Opening: SqliteConnectOptions::new().filename(..).create_if_missing(false).read_only(..).busy_timeout(5 s).foreign_keys(true).pragma("trusted_schema","OFF") plus W1-11's disable_statement_logging(). APIs [C sqlx-sqlite-0.9.0/src/options/mod.rs:226,240,294,303,323,358].
  • One workspace connection: SqlitePoolOptions::max_connections(1). after_connect installs set_progress_handler(1000, move || !flag.load(Relaxed)); returning false interrupts [C connection/mod.rs:386-399]. before_acquire rejects abandoned connections [C sqlx-core-0.9.0/src/pool/options.rs:380,435]. The probe uses try_acquire [C pool/mod.rs:366]. Discard uses close_on_drop [C pool/connection.rs:97].
  • Cancellation goes through the progress handler only. lock_handle waits for the worker to unlock [C connection/mod.rs:199-203], so the text "sqlite3_interrupt through lock_handle" is deleted (review).
  • Transaction state: one interim unsafe call, sqlite3_get_autocommit(handle.as_raw_handle().as_ptr()). as_raw_handle is [C connection/mod.rs:371]. sqlx keeps in_transaction pub(crate) [C connection/handle.rs:85]. File the upstream accessor PR. Name rusqlite 0.40 with deadpool and deadpool-sync as the fallback (review).
  • Index SQL: sqlparser 0.63 CreateIndex.columns / .predicate with Spanned source slices, falling back to the raw text when a span is empty. CreateIndex is at [C sqlparser-0.63.0/src/ast/ddl.rs:2819-2847]; Spanned at ast/mod.rs:152, spans.rs:695,727. Spans are line and column, so they need a line/col to byte conversion [I]. The hand-rolled depth-0 splitter in the spec is replaced.

Remaining work

  • Create under crates/drivers/sqlite/src/:
    • driver.rs, capabilities.rs, open.rs, create_file.rs, connection.rs, interrupt.rs, write_batch.rs
    • editor_session.rs, autocommit.rs, catalog.rs, metadata.rs, index_sql.rs (sqlparser)
    • errors.rs, plus W3-19's foreign_key_verification.rs and W3-18's catalog/{table_definition, dependents}.rs
  • Change lib.rs.
  • Tests: create tests/integration/{main.rs, open_semantics.rs, sessions.rs, metadata.rs} and W3-09 tests/decode.rs.
  • Port tests/values.rs (uses ":memory:", values.rs:5-11) and tests/csv_import.rs (csv_import.rs:13-19) to tempfile FileEndpoints on the v2 API. :memory: stops working.
  • Keep tests/script_plan_matches_sqlite3_complete.rs (no driver API).
  • Cargo:
    • Move libsqlite3-sys to [dependencies] with default-features = false, using the workspace range ">=0.30.1, <0.38.0" (linux/Cargo.toml:47). It already equals sqlx-sqlite 0.9.0's range [C sqlx-sqlite-0.9.0/Cargo.toml:141-142]. Widen it in lockstep with sqlx, and do not pin ~0.9.0.
    • Add uuid (workspace 1.x, v4) for temporary names, sqlparser (workspace 0.63) and tokio-util (rt).
    • The spec's "rust_decimal and serde_json are removed" is already true: neither is in sqlite/Cargo.toml.

Dependency changes (exact): libsqlite3-sys >=0.30.1, <0.38.0 (locked 0.37.0, Cargo.lock:2751) becomes a normal dependency. Add sqlparser 0.63.0 (already locked). Add uuid 1 (already locked 1.26.1).

Tests required:

  • Unit:
    • index_sql::key_list_splits_depth_zero_with_quotes_brackets_comments, renamed to fit the parser-based implementation; keep the same fixtures
    • index_sql::where_tail_is_predicate
    • errors::cantopen_distinguishes_missing_and_denied
    • open::options_never_create_for_open_existing
    • an empty-span fallback case (review)
  • open_semantics.rs: 9 cases (details:8075-8084).
  • sessions.rs: 6 cases plus conformance with SqliteEngine.
  • metadata.rs: expression_desc_partial_index_keys, views_listed_and_attached_schema_listed, rowid_identity_for_keyless_table, and the auto-increment cases from lib.rs:701-775.
  • Carried in:
    • W1-11 sqlite_emits_no_sqlx_query_events
    • W3-19 write_batch_foreign_keys.rs (4 cases)
    • W3-18 dependents_and_source_are_read
    • W9-16 name read_only_insert_is_read_only
  • All of these run without Docker in the fast job.

Depends on:

  • W2-11 (missing pieces)
  • W3-06 (present)
  • W3-18 (dependents, TableDefinition)
  • W3-19 (BEGIN IMMEDIATE, FK suspension in the same write_batch)
  • W1-11 (statement logging)
  • W9-07 (conformance)
  • W4-06 (consumes FileEndpoint::for_reopen)
  • W2-17 and W2-18 (UI; downstream)

Spec text amended or stale:

  • "replaces lib.rs:15-481 and tests 483-650": now 785 lines, with tests at lib.rs:618-785.
  • The index_sql depth-0 splitter becomes sqlparser CreateIndex with spans (review).
  • "W2-14 deletes W9-01's tests/integration.rs": that file does not exist in this crate. The crate has values.rs, csv_import.rs and script_plan_matches_sqlite3_complete.rs, and the spec does not mention them.
  • FD-D11: every build of storage and sqlite needs libclang (sqlx-sqlite-0.9.0/Cargo.toml:83 unbundled = buildtime_bindgen).
  • W1-11 names sqlite/src/connect_options.rs while W2-14 names open.rs. Pick one.

Size: about 3,000 lines added and 800 deleted, about 3,800 changed. That is about 1,900 src and 1,100 tests, plus about 250 for porting values.rs and csv_import.rs.


W2-15 · SQL Server driver

Status: not started on v2. The dependency prerequisites are partly done:

  • W8-10 pinned tiberius to tiberius-rs c1d2e741 (linux/Cargo.toml:96-102, commit c3af59a01) [C].
  • W8-11's kerberos feature exists (mssql/Cargo.toml [features] kerberos = ["tiberius/integrated-auth-gssapi"]) [C]. W8-11's tests/krb5_profile.rs does not exist: the crates/drivers/mssql/tests/ directory holds only integration.rs [C].

The current driver has these defects, and W2-15 removes them [C]:

  • config.trust_cert() is unconditional (lib.rs:118), so the certificate is never validated, even with TLS on.
  • EncryptionLevel::Off encrypts the login only (lib.rs:113-117).
  • One Mutex<Client> (lib.rs:157).
  • Every login, password included, goes through spawn_blocking + block_on (lib.rs:65-66).
  • A fixed 5 s CONNECT_TIMEOUT maps to Transport Timeout (lib.rs:28, 73-81).
  • It dials its own TcpStream (lib.rs:147-150).
  • It stops at the second result set (lib.rs:418-424).
  • DateTimeOffset decodes through DateTime<Utc>, which loses the offset (lib.rs:509-513). That breaks datetimeoffset_plus_0530_round_trip_keeps_offset.
  • Routing becomes Protocol (lib.rs:759) and TLS becomes TlsFailure::Other (lib.rs:752-755).
  • A typed NULL is always nvarchar (lib.rs:462).

Under the pinned rev c1d2e74 there is no cancel_query [C _build/cargo-home/git/checkouts/tiberius-*/c1d2e74/src/client.rs:72,121,180,232]. trust_cert_ca replaces the roots with exactly one CA [C src/client/tls_stream/rustls_tls_stream.rs:123-130,148-152], despite its doc comment (src/client/config.rs:143-145).

Design after the amendments (review W2-04/05/06/07/08 deleted, W2-15, W8-10, FB-D16), updated for tiberius 0.13.0:

  • Depend on crates.io tiberius = "0.13" with default-features = false and features ["tds73", "rustls", "chrono"]. The kerberos feature maps to tiberius/integrated-auth-gssapi, which pulls libgssapi ^0.11. Delete [patch.crates-io], and empty allow-git in deny.toml:58. This applies the review's "drop the patch when crates.io 0.13 ships". Check with cargo deny that ring, native-tls and openssl-sys stay out [I: the provider fallback is aws-lc-rs, and native-tls is off with no defaults].
  • Login: transport.open(), then compat_write(), then Client::connect under the login timeout, raced with cancel.
  • Config:
    • host(service host), port, database, application_name("TablePro")
    • encryption NotSupported for Disable and Required otherwise
    • command_timeout(None): the 30 s default desyncs any statement longer than 30 s
    • handshake_timeout(Some(login_timeout)), or None plus the outer timeout
    • readonly is never called
  • Trust (review interim, now native upstream):
    • VerifyFull: default system roots plus trust_cert_ca(path) (additive in 0.13), plus hostname_in_certificate for the server-name override.
    • Require: trust_cert().
    • Typed TLS failures come from classifying Error::Tls(String) text ("coarser classification", per the review).
    • The spec's ObservedServerVerifier and tls_client_config(...) are not implementable. Keep an upstream PR for tls_client_config on the watch list.
  • Kerberos only: spawn_blocking(move || handle.block_on(Client::connect(..))), selected against cancel and login_timeout. Password logins run on the async workers.
  • Routing: the first Error::Routing reruns once with transport.reroute. A second one gives Protocol.
  • MONEY: there is no money_as_numeric in 0.13.0, so MONEY columns are projected as CAST(.. AS decimal(19,4)) (SMALLMONEY as decimal(10,4)) by the SQL Server dialect projection. Editor MONEY keeps F64 and carries an "approximate" label.
  • Cancel: in-band. Drop the request future (which ends the borrow), then call client.cancel_query() under cancel_grace, then IF @@TRANCOUNT > 0 ROLLBACK. If cancel_query errors (the poisoned flag is set during a write) or the connection is desynced, discard the connection (review: "discard poisoned connections").
  • MssqlManager on deadpool: recycle runs SELECT @@TRANCOUNT under probe_timeout. The probe uses timeout_get with wait ZERO, then falls back to a standalone login.

Remaining work

  • Create under crates/drivers/mssql/src/:
    • driver.rs, capabilities.rs (cfg-selected AUTH_MODES)
    • login.rs
    • tls_config.rs (trust mapping and text classification instead of the observed verifier)
    • mssql_manager.rs, connection.rs
    • in_band_request.rs (uses cancel_query), cancel.rs, read_only_guard.rs, write_batch.rs, editor_session.rs
    • catalog.rs (moves format_mssql_type / map_referential_action and their tests from lib.rs:606-681)
    • metadata.rs, decode.rs, encode.rs, errors.rs
  • Change lib.rs.
  • Delete tests/integration.rs (444 lines). Create tests/integration/{main, decode, tls, cancellation, read_only, sessions, write_batch}.rs.
  • Cargo:
    • Add deadpool 0.13.1 and tokio-util (compat, rt). Add tablepro-net only if classify is reused.
    • Remove dev-dependency rust_decimal. Add dev-dependencies tablepro-test-fixtures (MssqlFixture exists at crates/test-fixtures/src/mssql_fixture.rs:15-83) and tablepro-test-support.
  • linux/Cargo.toml lines 39 and 96-102, and deny.toml:58.
  • No build-linux.yml edit.
  • The review's hand-rolled list also names normalize_mssql_default (lib.rs:631-669), to be replaced by sqlparser MsSqlDialect::parse_expr. That sits with W3-08's catalog classification, but this rewrite is where it moves.

Dependency changes (exact):

  • tiberius goes from git c1d2e741 (0.12.3) to crates.io 0.13.0, without rust_decimal, without [patch], without allow-git.
  • libgssapi moves from 0.8.3 (Cargo.lock:2713) to 0.11.x through the feature. cargo tree -d must show a single GSSAPI binding.
  • Add deadpool 0.13.1.

Tests required:

  • Unit:
    • login::routing_followed_once_then_protocol_error
    • login::config_never_sets_readonly_and_uses_routed_host
    • read_only_guard::wraps_statement_in_begin_rollback
    • errors::tds_number_table
    • the TLS text classification table (replaces the observation-based table)
    • capabilities::auth_modes_follow_kerberos_feature
    • the catalog formatting tests moved from lib.rs:913-955
    • new: login::command_timeout_disabled
  • decode.rs: 7 cases. money_max_min_exact and smallmoney now go through the CAST projection.
  • tls.rs:
    • verify_full_with_ca_ok
    • without_ca_unknown_issuer
    • disable_against_forced_encryption_server_requires_tls
    • name_mismatch_classified
  • cancellation.rs: 3 cases plus conformance with MssqlEngine.
  • read_only.rs: 2 cases. sessions.rs: 4 cases. write_batch.rs: 3 cases.
  • Recommended new cases:
    • waitfor_40s_completes_without_command_desync
    • cancel_after_dropped_row_stream_discards_client
  • krb5_profile.rs: the spec says it "stays", but it does not exist. W8-11 still owes it.
  • All of these are CI-only: there is no arm64 SQL Server image.

Depends on:

  • W2-11
  • W3-06 and W3-07 (MSSQL dialect, including the MONEY CAST projection and COUNT_BIG)
  • W3-08 and W3-18 (default_constraint_name)
  • W3-10 (static default drop)
  • W8-10 (re-scoped: move to crates.io 0.13.0)
  • W8-11 (krb5_profile test missing)
  • W9-07, W9-08, W4-06
  • The review deletes W2-04 through W2-08; their work is upstream.

Spec text amended or stale:

  • The title "on the tiberius fork" and the API line "tiberius fork (c1d2e741 + W2-04..08)".
  • Client::cancel_pending becomes Client::cancel_query.
  • Config::money_as_numeric(true) does not exist; use the CAST interim.
  • tls_client_config(tls_config::observed(...)), ObservedServerVerifier, and the "observation Passed/Failed" classification are not implementable.
  • "The W2-04 matrix now returns that Error::Tls instead of panicking" (now upstream).
  • "replaces mssql/src/lib.rs:19-728 including Mutex (lib.rs:141-143) and CONNECT_TIMEOUT (lib.rs:68-72)": now lib.rs:157 and 28/73-81.
  • "krb5_profile.rs stays": the file is absent.
  • New requirement not in the spec: command_timeout(None).

Size: about 4,500 lines added and 1,400 deleted, about 5,900 changed. That is about 2,700 src and 1,800 tests.


W2-16 · ClickHouse driver

Status: not started on v2. [C]

  • Connect uses clickhouse::Client::default().with_url(..), the crate's own HttpConnector with crate TLS, so there is no transport injection and SSH is impossible (lib.rs:53-91).
  • Editor and workspace reads run client.query(&escape_bind_markers(sql)) (lib.rs:402-406, 787-789) through a hand-rolled LineReader (lib.rs:356-396).
  • Types are parsed with base_type / type_is_nullable (lib.rs:459-481) and decoded by json_to_value (lib.rs:483-534).
  • Literals are inlined by bind_placeholders / literal (lib.rs:612-764).
  • Errors are classified by string matching (network_error, lib.rs:817-828).
  • There is no query_id, KILL, write ledger or commit resolution.

Reusable pieces [C]:

  • clickhouse_code (lib.rs:796-800) and the code table (lib.rs:830-846)
  • ClickHouseFixture with HTTP and HTTPS ports and a PKI (crates/test-fixtures/src/clickhouse_fixture.rs:89-109)
  • CommitToken::ClickHouseQuery
  • the backslash-escaping identifier quoting from W3-01 (commit 320e65dfb, crates/core/src/sql_dialect.rs:28-40)

Design after the amendments (review W2-16, FC-D5, D12):

  • client_factory::base_client:
    • hyper_util::client::legacy::Client::builder(TokioExecutor::new()).pool_idle_timeout(2 s).build(HttpsConnectorBuilder::new().with_tls_config(cfg).https_or_http().enable_http1().wrap_connector(connector)). APIs [C hyper-rustls-0.27.9/src/connector/builder.rs:60,208,252,359].
    • The TLS config comes from tablepro_net::tls::build_client_config (crates/net/src/tls/client_config.rs:24-52): rustls-platform-verifier new_with_extra_roots, with Require unverified.
    • Then clickhouse::Client::with_http_client(http).with_url(https?://<service host>:<port>), plus user, password and database, and with_setting for mutations_sync=1, readonly=2, allow_ddl=0 [C clickhouse-0.15.2/src/lib.rs:152,185,382].
    • Add with_product_info.
  • Connector: one TransportConnector: Service<Uri> whose call() runs transport.open() under connect_timeout. Its IO type is either a newtype over TokioIo<Box<dyn TransportStream>> implementing Connection (spec), or, matching the review's wording, an SSH path that returns TokioIo<UnixStream> (Connection comes free, at hyper-util http.rs:583,598).
    • Recommendation [I]: use the single transport-backed connector. It covers both DirectTcp (socket2 keepalive) and the SSH Unix socket with one code path of about 60 lines, and it is the "Unix-socket connector for SSH forwards" the review asks for. Avoid a second hyper-util HttpConnector path.
  • Editor SQL uses Client::query_raw [C lib.rs:606]. escape_bind_markers is deleted.
  • KILL QUERY keeps Client::query("KILL QUERY WHERE query_id = ? ASYNC").bind(id.to_string()) on a sessionless control client. It is not rendered by the dialect literal, per the review and the fact-check correction at review:553.
  • query_id uses Query::with_setting("query_id", uuid) [C query.rs:336; lib.rs:783 settings::QUERY_ID = "query_id"].
  • Reads use fetch_bytes("JSONCompactEachRowWithNamesAndTypes") and tokio::io::AsyncBufReadExt::read_until(b'\n') on the BytesCursor (AsyncBufRead, [C cursors/bytes.rs:193]). LineReader is deleted.
  • Header types go through clickhouse_types::DataTypeNode::new(&str) [C clickhouse-types-0.1.3/src/data_types.rs:118]. base_type, type_is_nullable and unwrap_type are deleted.
  • Row identity and sorting keys use the system.columns key flags plus sqlparser. split_key_expression (lib.rs:563-589) is deleted.
  • Network errors are classified by walking source() and io::Error::get_ref (tablepro_net::tls::classify already walks the chain, classify.rs:6-18). That replaces string matching.
  • fetch_native is recorded as rejected (no Decimal, Date family, Enum or Int256).
  • lz4: decide explicitly. The workspace is default-features = false, so lz4 is off. With lz4 on, fetch_bytes would request compressed responses [C query.rs:258, compression/mod.rs:40-45]. Recommendation [I]: keep lz4 off, and record it in the ADR. JSON over HTTP with keep-alive is CPU-cheap, and lz4 adds lz4_flex and cityhash.
  • App SQL binding: bind_placeholders and literal are deleted, but the core ClickHouse dialect still renders ? placeholders (crates/core/src/sql_dialect.rs:41-47, dialect/engine_dialect.rs:96-110). The review amendment for W3-07 and W3-11 moves app-built values to server-side {pN:Type} params, so the driver sends query_raw(sql).param("pN", value) [C query.rs:357; None becomes \N per sql/ser.rs:539]. That is a cross-lane dependency on W3-07.
  • Writes, resolve_commit and the editor session follow the spec unchanged: probe-first, own query_id per statement, wait_end_of_query=1, max_execution_time=0, the write_error_class table, and system.query_log / system.mutations resolution.

Remaining work

  • Create under crates/drivers/clickhouse/src/:
    • driver.rs, capabilities.rs
    • transport_connector.rs and transport_io.rs (or merged into one file)
    • client_factory.rs, connection.rs
    • row_seed.rs, cell_seed.rs (FB-D12)
    • cancel.rs, write_batch.rs, write_error_class.rs, commit_resolution.rs, editor_session.rs, catalog.rs, metadata.rs, errors.rs
    • line_reader.rs shrinks to a small read_until helper, or goes away.
  • Change lib.rs.
  • Delete tests/integration.rs (380 lines). Create tests/integration/{main, keys_and_decimals, writes, tls, sessions}.rs.
  • Cargo:
    • clickhouse with default-features = false and features ["uuid"], plus chrono only if typed Row structs keep chrono fields. Remove rustls-tls-aws-lc and rustls-tls-webpki-roots from linux/Cargo.toml:37.
    • Add clickhouse-types = "0.1.3", hyper-rustls (workspace 0.27, aws-lc-rs/http1/tls12), hyper-util (workspace 0.1, client-legacy/http1/tokio), tower-service 0.3, http 1, tablepro-net, tokio-util, and sqlparser (workspace) for key expressions.
    • Keep serde_json (it backs the JSON cell seeds).
    • Dev-dependencies: tablepro-test-fixtures, tablepro-test-support.
  • deny.toml wrappers.
  • No build-linux.yml edit.

Dependency changes (exact):

  • Add clickhouse-types 0.1.3 directly, plus hyper-rustls 0.27.9, hyper-util 0.1.20, tower-service 0.3.3 and http 1 as normal dependencies. All are already in Cargo.lock.
  • The clickhouse features drop TLS. That removes the webpki-roots edge [I].

Tests required:

  • Unit:
    • errors::code_prefix_parsing
    • cell_seed::invalid_utf8_string_is_bytes
    • row_seed::dynamic_column_retry_isolates_invalid_cell
    • write_batch::affected_exactly_is_guard_unsupported_without_network
    • write_error_class::unknown_versus_refused_codes
    • commit_resolution::mapping_table
    • transport_connector::timeout_maps_connect_phase
    • transport_connector::future_is_unpin_send
    • new: network_error_classified_by_source
  • keys_and_decimals.rs: 4 cases. writes.rs: 10 cases, including read_only_insert_164.
  • tls.rs: https_verify_full_ok, name_mismatch, http_port_with_https_server_refused.
  • sessions.rs: 4 cases plus conformance with ClickHouseEngine.
  • Recommended new case: connects_through_ssh_forward_socket using OpenSshFixture. It proves the review's Unix connector.

Depends on:

  • W2-11
  • W3-01 (done, commit 320e65dfb)
  • W3-06 and W3-07 (server-side param placeholders)
  • W3-08 and W3-18 (sorting and partition key flags, SHOW CREATE TABLE)
  • W9-07, W9-08, W4-06
  • Later: W5-14 edits keys_and_decimals.rs.

Spec text amended or stale:

  • "deleting bind_placeholders (598-712), literal (714-741), json_to_value (476-520), map_clickhouse_error (772-800)": now 612-726, 728-764, 483-534, 802-846.
  • "escape_bind_markers stays" is reversed by query_raw, so it is deleted.
  • "Crate TLS features stay off" contradicts the current workspace manifest, which has to change.
  • "KILL QUERY ... with the UUID rendered by the ClickHouse dialect's string literal function" becomes client-side bind (review).
  • "clickhouse-types 0.1" / "0.1.2 DataTypeNode::new" become 0.1.3.
  • "fetch_bytes ... feeds LineReader" becomes read_until.
  • The spec's TransportConnector / TransportIo against the review's "~15-line Service returning TokioIo" needs reconciling (recommendation above).

Size: about 4,200 lines added and 1,500 deleted, about 5,700 changed. That is about 2,600 src and 1,600 tests.


W3-09 · Lossless decode and encode (MySQL and SQLite parts only)

Status: partly done. Commit e861c993b ("read every value in the form the server sent it") added the following [C]:

  • MySQL (crates/drivers/mysql/src/lib.rs:374-482):
    • three-way Null / value / Undecodable (decode, decode_text, decode_bytes)
    • DECIMAL from text into SqlDecimal (:389)
    • TIME into SqlTime (:393, :474-482)
    • BIT into Bits (:409-412)
    • JSON into JsonText (:404-406)
    • FLOAT into Float32 (:384)
    • unsigned types into UInt (:382-383)
  • SQLite: decode by runtime storage class (crates/drivers/sqlite/src/lib.rs:387-419).
  • The core Value, SqlDecimal::from_scaled_i128 and ParamType exist (W3-05) [C].

Gaps against the spec:

  • MySQL [C]:
    • There is no decode_cell(MySqlValueRef, MySqlWireFormat, MySqlTimestampMode). MySqlValueRef::format() is pub(crate) [C sqlx-mysql-0.9.0/src/value.rs:36], so the driver passes the format in.
    • Integer signedness is decided by type-name text, not <u64 as Type<MySql>>::compatible. That function checks the UNSIGNED flag [C type_info.rs:100-101] and covers Year [C types/uint.rs:19-27].
    • Narrow unsigned and YEAR decode to UInt (lib.rs:382-383). The spec says Int through i64::try_from.
    • BOOLEAN decodes to Bool (lib.rs:378). The spec says Int, so a stored 2 stays 2.
    • The string family is chosen by type name (lib.rs:413-416), so a VARCHAR ... COLLATE utf8mb4_bin column, which sqlx names VARBINARY, decodes as Bytes. The spec uses <str as Type<MySql>>::compatible, which is false only for Collation::BINARY [C types/str.rs:25-43].
    • DATE, DATETIME and TIMESTAMP use the chrono decoders. A zero date becomes Undecodable; the spec says Other. There is no mysql_temporal binary-length parser (0/4/7/11).
    • TIMESTAMP is always UTC (:397-401). There is no SessionLocal mode for editor sessions.
    • Encoding binds by Value, not by ParamType (lib.rs:484-511).
  • SQLite [C]:
    • Decode adds declared-type refinements: BOOLEAN 0/1 becomes Bool, and DATE, TIME and DATETIME text is parsed (lib.rs:397-417). The spec says "decode by runtime storage class through type_info().name()" (see open question Q4).
    • Encoding binds by Value (lib.rs:476-508). The spec binds by ParamType, or by Value for dynamic columns.

Remaining work

  • Create:
    • crates/drivers/mysql/src/{decode.rs, mysql_wire_format.rs, mysql_timestamp_mode.rs, mysql_temporal.rs, encode.rs}
    • crates/drivers/sqlite/src/{decode.rs, encode.rs}
    • crates/drivers/sqlite/tests/decode.rs
    • MySQL integration cases inside crates/drivers/mysql/tests/integration/ (not the deleted tests/integration.rs; reconcile plan.md:1078)
  • Delete extract_value and bind_*_params in both drivers.
  • No new dependencies. sqlx 0.9.0 and chrono are already present.

Tests required:

  • MySQL unit:
    • mysql_temporal_binary_lengths_0_4_7_11
    • mysql_zero_date_is_other_in_both_formats
    • narrow_unsigned_converts_to_int_checked
  • MySQL integration (8.4 and MariaDB 11.4):
    • unsigned_max_values_decode_in_both_protocols
    • int_unsigned_max_id_row_edits_through_plan_save
    • bin_collation_text_decodes_as_text
    • decoded_variants_match_catalog_kinds
    • DECIMAL(65,30), BIT(10), YEAR, GEOMETRY, TIME -838:59:59, tinyint(1)=2, zero date gives Other
    • bigint_unsigned_max_id_edit_affects_one_row (the name the W9-16 regression map cites)
  • SQLite integration (sqlite/tests/decode.rs):
    • count_sum_and_expression_columns_are_typed
    • decimal_declared_column_decodes_storage_class
    • strict_table_round_trip
  • Existing tests to update:
    • crates/drivers/mysql/tests/integration.rs:280 wire_only_types_read_back and :110 value_roundtrip_all_types (they use rust_decimal)
    • crates/drivers/sqlite/tests/values.rs:40-74 (asserts Bool and Date; changes if Q4 resolves to pure storage class)

Depends on:

  • W3-05 (done)
  • W3-06 (ParamType per column; present in core engine_dialect.rs)
  • W3-08 (catalog kinds, for decoded_variants_match_catalog_kinds)
  • W8-12 (done, commit 19b8f4e49)
  • W2-12 (the PG part, outside this lane)
  • W2-13 and W2-14 own the files these modules land in.

Spec text amended or stale:

  • The file list says crates/drivers/mysql/tests/integration.rs is edited, but W2-13 deletes it. Use tests/integration/decode.rs.
  • The PG amendments (FB-D5, postgres-protocol decoders) do not touch this lane.

Size: about 1,300 lines changed (about 700 src and 600 tests). These lines live inside W2-13 and W2-14's files, so do not add them twice to the totals.


Implementation order

Prerequisite from other lanes: W2-11 must put the Connection v2 trait, DatabaseDriver v2, ConnectOptions v2 (target, credentials, workspace settings, environment) and APPLICATION_NAME into core first. Add W3-18's TableDefinition, W3-19's ForeignKeyMode and W9-07's conformance skeleton too, so every driver compiles against the final trait once.

  1. Dependency groundwork commit (W8-10 re-scoped). Move tiberius to crates.io 0.13.0: default-features = false, ["tds73", "rustls", "chrono"], delete [patch.crates-io], empty allow-git. Adapt the v1 mssql driver minimally: command_timeout(None), and keep trust_cert behaviour for now. Add deadpool 0.13.1 to the workspace. Why first: SQL Server can only be tested in CI, and its upstream semantics (cancel, poisoning, timeouts) are the largest unknown, so CI feedback should start early.
  2. W2-14 + W3-09 SQLite. No network and no Docker. It exercises Connection v2, OwnedCalls (statement-scoped stop), WriteLedger (Transactional) and EditorSession end to end, with the fastest test loop. Port values.rs and csv_import.rs off :memory:.
  3. W2-13 + W3-09 MySQL. Docker is available locally (arm64 MySQL and MariaDB). It introduces the deadpool manager pattern (reused by MSSQL), connection-scoped KILL QUERY, and route-based SSH over the OpenSSH socket.
  4. W2-16 ClickHouse. Docker is available locally. It needs W3-07's server-side param placeholders. It adds the transport connector, the StatementByStatement ledger and commit resolution.
  5. W2-15 SQL Server, iterated in CI. It reuses the MySQL manager shape and the in-band variant of OwnedCalls.

Optional mitigation [I]: build each v2 driver additively in new modules beside the v1 lib.rs, unexported, and flip the exports in the single switch commit together with W4-06. That keeps the atomic commit reviewable without breaking the build between commits.

Open questions and risks

  • Q1 (blocker): core contract missing. None of the five tasks can compile against v2 until W2-11 lands the trait, DatabaseDriver::capabilities/connect(options, cancel) and ConnectOptions v2. W3-18 and W3-19 add trait methods and fields (fetch_table_definition, WriteBatch.foreign_keys). There is also a conflict: W2-11 has WriteBatch carrying table, which MySQL's engine lookup needs and core has today; W3-19's text omits table. Keep table.
  • Q2 (blocker for W2-16): ClickHouse parameter binding. The core dialect renders ? for ClickHouse (sql_dialect.rs:41-47), and the switch deletes the driver's literal inliner. W3-07 must render {pN:Type} server-side params, or the driver has no safe binding path.
  • Q3 (risk, W2-15): tiberius 0.13 cancellation semantics. The docs say "a result stream dropped mid-response cannot be recovered", but also that cancel_query drains to DONE_ATTN so the connection can be reused. The spec test pooled_query_bound_waitfor_deadline_server_cancelled_client_reused requires reuse. It likely holds for WAITFOR, where no rows are in flight; it may not hold mid-row-stream, which forces a discard. Unverified locally (CI only). Also: command_timeout defaults to 30 s and poisons the connection, so leaving it set breaks long editor queries.
  • Q4 (decision, W3-09 SQLite): declared-type refinements. Keep the BOOLEAN-to-Bool and DATE/DATETIME text parsing (current behaviour, asserted by tests/values.rs:67-71), or decode by storage class only as the spec says? Parsing DATETIME text and re-encoding it can change its spelling (for example T/Z forms), so a row-key predicate built from the decoded value may stop matching [I]. Recommend storage class only, and let the column kind present the value.
  • Q5 (decision, W2-16): connector shape. The spec has one transport-backed TransportConnector for both routes; the review has a Unix-socket connector for SSH. Recommend the single transport-backed connector. Also decide lz4: recommend off, recorded in the ADR.
  • R1 (W2-13): direct TCP on sqlx 0.9.0 has no TCP_NODELAY (#4336 is unreleased) and no keepalive (#4355). Possible Nagle latency, and dead peers are found only by the health probe. FC-D3 cannot be met. Record a dated watch item. Do not pin sqlx to git: deny.toml allows only tiberius.
  • R2 (W2-13): trust sets differ by engine. MySQL uses sqlx's webpki roots plus PEM. The corporate-CA PEM bundle (mysql_ca_bundle_pem) already exists, but the review keeps it only if system CAs are required. This needs a decision and documentation.
  • R3 (W2-14): unsafe interim. sqlite3_get_autocommit relies on as_raw_handle, which is semver-exempt in sqlx [C connection/mod.rs:360-370]. The direct libsqlite3-sys range must widen together with sqlx (main already allows <0.39). Every sqlite and storage build needs libclang.
  • R4 (W2-14): sqlparser spans are line/column, so expression keys need offset conversion. Fall back to raw text on empty spans. sqlparser breaks its API every 3 to 4 months.
  • R5 (W2-16): resolution heuristics. system.query_log may be disabled or inaccessible, which makes results Unresolvable. Matching system.mutations by time window is heuristic. Whether KILL QUERY accepts server-side params is unverified, so keep client-side bind.
  • R6 (W2-15): W8-11 is incomplete. tests/krb5_profile.rs was never added. libgssapi moves from 0.8 to 0.11 with tiberius 0.13. Check cargo tree -d and cargo deny (ring and native-tls bans) after the bump.
  • R7 (all): switch size. The four drivers plus W3-09 add up to about 20,000 changed lines (W2-13 about 5,400, W2-14 about 3,800, W2-15 about 5,900, W2-16 about 5,700; W3-09's about 1,300 is inside those). This is on top of the core and app lanes, in one atomic commit. SQL Server cannot be run on the developer machine (arm64).
  • R8 (W2-13 and W2-14): file names overlap with W1-11. connect_options.rs (W1-11) against physical.rs / open.rs (W2-13/W2-14). Settle the names before work starts so disable_statement_logging() has one home.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/app-ssh.md (part 1 of 2)

Local, gitignored analysis from the old machine. Save the body inside the block to that path (join the parts in order).

lanes/app-ssh.md (part 1 of 2)

Lane app-ssh: app and SSH tasks of Steps 30-39

Tasks: W1-11 W2-17 W4-06 W6-11 W6-12 W6-13 (Step 30), W4-07 W6-14 (31), W6-15 (32), W4-08 W4-09 (33), W4-10 (34), W5-01 (35), W4-11 (36), W4-12 (37), W5-02 (38), W5-03 W6-17 (39).

Baseline: the worktree HEAD is origin/linux 06794b85c plus three chore commits outside linux/ (git merge-base HEAD origin/linux = 06794b85c). Every path below is relative to linux/ unless it starts with .github/. "Confirmed" means I opened the file at that line. "Inferred" means a conclusion I drew and did not execute.

1. Findings that cut across the lane

  1. The core contract v2 does not exist yet, so every Step 30 app task is blocked on another lane. (confirmed)

    • crates/core/src/connection.rs:26-37 is still the v1 ConnectOptions { host, port, …, use_tls, service_endpoint }.
    • crates/core/src/connection.rs:65-118 is the v1 Connection trait (list_tables, fetch_rows, query, execute_in_transaction, ping, close).
    • crates/core/src/driver.rs:7-49 is the v1 DatabaseDriver (default_port, is_file_based, connect(ConnectOptions)). It has no capabilities().
    • crates/core/src/registry.rs still holds the v1 DriverRegistry.
    • What does exist, additively: DriverCapabilities (capabilities.rs:60-73), EditorSession (editor_session.rs:55-75), CallOptions, OwnedCalls, TlsConfig, NetworkEndpoint, FileEndpoint, LivenessPolicy, the lossless Value (value/mod.rs:28-51), DriverError::category (error.rs:132-152) and SshFailure::category (ssh_failure.rs:76).
    • All five drivers still impl DatabaseDriver v1 (crates/drivers/*/src/lib.rs).
    • W4-06, W1-11, W6-12, W6-13 and W2-17 cannot compile until W2-11 (core) and W2-12..W2-16 (drivers) land in the same stacked branch.
  2. The OpenSSH transport is finished, but nothing uses it. (confirmed)

    • The pieces: crates/ssh/src/session.rs (ControlMaster handshake with askpass and a paused deadline), forward.rs (SshSession::forward returns a Unix-socket Transport, plus reroute and explain_closed_stream), askpass_bridge.rs (peer_cred check, then supplied secret once, then CredentialInteraction::Attended(prompter)), services.rs (SshServices { runtime, ssh_program, ssh_keygen_program, askpass_program }), argv.rs (-J jump hosts, -i, PreferredAuthentications, forced -o options), known_hosts.rs (forget_host_key through ssh-keygen -R), sweep.rs, and src/bin/tablepro-askpass.rs.
    • Docker tests are in crates/ssh/tests/openssh_session.rs:131-287.
    • The app still imports tablepro_ssh::russh_tunnel in five files:
      • services/connection_service.rs:8
      • services/database_service.rs:8
      • services/connection_monitor.rs:7
      • ui/ssh_section.rs:5
      • ui/error_text.rs:4
  3. Removing russh needs a GTK credential prompter in Step 30, or first connections to a new SSH host regress. (confirmed + inferred)

    • Today's russh interim learns unknown host keys silently (TOFU): russh_tunnel.rs:170-176 calls learn_known_hosts_path.
    • With OpenSSH, the host-key question arrives as an askpass prompt. askpass_bridge.rs:245-248 answers Cancel when the interaction is Unattended and records the declined key.
    • No CredentialPrompter implementation exists in crates/app (grep for CredentialPrompter|CredentialInteraction in crates/app/src finds nothing).
    • Unless the GTK prompter from W1-12 (Step 42), meaning GtkCredentialPrompter plus an adw::AlertDialog presenter, is pulled into Step 30, every connection to a host missing from known_hosts fails with HostKeyUnknown. Keyboard-interactive also becomes impossible.
    • The problem statement also lists "nothing asks for a missing password" and "imported connections cannot be used". Only W1-12's Attended KeyringCredentials path fixes those, so W1-12 has to come forward anyway if this PR is to resolve them.
  4. Relm4 command futures do not run on the app runtime. (confirmed)

    • ComponentSender::command spawns through crate::spawn (relm4-0.11.0/src/channel/component.rs:85). That is Relm4's own lazy runtime with RELM_THREADS defaulting to 1 worker (relm4-0.11.0/src/lib.rs:182-189). Nothing in the app sets RELM_THREADS (grep finds nothing).
    • 25 .command( / oneshot_command( sites run driver futures there:
      • ui/app/browse.rs 3
      • ui/app/connection.rs 5
      • ui/app/structure.rs 5
      • ui/app/import.rs, row_ops.rs, status_pages.rs 1 each
      • ui/connect_dialog.rs 2
      • ui/editor/mod.rs 1
      • ui/history_dialog.rs 3
      • ui/saved_queries_dialog.rs 3
    • The clippy bans in crates/app/clippy.toml:104-106 cover relm4::spawn* but not ComponentSender::command.
    • Every rewrite in this lane replaces these sites with Tasks::spawn_task + glib::spawn_future_local.
  5. Real defect: the CSV import polls a sqlx future on the GTK thread. (confirmed code path; the panic is inferred from sqlx source)

    • ui/app/import.rs:58-66 calls connection.fetch_columns(..).await inside glib::spawn_future_local.
    • sqlx-core 0.9.0 rt::timeout panics with "this functionality requires a Tokio context" when no runtime is entered (sqlx-core-0.9.0/src/rt/mod.rs:34-51,164-169). Pool acquire goes through it (src/pool/inner.rs:254).
    • The GTK thread never enters a runtime; crates/app/clippy.toml:91-95 bans it.
    • W4-06's rewrite onto Session::run fixes this. It is also small enough to fix on its own before Step 30.
  6. Real defect: Format cannot be undone. (confirmed)

    • ui/editor/mod.rs:685 replaces the buffer with buffer.set_text.
    • The GTK 4 docs (https://docs.gtk.org/gtk4/method.TextBuffer.set_text.html) say set_text "is automatically marked as an irreversible action in the undo stack". The comment at mod.rs:682-684 ("Ctrl+Z puts the old text back") is wrong.
    • The same applies to ReplaceQuery (mod.rs:657), FileLoaded (mod.rs:550) and the file drop (mod.rs:411-425).
    • W6-04's replace_all_undoable does not exist anywhere (grep finds nothing), but W6-14's spec depends on it.
  7. The plan contradicts itself, and shipped code, on keyring-failure saves. (confirmed)

    • W4-06 persist_pending_save says: if any keyring write fails, no entry is saved (test keyring_failure_saves_no_entry).
    • W6-13 PendingSave says: persist, then show a toast per failed secret.
    • The shipped W6-09 code follows W6-13: ui/connect_dialog.rs:779-833 stores secrets first, records failures in SecretSaveReport, then writes the entry with has_passphrase=false.
    • That behaviour is pinned by tests/secret_save_flow.rs:97 (passphrase_store_failure_saves_has_passphrase_false_in_one_write).
    • Recommendation: keep the shipped behaviour and drop W4-06's keyring_failure_saves_no_entry and its toast text.
  8. The app package has no Docker CI job. (confirmed)

    • The integration matrix in .github/workflows/build-linux.yml:164-175 lists drivers, tablepro-ssh and tablepro-test-fixtures, not tablepro.
    • W4-06 (tests/session_postgres.rs) and W4-08 (tests/tracker_save_postgres.rs) need it, and the job then needs the GTK/libadwaita/sourceview -dev packages.
    • Alternative (open question 2): the Session stack is GTK-free. crates/app/src/session/*.rs imports no gtk/glib (grep), and the tablepro-session crate exists with only runtime/. Putting Session, OperationRunner, ConnectionLink, Connector and SchemaCatalog in crates/session keeps those tests in a GTK-free Docker job.
  9. async-channel is not in Cargo.lock (confirmed: no async-channel package). The review's "new (runtime channels)" amendment says to pick one channel family. tokio::sync is "runtime agnostic" (tokio-1.53.1/src/sync/mod.rs:440), so tokio::sync::mpsc/oneshot/watch can be received inside glib::spawn_future_local.

    • Recommendation: use tokio::sync everywhere the plan names async_channel: W6-12's ChannelResultSink, W4-11's WorkspaceStore writer and W1-12's prompter queue. That adds no dependency.
  10. The plan's spawner names are stale. (confirmed)

    • The plan names Arc<dyn TaskSpawner>, TokioSpawner, guarded_input, GuardedCommand and spawn_task.
    • The code has the concrete tablepro_session::runtime::Tasks (crates/session/src/runtime/tasks.rs:11-56: spawn_task, spawn_blocking_task, spawn_owned), TaskResult, TaskPanic and catch_panic (runtime/mod.rs).
    • None of TaskSpawner, GuardedCommand, WatchForwarder or ProcessHold exists (grep).
    • Specs should be read with Tasks in place of TaskSpawner. WatchForwarder and ProcessHold are new work inside W4-06.

2. Relm4 usage today, and which of my tasks rewrite each file

Counts come from grep over crates/app/src.

  • The "Command" column counts .command( and oneshot_command( sites.
  • "Imports only" means the file uses the relm4::{adw, gtk} re-exports and no Relm4 type.
  • relm4::spawn and spawn_local now have 0 uses (banned by crates/app/clippy.toml:104-106 since fdcb04d77). The review's 11 spawn, 3 spawn_local, 9 components and 2 FactoryVecDeque are stale.
File Lines Component impl ComponentSender FactorySender relm4::Sender FactoryVecDeque refs (sites) relm4::channel Command Rewritten by my tasks
lib.rs 182 RelmApp::from_app (99-111) no (W4-13 at Step 40, or the migration task)
services/change_tracker.rs 1093 2 (159, 187) W4-07, W4-08
ui/app/mod.rs 1772 App SimpleComponent (628-629) 3 1 (1335) 7 (2 sites: 971 sidebar, 1165 connections popover) edited by W4-06/07/09/10/11/12 and W6-17; the root stays Relm4 until W4-13/W4-14
ui/app/browse.rs 294 5 3 deleted by W4-10
ui/app/connection.rs 733 19 5 W4-06, W6-13, W6-17
ui/app/import.rs 231 4 1 W4-06 call sites only
ui/app/row_ops.rs 222 2 1 W4-06, W4-12, W5-02
ui/app/status_pages.rs 257 6 1 W4-06
ui/app/structure.rs 720 12 5 W4-06, W4-07, W4-10, W4-12
ui/app/workspace_tabs.rs 1410 17 W4-06/07/09/10/11/12, W6-12, W6-17
ui/browse_tab.rs 2985 BrowseTab SimpleComponent (1342) 5 1 2 (1493, 1776) yes: W4-10 rewrites it (also W4-06/07/08/12, W5-02/03)
ui/connect_dialog.rs 1261 ConnectDialog Component (161-162) 3 2 yes: W6-13 deletes and rebuilds it
ui/editor/mod.rs 1279 SqlEditor SimpleComponent (118-119) 4 3 1 (439) 1 yes: W6-12 rewrites it
ui/editor/statement_cursor.rs 122 imports only W6-15 (swap imports)
ui/grid.rs 2414 13 yes: W5-01 moves it, W5-02 replaces the senders
ui/filter_strip.rs 731 imports only W4-09 (swap imports)
ui/ssh_section.rs 242 imports only yes: W6-13
ui/structure_tab/mod.rs 1110 StructureTab SimpleComponent (495) 7 yes: W4-10 (also W4-09, W4-12)
ui/structure_tab/{columns,fks,indexes}.rs 418/68/65 2/2/2 converted with StructureTab in W4-10
ui/structure_tab_dialogs.rs 404 3 converted with StructureTab in W4-10
ui/history_dialog.rs 1086 HistoryDialog Component (91) 8 3 W4-06 changes one line (120); conversion is left to the migration task
ui/connection_row.rs 470 ConnectionRow FactoryComponent (58-59) 4 no
ui/sidebar_row.rs 318 SidebarRow FactoryComponent (55-56) 4 1 no (W4-10 feeds it, App owns the factory)
ui/welcome_view.rs 280 WelcomeView SimpleComponent (45) 2 4 (1 site: 61) no
ui/saved_queries_dialog.rs 646 SavedQueriesDialog Component (61) 9 3 no
ui/import_dialog.rs 483 ImportCsvDialog Component (83) 4 no
ui/export_dialog.rs, ui/connection_list.rs 440/251 imports only no

Totals: 30 files reference relm4. There are 11 component impls (9 components, 2 factories), 119 ComponentSender, 8 FactorySender, 21 relm4::Sender, 3 FactoryVecDeque sites, 3 relm4::channel and 25 command sites.

Folding plan (FA-D1 amendment):

  • SqlEditor (W6-12), ConnectDialog (W6-13), BrowseTab and StructureTab with its four sub-files (W4-10), and the grid (W5-01/W5-02) become glib::Object subclasses with #[derive(gtk::CompositeTemplate)] .ui files in data/resources/ui/, registered in data/resources/resources.gresource.xml.
  • ui/preferences_dialog/imp.rs:10-11 with data/resources/ui/preferences-dialog.ui is the in-tree precedent.
  • Output enums become signals. Each converted child emits signals, and App connects them and forwards into its Relm4 input sender until W4-13/W4-14 replace the root.
  • Enable gtk4 feature xml_validation (gtk4-0.11.4/Cargo.toml:171). The direct feature floors from the review are already declared (Cargo.toml:62-66).
  • The migration task still owns: lib.rs, the App root, welcome_view, connection_row, sidebar_row, connection_list, history_dialog, saved_queries_dialog, import_dialog, export_dialog and app/import.rs.

3. App files that consume v1 driver types (all confirmed by grep)

  • Deleted by W4-06:
    • services/database_service.rs (Connection, ConnectOptions, DatabaseDriver, SshTunnel, global instance())
    • services/connection_service.rs (DriverRegistry, ConnectOptions, Connection, ReadOnlyConnection, TableInfo, AuthMode, russh types)
    • services/connection_monitor.rs (Connection::ping, SshTunnel)
  • Composition root: lib.rs (DriverRegistry, build_registry at 120-128).
  • Global database_service::instance() call sites:
    • ui/app/browse.rs:49,128,159
    • ui/app/connection.rs:38,55,125,192,533
    • ui/app/import.rs:19,23,63,116
    • ui/app/ran_statements.rs:35
    • ui/app/row_ops.rs:29
    • ui/app/status_pages.rs:88,115,160
    • ui/app/structure.rs:125,304,486,574,672
    • ui/app/workspace_tabs.rs:423,971
    • ui/connect_dialog.rs:843-848
    • ui/editor/mod.rs:673,695,713,735
    • ui/history_dialog.rs:120
  • v1 methods:
    • execute_in_transaction / reports_rows_affected in ui/app/row_ops.rs
    • ddl_is_transactional, fetch_indexes, fetch_foreign_keys, execute in ui/app/structure.rs
    • fetch_rows, query_params, fetch_columns in ui/app/browse.rs
    • query in ui/editor/mod.rs:846
    • default_port(), is_file_based(), supports_integrated_auth(), display_name() in ui/connect_dialog.rs:607-625
  • v1 metadata types:
    • TableInfo: ui/app/{mod,connection,structure,workspace_tabs}.rs, ui/connect_dialog.rs, ui/sidebar_row.rs
    • IndexInfo and ForeignKeyInfo: ui/app/{mod,structure}.rs, ui/structure_tab/{mod,indexes,fks}.rs, ui/structure_tab_dialogs.rs
  • v1 SQL builders keyed by driver id:
    • sql_dialect::quote_ident / BuildSqlError in services/change_tracker.rs, ui/app/browse.rs, ui/error_text.rs:2
    • sql_ddl in services/structure_tracker.rs, ui/app/{mod,structure}.rs, ui/structure_tab/{mod,columns}.rs, ui/structure_tab_dialogs.rs
  • QueryResult produced by v1 drivers: ui/app/{browse,mod}.rs, ui/browse_tab.rs, ui/editor/mod.rs, ui/export_dialog.rs, ui/grid.rs.
  • SavedConnection.use_tls / host-port shape (W1-11's storage change):
    • services/secret_labels.rs, session/session_key.rs, ui/connection_list.rs, ui/connection_row.rs, ui/welcome_view.rs, ui/connect_dialog.rs, tests/secret_save_flow.rs
    • outside the app: crates/storage/src/connections.rs:22, crates/storage/tests/isolation.rs
  • Test doubles still on v1: crates/test-support/src/fake_connection.rs:6 and fake_driver.rs:29-42.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/app-ssh.md (part 2 of 2)

Local, gitignored analysis from the old machine. Save the body inside the block to that path (join the parts in order).

lanes/app-ssh.md (part 2 of 2)

4. Per task

W1-11 · SSH session transport, connect and credential shapes, keyring credentials, sqlx statement logging off (Step 30)

Status: partly done.

  • Done. The OpenSSH transport replaces the spec's russh design (finding 2). The core pieces are present:
    • SshFailure::category (crates/core/src/ssh_failure.rs:76-93), with test category_matrix at :106
    • TransportError::category delegating to SSH (transport_error.rs:25-31), with test :46
    • DriverError::category delegating to Transport (error.rs:134)
    • CredentialInteraction, CredentialPrompter, PromptPurpose (crates/core/src/credentials/*)
    • FakePrompter (crates/test-support/src/fake_prompter.rs)
  • Missing.
    • App wiring: no SshServices is built anywhere in the app, and there is no ConnectServices, KeyringCredentials or CredentialSource.
    • russh is still compiled: crates/ssh/src/russh_tunnel.rs (449 lines), lib.rs:12, crates/ssh/Cargo.toml:20, Cargo.toml:33.
    • SavedConnection.use_tls remains (crates/storage/src/connections.rs:22).
    • The drivers do not call disable_statement_logging. Only crates/storage/src/query_history/mod.rs:60 does.

Remaining work.

  • SSH crate.
    • Delete crates/ssh/src/russh_tunnel.rs and the pub mod russh_tunnel line (crates/ssh/src/lib.rs:12).
    • Drop russh.workspace = true (crates/ssh/Cargo.toml:20) and russh = "0.63" (Cargo.toml:33).
  • Supply chain and docs.
    • Rescope the RUSTSEC-2023-0071 reason in deny.toml:11 to Stage B: sqlx-mysql public-key encryption only.
    • Update docs/decisions/0009-crypto-provider-and-sources.md:20-21, docs/storage.md:52, and ROADMAP.md:7,100.
  • SSH tests, added to crates/ssh/tests/openssh_session.rs (amended set):
    • no_loopback_listener_while_tunnelled: no LISTEN rows for our inodes in /proc/self/net/tcp{,6}.
    • reroute_reuses_session: the same master_pid() after Transport::reroute.
    • cancel_during_handshake_returns_cancelled: expects SshFailure::InteractionCancelled.
    • keepalive_detects_black_hole: pause the sshd container; ServerAliveInterval 1 × 2; closed() fires within 4 s.
    • postgres_verify_full_through_ssh_transport_uses_service_name: belongs with W2-12 once PG takes a Transport.
  • App: new files in crates/app/src/session/.
    • connect_services.rs: ConnectServices { environment, ssh: tablepro_ssh::SshServices, liveness }. There is no trust/host_keys field (finding 2, FC-D8).
    • connector.rs and driver_connector.rs: wire SshSession::connect(config, services, liveness, interaction, cancel) (crates/ssh/src/session.rs:52-58) and SshSession::forward(target, liveness) (forward.rs:28-32).
    • credential_source.rs, credential_demand.rs, refreshed_request.rs, keyring_credentials.rs, no_stored_credentials.rs. KeyringCredentials holds Arc<dyn SecretVault>, which is what storage.rs/connect_dialog.rs:726 use today. The spec's SecretStore is stale.
  • App: lib.rs.
    • Build SshServices:
      • runtime: Arc::new(SshRuntimeCell::new(glib::user_runtime_dir()))
      • ssh_program: "ssh"
      • ssh_keygen_program: "ssh-keygen"
      • askpass_program: Path::new(config::LIBEXECDIR).join("tablepro-askpass") (crates/app/src/config.rs:5)
    • Call verify_askpass_helper() (crates/ssh/src/services.rs:50).
    • Spawn sweep_stale_masters on Tasks at startup.
  • App: GTK prompter pulled from W1-12 (finding 3). New ui/credential_prompt_dialog.rs: GtkCredentialPrompter: CredentialPrompter plus a presenter that shows one adw::AlertDialog at a time. It covers SshHostKeyConfirmation, SshPassword, SshPassphrase, SshKeyboardInteractive (every field secret, per the FC-D17 amendment) and SshConfirmation. A tokio mpsc queue drained by glib::spawn_future_local.
  • Storage. In crates/storage/src/connections.rs, tls: TlsConfig replaces use_tls. Rewrite the fixtures at version 1, with no migration (owner decision). Update crates/storage/tests/isolation.rs and crates/app/tests/secret_save_flow.rs:20-48.
  • Error text. ui/error_text.rs gets one sentence per SshFailure variant (ssh_failure.rs:9-72); transport_message currently falls back to to_string() at error_text.rs:144. Delete ssh_message(SshError) (error_text.rs:20-28).
  • Drivers. .disable_statement_logging() on MySqlConnectOptions (crates/drivers/mysql/src/lib.rs:38) and SqliteConnectOptions (crates/drivers/sqlite/src/lib.rs:51). The spec's connect_options.rs files do not exist. Coordinate with the W2-13/W2-14 rewrites.
  • Test support. New crates/test-support/src/fake_connector.rs and fake_credential_source.rs.

Dependencies.

  • Removed: russh 0.63.3. The lock loses 51 packages, computed from Cargo.lock: ssh-key 0.7.0-rc.11, russh-cryptovec 0.62.0, num-bigint 0.5.1, ml-kem 0.3.2, p256/p384/p521 0.14.0, ed25519-dalek 3.0.0, enum_dispatch 0.3.13, nix 0.31.3, pageant 0.2.3 and others. num-bigint 0.4.8 stays (via oo7).
  • Added: tablepro-net path dependency in crates/app/Cargo.toml (for DirectTcpTransport), futures.workspace (0.3.34, already in lock), and tracing-subscriber 0.3 as a dev-dependency of the mysql and sqlite drivers.

Tests.

  • SSH: the four or five transport tests above.
  • Core: ssh_failure_category_matrix exists as category_matrix; driver_error_delegates_ssh_category is covered by transport_error.rs:46 plus an error.rs case.
  • Storage: saved_connection_requires_tls_field.
  • App: keyring_credentials_not_stored_is_error_not_empty_password, keyring_credentials_found_injects_secret, monitor_passes_unattended.
  • Drivers: mysql_emits_no_sqlx_query_events, sqlite_emits_no_sqlx_query_events.
  • Prompter (from W1-12): presenter_builds_rows_per_field_and_remember_switch, keyboard_interactive_echo_field_is_entry_row, closing_dialog_replies_cancelled, prompts_are_presented_sequentially.

Depends on: W2-11 (ConnectRequest, RequestTarget, DriverCapabilities on drivers), W2-13 and W2-14 (driver files), W4-05 (done, 7485ae7c6), W4-06 (same commit).

Stale or amended spec text.

  • Everything russh is replaced by FC-D17 / W8-04: FirstHopTransport, SshTransport with channel_open_direct_tcpip, Handler::disconnected, KnownHostsStore, HostKeyVerifier, route_connector.rs, tests/route.rs, tests/authentication.rs, the deleted tunnel.rs and the "MySQL uses PrivateSocketRelay" line.
  • MySQL now uses the OpenSSH socket (FC-D2 amendment).
  • ProxyCommandUnsupported, JumpLoop and JumpChainTooLong do not exist; OpenSSH handles them.
  • The plan's "step 25" wording is the old step number.

Size: about 2,300 changed lines (+1,750, −560).

W2-17 · App surfaces for contract v2: Security group, SQLite file rows, read-only subtitles, categorised driver errors (Step 30)

Status: partly done (error text only).

  • Present in ui/error_text.rs:
    • one sentence per TlsFailure (:151-170)
    • file errors (:95-106)
    • WriteOutcome texts (:63-91)
    • Busy and EditorSessionBusy (:107-112)
  • Missing:
    • driver_message still matches variants and ends in a wildcard other => other.to_string() (:124); it should match category() exhaustively.
    • There is no tls_remedy.
    • The Encryption control is still a use_tls SwitchRow (connect_dialog.rs:241-243).
    • SQLite still uses the Database EntryRow retitled "File path" (connect_dialog.rs:627-631); there is no file chooser.
    • The read-only SwitchRow has a fixed subtitle (:246-250).

Remaining work.

  • crates/app/Cargo.toml: add tablepro-net.
  • New ui/error_text/tls_remedy.rs, or a tls_remedy() in error_text as W6-11 names it (pick one).
  • New ui/connect_dialog/pem_validation.rs: a generation-tagged Tasks::spawn_blocking_task(tablepro_net::tls::check_pem_files), from crates/net/src/tls/pem_check.rs:8.
  • New ui/connect_dialog/read_only_row.rs: subtitle chosen by ReadOnlyEnforcement (capabilities.rs:19-23).
  • tls_section.rs and sqlite_file_row.rs are built once, inside W6-13's module (W6-13 says W6 is the only implementer).
  • Toast buttons "Choose CA Certificate…" and "Set TLS Server Name" focus the row.
  • Each new widget is a CompositeTemplate .ui under data/resources/ui/, listed in po/POTFILES.in.

Dependencies: none new beyond tablepro-net (path dependency).

Tests.

  • Unit: every_category_has_text, diagnostics_codes_rendered_per_engine, partially_applied_and_commit_unknown_never_claim_rollback, unknown_issuer_and_name_mismatch_have_actions, stale_generation_result_is_ignored.
  • #[gtk::test]: encryption_lists_only_capability_modes, sqlite_shows_file_row_hides_security, invalid_ca_marks_row_error_and_disables_connect, connect_insensitive_while_pem_check_pending, mysql_requires_database, read_only_subtitle_follows_enforcement, driver_switch_clears_network_prefills.
  • The last one contradicts W6-13's driver_switch_keeps_host_database_username and the shipped behaviour at connect_dialog.rs:600-603 ("host, database and username stay"). Keep the shipped rule and drop W2-17's test.

Depends on: W2-11, W2-12..W2-16 (capabilities on every driver), W4-06, W6-11, W6-13.

Stale or amended spec text.

  • AppServices::production and application/app_services.rs do not exist: there is no application/ directory, and lib.rs:105-111 passes AppInit.
  • Arc<SystemTrustSource> and preload() are removed by the FC-D8 amendment (rustls-platform-verifier, per-engine trust). Nothing to preload.
  • net::tls::pem_bundle::check_pem_files is really tablepro_net::tls::check_pem_files.
  • The Relm4 widgets become gtk-rs subclasses (FA-D1).

Size: about 450 lines unique to this task, plus about 500 counted under W6-13 (Security and SQLite rows).

W4-06 · Generation-leased Sessions replace DatabaseService (Step 30)

Status: not started. Only W4-05's pure primitives exist (crates/app/src/session/*.rs, 17 files: HealthPolicy, MonitorMailbox, ConnectAttempts, SessionKey and others; 7485ae7c6). The global DatabaseService singleton (services/database_service.rs:12-16) and the 30-second ping monitor with fixed backoff (connection_monitor.rs:194-256) are live. App keeps:

  • current_driver_id (ui/app/mod.rs:116)
  • read_only (:122)
  • the 1 Hz PollHealth timer (:1327-1328)
  • the three FactoryVecDeque fields

Remaining work.

  • Create in crates/app/src/session/, or in crates/session (open question 2):
    • session.rs, session_deps.rs
    • operation_runner.rs, operation_kind.rs, operation_error.rs, replay_rule.rs
    • deadline_policy.rs, default_deadlines.rs
    • connection_metadata.rs, close_report.rs, established_connection.rs, connection_link.rs, connection_lease.rs, retired_connection.rs, editor_session_shutdown.rs
    • health_monitor.rs, reconnect_plan.rs, reachability_target.rs
    • connect_request.rs, request_target.rs, connect_source.rs, connect_intent.rs
    • pending_save.rs, secret_writes.rs, connection_writes.rs, persist_pending_save.rs
    • pending_session.rs, connect_error.rs, attempt_outcome.rs, resolve_saved.rs
    • session_registry.rs, session_claim.rs, claim_error.rs, registry_event.rs
    • watch_forwarder.rs: a tokio::sync::watch receiver drained on glib::spawn_future_local; the spec uses it but never lists the file.
    • The connector and credential files are shared with W1-11.
  • Create in crates/app/src/runtime/: process_hold.rs, hold_token.rs, gio_process_hold.rs, built on gio::ApplicationExtManual::hold, which returns ApplicationHoldGuard (gio-0.22.9/src/application.rs:161-185).
  • Delete: services/database_service.rs, services/connection_monitor.rs, services/connection_service.rs, and their lines in services/mod.rs:46-48.
  • Change:
    • lib.rs: AppInit gains sessions, connector, hold and ssh.
    • ui/app/mod.rs, connection.rs, browse.rs, row_ops.rs, structure.rs, workspace_tabs.rs, status_pages.rs.
    • Also import.rs and ran_statements.rs, which the spec misses (finding 5 for import).
    • ui/connect_dialog.rs (W6-13), ui/browse_tab.rs, ui/structure_tab/mod.rs, ui/editor/mod.rs (W6-12), ui/history_dialog.rs:120, ui/error_text.rs (W6-11).
    • crates/test-support/src/fake_connection.rs and fake_driver.rs move to CONN v2; add fake_editor_session.rs.
    • New crates/app/tests/session_postgres.rs.
    • .github/workflows/build-linux.yml: add the app package to the Docker matrix with GTK -dev packages, or keep this test in crates/session (finding 8).

Dependencies: add futures.workspace = true (0.3.34 is in the lock; the app manifest lacks it, crates/app/Cargo.toml:35-63). tokio-util already enables rt for AbortOnDropHandle (tokio-util-0.7.19/src/task/abort_on_drop.rs:21). CancellationToken::child_token and drop_guard are at cancellation_token.rs:204,275. No new crate.

Tests (spec list, unchanged except where noted).

  • connection_link.rs: lease_survives_replace.
  • operation_runner.rs: loss_on_retired_generation_does_not_post_current_loss, page_replays_once_on_newer_lease_after_connection_lost, server_error_is_not_replayed_on_newer_lease, save_is_never_replayed, caller_cancel_maps_cancelled_session_cancel_maps_closed, panic_posts_probe_requested.
  • replay_rule.rs: replay_table.
  • health_monitor.rs: reconnects_on_connection_lost_with_exact_backoff, refresh_called_each_attempt, busy_probe_keeps_online, lease_lost_future_wakes_monitor, late_reconnect_after_cancel_is_discarded_and_closed.
  • session.rs: begin_shutdown_closes_once_then_already_closed, hanging_close_reports_abandoned_at_close_timeout, dropping_last_arc_ends_monitor.
  • editor_session_shutdown.rs: closes_each_session_once, hanging_editor_close_abandoned_at_close_timeout.
  • driver_connector.rs: cancelled_establish_closes_late_connection, kerberos_refused_for_driver_without_mode.
  • persist_pending_save.rs: secrets_written_before_entry. keyring_failure_saves_no_entry conflicts with shipped behaviour (finding 7).
  • pending_session.rs: discard_closes_connection.
  • session_registry.rs: claim_refuses_duplicate, claim_drop_releases_key, live_count_event_on_attach_and_drop.
  • keyring_credentials.rs: storage_error_propagates_as_keyring.
  • reachability_target.rs: for_request_table.
  • error_text.rs: driver_error_text_covers_every_category, connect_error_text_covers_every_variant.
  • GTK: session_start_without_entered_runtime, stale_attempt_outcome_is_discarded, disconnect_closes_editor_sessions_once, reply_from_previous_session_is_dropped.
  • Integration: pg_terminate_backend_triggers_reconnect_and_next_query_succeeds, saturated_pool_operation_busy_probe_ok_stays_online.
  • Add: csv_import_reads_columns_through_session to cover finding 5.

Depends on:

  • W2-11 and W2-12..W2-16 (hard blockers)
  • W3-05 and W3-06 (dialect via lease.dialect())
  • W4-05 (done)
  • W6-11, W6-12, W6-13 (same commit)
  • W1-11
  • W1-03, W1-07, W1-13: superseded by OpenSSH or already done (W1-07 landed in 427af08ac)

Stale or amended spec text.

  • application/app_services.rs does not exist.
  • ui/editor.rs is now ui/editor/mod.rs (W6-02 split).
  • history_dialog.rs:113 is now :120.
  • connect_dialog.rs:649-663 is now :779-833, and that code already writes secrets first.
  • ConnectServices.host_keys (russh HostKeyVerifier) becomes SshServices.
  • KeyringCredentials { secrets: SecretStore } becomes Arc<dyn SecretVault>.
  • TaskSpawner/spawner becomes Tasks (finding 10).
  • The "W6 accessor SqlEditor::editor_session()" becomes a method on the gtk-rs SqlEditor subclass.
  • The "relm4 Component" and guarded_input vocabulary becomes gtk-rs plus glib::spawn_future_local (FA-D1).

Size: about 6,000 changed lines: new session stack and tests about 3,200, call-site rewrites about 2,200, deletions about 590.

W6-11 · Map driver, connect and operation errors by ErrorCategory (Step 30)

Status: partly done.

  • driver_message, transport_message, tls_message and secret_message exist (ui/error_text.rs:30-170,262-284) with tests (:178-260,291-330).
  • They are variant-based with wildcard fallbacks (:124, :144, :168), not category-exhaustive.
  • build_sql_message exists only under #[cfg(test)] (:6-18) and covers the v1 BuildSqlError.
  • There is no connect_error_message, operation_error_message, script_diagnostic_message or tls_remedy.

Remaining work in crates/app/src/ui/error_text.rs:

  • Rewrite driver_message as match error.category() with no wildcard. ErrorCategory (error_category.rs:1-15) is not #[non_exhaustive], so that is possible. Details come from if let, because DriverError is #[non_exhaustive] (error.rs:24).
  • Add connect_error_message(&ConnectError), operation_error_message(&OperationError), sql_build_message(&SqlBuildError) (core statement/sql_build_error.rs), script_diagnostic_message(&ScriptDiagnostic, line) (sql_syntax/script/script_diagnostic.rs), tls_remedy(&DriverError) and SshFailure texts.
  • Delete ssh_message.
  • keyring_store_message already lives in services/secret_save_report.rs:27 as messages(); keep it.
  • If the module passes 800 lines, split it into error_text/ submodules.

Dependencies: none.

Tests: every_category_has_a_message, server_message_includes_code_detail_hint, tls_unknown_issuer_suggests_ca, auth_message_has_no_secret, busy_and_editor_busy_messages_differ, file_not_found_names_path, timeout_phase_login_vs_statement_differ, unterminated_dollar_quote_diagnostic_names_line.

Depends on: W2-11 (ConnectError and OperationError come with W4-06; SqlBuildError exists). W6-05 is done (script planner). W6-09 is done (3d833626f).

Stale or amended: nothing beyond the W2-17 overlap: one tls_remedy, defined here and consumed by W2-17 and W6-13.

Size: about 450.

W6-12 · Run editor SQL through a tab-owned EditorSession with a results list (Step 30)

Status: not started. The editor still runs everything through the global connection:

  • database_service::instance().active() (ui/editor/mod.rs:713)
  • conn.query(&sql) per statement (:846)
  • a client-side tokio::select! timeout race on Relm4's runtime (:739-788)
  • StatementOutcome sub-tabs (:51-69, :906-1045)

The script split already goes through the core ScriptPlan (statement_cursor.rs:16-23). RunAtCursor uses statement_bounds_at (mod.rs:493-516).

Remaining work.

  • Rewrite ui/editor/mod.rs as a gtk-rs SqlEditor subclass with a CompositeTemplate data/resources/ui/sql-editor.ui.
  • New files in ui/editor/: run_scope.rs, run_request.rs, script_runner.rs, editor_session_slot.rs, channel_result_sink.rs (tokio mpsc bounded(8); finding 9), run_log.rs, run_log_entry.rs, result_item.rs (GObject), results_pane.rs (+ .ui), sidebar_policy.rs, run_status.rs, transaction_dirty.rs, run_history.rs.
  • New services/engine_profile.rs and services/workspace_identity.rs.
  • Change:
    • ui/app/workspace_tabs.rs:167-190,575-595: SqlEditorInit becomes a constructor.
    • ui/app/mod.rs.
  • Delete from mod.rs: StatementOutcome*, run_statements, render_outcomes, build_outcome_widget, summary_label and their tests (:1266-1279).
  • History goes through HistoryService::store() (services/history_service.rs).

Dependencies: none. The plan's async_channel is replaced by tokio mpsc.

Tests (spec list).

  • Pure: run_scope_prefers_selection, run_scope_statement_at_cursor_uses_plan, run_log_maps_events, run_log_stop_policy_marks_rest_not_run, run_log_continue_policy_runs_next_batch, sidebar_policy_single_entry_hides_toggle, sidebar_policy_two_entries_expanded_shows_list, transaction_dirty_cleared_when_lost, transaction_dirty_unknown_needs_a_run.
  • GTK, with FakeEditorSession: selected_text_is_the_only_sql_sent, pg_script_sends_one_statement_per_run, vacuum_then_select_sends_two_runs, failing_third_statement_keeps_first_two_and_marks_rest_not_run, mssql_go_script_sends_two_batches_second_twice, lexical_settings_change_replans_remainder, run_disabled_while_opening, failed_open_shows_retry_banner_and_retry_reopens, editor_session_busy_shows_failed_entry, unterminated_diagnostic_warns_and_still_sends, twenty_results_use_list_not_switcher, first_failure_is_selected, collapsed_selection_hides_sidebar_and_toggle_reopens_list, stop_cancels_run_token, connection_lost_reveals_banner_posts_probe_and_clears_dirty, history_records_executed_selection, markup_in_error_is_rendered_literally.

Depends on:

  • W2-11, including two additions that core lacks: EditorSession::lexical_settings() (the trait at editor_session.rs:55-75 has none) and a script-syntax/keyword declaration (W6-06, absent: DriverCapabilities has no script field).
  • W4-04 (done), W6-04 (not landed: there is no undoable replace), W6-06, W6-10 (done), W6-11, W4-06.

Stale or amended spec text.

  • The deleted files legacy_statements.rs, outcome_view.rs and schema_words.rs do not exist.
  • ScriptPlan::build(scoped, base, script, settings) really takes (text, SqlGrammar, LexicalSettings) (script_plan.rs:23).
  • Until W6-06, EngineProfile derives the grammar from dialect::grammar_for(driver_id) (engine_dialect.rs:72-80, which silently defaults unknown ids to PostgreSQL).
  • async_channel becomes tokio::sync::mpsc.
  • The Relm4 SimpleComponent becomes a gtk-rs subclass (FA-D1).

Size: about 2,600.

W6-13 · Rebuild the connect dialog on ConnectRequest v2 with Security, SQLite file and SSH agent rows (Step 30)

Status: not started.

  • ui/connect_dialog.rs (1261 lines) is a Relm4 Component (:161-162) that establishes and saves by itself (run_connect, :729-850).
  • ui/ssh_section.rs builds russh interim configs and refuses agent, keyboard-interactive and jump hosts through interim_ssh_target (services/connection_service.rs:132-162, called at ssh_section.rs:144).
  • W6-07, W6-08 and W6-09 behaviour is present (remembered driver and port_edited at :600-620; activates_default).

Remaining work.

  • Delete ui/connect_dialog.rs.
  • Create in ui/connect_dialog/:
    • mod.rs: a gtk-rs adw::Dialog subclass with data/resources/ui/connect-dialog.ui.
    • connect_form.rs (pure), form_field.rs, form_errors.rs, connect_submission.rs.
    • tls_section.rs (+ .ui), sqlite_file_row.rs (+ .ui), read_only_row.rs (W2-17).
  • Create services/pending_save.rs, or reuse W4-06's session/pending_save.rs (one type, not two).
  • Change:
    • ui/ssh_section.rs: build tablepro_ssh::SshConfig { destination: SshDestination::new(..), jump_hosts: SshDestination::parse_jump_list(..), auth } (crates/ssh/src/destination.rs:13,33). Auth offers Agent, Password, Private Key and Keyboard-interactive. Default to Agent when SSH_AUTH_SOCK is set. Delete the interim refusals.
    • ui/ssh_inputs.rs.
    • ui/app/connection.rs: on_connect_submission.
    • po/POTFILES.in.
  • Test button: Connector::establish then PendingSession::discard. The button relabels to Stop.

Dependencies: none new.

Tests (spec list).

  • Pure connect_form: verify_full_is_default, kerberos_rejected_when_driver_lacks_it, mysql_requires_database, client_identity_rejected_for_mssql, file_driver_requires_file, ssh_key_auth_requires_path, port_follows_endpoint_default_until_edited, driver_switch_keeps_host_database_username, hidden_fields_are_omitted_from_request, match_saved_connection_by_path_for_files, match_saved_connection_distinguishes_user_and_auth.
  • GTK: encryption_combo_lists_only_capability_modes, sqlite_shows_file_row_and_hides_security_and_ssh, invalid_ca_marks_row_error_and_disables_connect, new_database_sets_create_new_endpoint, test_button_stop_cancels_establish, test_success_toast_has_no_count, connect_emits_submission_without_establishing, agent_is_default_with_ssh_auth_sock.
  • Storage: pending_save_store_connection_writes_file.
  • Port the existing connect_dialog.rs tests (:933-1260, 6 unit and 6 GTK tests).

Depends on: W2-11 (caps, ConnectRequest), W4-06 (Connector, PendingSession), W6-11, W6-12 (WorkspaceIdentity), W1-11 (SshConfig shape). W1-03, W1-04 and W1-13 are superseded by OpenSSH. W6-09 is done.

Stale or amended spec text.

  • tablepro_net::tls::inspect_ca_file/inspect_client_identity do not exist; use check_pem_files (crates/net/src/tls/pem_check.rs:8), whose ConfigError::CaFile/ClientIdentityFile carry a CertificateFileProblem.
  • tablepro_ssh::SshAuth::Agent exists in the OpenSSH crate (crates/ssh/src/config.rs:15). The SSH contract's russh AgentClient text is stale.
  • The FD-C25 RSA subtitle rationale changes with russh gone: OpenSSH handles RSA identities. Re-check the wording.
  • The auth list in the spec omits KeyboardInteractive, which SavedSshAuth (storage connections.rs:61-71) and OpenSSH support. Keep it.

Size: about 2,800 (−1,261 −~150, +~1,400 Rust, +~300 .ui).

W4-07 · Per-workspace tracker registries with borrow-safe handles (Step 31)

Status: not started.

  • thread_local! registries remain at services/change_tracker.rs:724-765 (with_tab, with_tab_ref, open_tab, close_tab, any_pending_globally, pending_tabs) and services/structure_tracker.rs:99-131.
  • There are 53 call sites: ui/app/browse.rs 2, connection.rs 3, mod.rs 8, structure.rs 3, workspace_tabs.rs 15, ui/browse_tab.rs 18, ui/grid.rs 3, ui/structure_tab/mod.rs 3.
  • The grid reads the tracker through with_tab_ref inside bind (grid.rs:126-160,439-443).

Remaining work.

  • New in services/: change_tracker_registry.rs, tab_tracker_handle.rs, tracker_edit.rs, tracker_delta.rs, cell_overlay.rs, structure_tracker_registry.rs, structure_tracker_handle.rs.
  • Change: change_tracker.rs and structure_tracker.rs (remove the thread_local and free functions); ui/app/{mod,workspace_tabs,structure,connection}.rs; ui/browse_tab.rs; ui/grid.rs (TabGridContext at :79-160 holds Option<TabTrackerHandle>); ui/structure_tab/mod.rs.
  • CI: a step in .github/workflows/build-linux.yml that fails on thread_local! under crates/app/src/services.
  • Relm4 amendment: subscribe(relm4::Sender<TrackerEvent>) (change_tracker.rs:159,187) is dropped. Every mutation returns a TrackerDelta that the caller applies, so no channel is needed. The Relm4 forward at browse_tab.rs:1776-1790 goes away.

Dependencies: none.

Tests: registries_isolated_per_workspace, dirty_lists_only_pending_tabs, apply_returns_delta_with_pending_count, GTK edit_during_rebind_shows_pending_value, and the CI grep.

Depends on: W4-06.

Stale: the line numbers (grid.rs:88-130/398) moved to :121-160 and :439. "relm4 0.11 channel Sender" is dropped (FA-D1).

Size: about 1,100.

W4-08 · Undo restores the prior pending value; saves follow edit order (Step 33)

Status: not started.

  • The undo bug is present: track_cell_edit pushes UndoOp::CellEdit { prev_value: original } (change_tracker.rs:233-238), and undo removes the update entirely for persisted rows (:349). After A→B→C, undo returns to the original value, not B.
  • Save order comes from HashMap iteration: per-row updates at :547-555, deletes at :621.
  • The core ChangeSet is grouped by kind (crates/core/src/save/change_set.rs:5-9: inserts, updates, deletes), not the ordered changes: Vec<RowChange> that the WRITE amendment requires.

Remaining work.

  • New: services/undo_op.rs, services/touch.rs, crates/app/tests/tracker_save_postgres.rs.
  • Change: services/change_tracker.rs (Touch-stamped CellEdit, DeleteMark, DraftRow; exact before/after undo; ordered change_set()), services/tab_tracker_handle.rs, ui/browse_tab.rs (apply TrackerDelta.visual on Undo and Redo).
  • Core: ChangeSet becomes ordered Vec<RowChange>. This is the W3 lane's job inside Step 30. If it does not land there, W4-08 has to change core plus plan_save and every driver caller.

Dependencies: none.

Tests: undo_after_a_b_c_restores_b, undo_after_a_b_a_restores_b, redo_after_undo_reapplies_c, new_edit_after_undo_clears_redo, undo_delete_restores_prior_mark_absence, draft_cell_undo_restores_previous_draft_value, change_set_orders_rows_by_last_touch, change_set_places_delete_before_later_insert, change_set_identical_across_trackers, deleted_row_emits_only_delete, and the integration test replace_by_unique_key_saves, which needs the app or session package in the Docker job (finding 8).

Depends on: W3-06 (dialect plan_save), W4-07, and the ordered ChangeSet in core.

Stale: line numbers (261-266, 374, 576-583, 655) are now 233-238, 349, 547-555, 621.

Size: about 800.

W4-09 · Break reference cycles in tab view, grid, filter strip, editor and structure tab (Step 33)

Status: not started. The cycles are present:

  • tab_view.connect_n_pages_notify captures a strong overview_button (ui/app/workspace_tabs.rs:56-60).
  • connect_create_tab captures strong clones (:168-190).
  • column.connect_fixed_width_notify captures column_for_save (ui/grid.rs:585-589).
  • The filter_strip Rebuilder Rc captures its own slot (ui/filter_strip.rs:305,375-381).
  • The editor's StyleManager::connect_dark_notify is never disconnected and holds the view (ui/editor/mod.rs:257-263); likewise settings.gio().connect_changed (:264-270).
  • The DropTarget holds the view strongly (:411-413).
  • structure_tab/mod.rs:569-572 holds the StyleManager handler with a strong buffer.

Remaining work:

  • workspace_tabs.rs: bind_property("n-pages", …).transform_to, plus glib::clone!(#[weak] …).
  • grid.rs: use the signal's column argument.
  • filter_strip.rs: hold a Weak<RefCell<Option<Rebuilder>>>.
  • Editor and structure tab: store the SignalHandlerId and disconnect on dispose. These become dispose in the gtk-rs subclasses that W6-12 and W4-10 introduce; there is no SimpleComponent::shutdown.
  • Swap the relm4::{adw,gtk} imports in filter_strip.rs:35-36.

Dependencies: none.

Tests: tab_view_freed_after_teardown, closed_editor_frees_source_view (now drops the SqlEditor widget, not a Controller<SqlEditor>), column_freed_with_column_view, strip_freed_after_drop, style_handler_disconnected_on_shutdown.

Depends on: W4-07, and W6-12 / W4-10 for where the dispose hooks live.

Stale: every line number (46-59, 158-198, 544-551, 521-560, editor.rs:220,236-258,339-365, structure_tab:555). "relm4 Component::shutdown" becomes ObjectImpl::dispose (FA-D1).

Size: about 250.

W4-10 · Tabs own sequenced, cancellable fetches that recover after reconnect; shared SchemaCatalog (Step 34)

Status: not started.

  • App issues every browse fetch: ui/app/browse.rs:30-294, through AppMsg::FetchBrowsePage, RowsLoaded and related messages (ui/app/mod.rs).
  • Structure loads go through ui/app/structure.rs:125,304.
  • BrowseTab is a Relm4 SimpleComponent (browse_tab.rs:1342).
  • There is no RequestSequencer, SchemaCatalog or FailedRequests.

Remaining work.

  • New:
    • runtime/request_sequencer.rs, request_stamp.rs, request_ticket.rs, stamped.rs
    • session/schema_catalog.rs, object_list_snapshot.rs
    • ui/browse_tab/ (move browse_tab.rs to browse_tab/mod.rs) with browse_request_kind.rs, browse_command.rs, page_reply.rs, failed_requests.rs
    • ui/structure_tab/structure_command.rs
  • Delete: ui/app/browse.rs.
  • Change: session/session.rs and health_monitor.rs (commit catalog on reconnect), ui/structure_tab/mod.rs, ui/app/{mod,structure,connection,workspace_tabs}.rs (catalog_watch feeds the sidebar factory).
  • Relm4 amendment: BrowseTab and StructureTab (with columns, fks, indexes and structure_tab_dialogs) become gtk-rs subclasses with .ui templates instead of "a relm4 Component". The two relm4::channels in browse_tab go.
  • futures::future::Shared comes from futures (added in W4-06).

Dependencies: none beyond futures 0.3.34.

Tests: issue_cancels_previous_token_of_same_kind, accept_rejects_superseded_stamp, duplicate_reply_dropped, drop_cancels_all, ten_concurrent_objects_calls_one_list_objects, refresh_during_fetch_reruns_once, failed_refresh_keeps_objects_sets_last_error, columns_coalesce_per_table, invalidate_keeps_data_until_refetch, dropped_awaiter_does_not_cancel_fetch, stale_page_reply_ignored, closing_tab_cancels_call_options_token, failed_load_refetches_after_link_replace, loaded_tab_keeps_data_on_link_replace, failed_structure_load_refetches_after_link_replace, late_catalog_commit_after_disconnect_leaves_sidebar_untouched, and integration run_cancellable_stops_server_statement.

Depends on: W1-09 (done), W3-06, W3-11, W3-17 (done, 92aea7112), W4-07, W4-08, W4-06.

Stale: "BrowseTab becomes a relm4 Component (CommandOutput, update_cmd)" (FA-D1). spawn_task lives on Tasks.

Size: about 3,500 including the gtk-rs conversion of the two tabs; about 1,900 without it.

W4-11 · Per-session WorkspaceStore with off-thread writer; editor changes carry no text; dropped files load asynchronously (Step 36)

Status: partly done.

  • Done:
    • workspace/persist_debouncer.rs (trailing debouncer, Drop cancels, three tests) and workspace/draft_writer.rs from W1-10 (aa002205e).
    • Editor tabs persist a draft id with query: String::new() (ui/app/workspace_tabs.rs:988-993).
  • Missing:
    • Workspace state is still one global JSON file, read and written synchronously on the GTK thread: workspace_state.rs:111-148 → services::write_json → storage::fs::write_private_blocking (services/mod.rs:71-82), behind a process-wide static FILE_LOCK (workspace_state.rs:15).
    • It finds its key through database_service::instance().active_id() (workspace_tabs.rs:971).
    • There is no blocked-key handling: an unreadable file becomes unwrap_or_default (workspace_state.rs:116-120).
    • SqlEditorOutput::QueryChanged(String) carries the whole text on every change (ui/editor/mod.rs:107,314).
    • Dropped files are read with std::fs::read_to_string on the GTK thread (mod.rs:411-425).

Remaining work.

  • New in workspace/: workspace_store.rs, write_job.rs, workspace_load.rs, workspace_load_error.rs, workspace_save_error.rs, workspace_file_name.rs, workspace_record.rs, tab_record.rs, sort_record.rs. mod.rs and persist_debouncer.rs already exist.
  • New: ui/editor_file_drop.rs, using sourceview5::FileLoader::load_future.
  • Delete: services/workspace_state.rs.
  • Change: lib.rs (mod workspace is already present at lib.rs:20; make it pub only if tests need it), ui/app/{mod,workspace_tabs}.rs, ui/editor/mod.rs.
  • CI: a std::fs:: grep over ui/editor, ui/app and editor_file_drop.rs. It will flag ui/app/connection.rs:387 (synchronous std::fs::write for connection export), which then needs fixing too.
  • The writer channel is tokio mpsc, not async-channel (finding 9).

Dependencies: none. The spec's "async-channel returns to crates/app" is dropped.

Tests:

  • round_trip, clamped_limits_tabs_and_resets_active_tab
  • unsupported_version_blocks_key, blocked_key_leaves_bytes_unchanged, reset_moves_aside_and_unblocks, written_file_mode_0600, writer_coalesces_latest_record_per_key, flush_waits_for_pending_write, notfound_loads_empty
  • names_for_saved_and_file_keys
  • fires_once_after_last_request: exists as persist_debouncer.rs tests; check its name and the 2 s bound.
  • request_after_fire_emits_no_critical: likewise check it exists.
  • restore_selects_active_tab_and_reopens_structure_tabs, typing_emits_content_changed_without_text, dropped_file_loads_into_empty_buffer, dropped_file_inserts_at_cursor_in_non_empty_buffer, unreadable_dropped_file_shows_toast

Depends on: W1-05, W1-09, W1-10 (all done), W4-02 (done), W4-10, W4-06 (SessionKey claim).

Stale:

  • workspace/mod.rs (new) and persist_debouncer.rs (new) already exist.
  • ui/editor.rs is now ui/editor/mod.rs.
  • application/app_services.rs does not exist.
  • async_channel becomes tokio.

Size: about 1,200.

W4-12 · SaveLedger, stoppable saves, event-driven CloseGuard, unknown outcomes, WindowInhibitor (Step 37)

Status: not started.

  • Polling bookkeeping is live: close_after_save, close_window_after_save, in_flight_saves, structure_saves_in_flight and dec_close_after_save (ui/app/mod.rs:39,136-156; ui/app/structure.rs:160-462).
  • There is no inhibit and no SaveLedger.

Remaining work.

  • New in workspace/: save_id.rs, save_ticket.rs, save_kind.rs, save_outcome.rs, unknown_cause.rs, rollback_cause.rs, save_ledger.rs, save_start.rs, save_rejection.rs, save_snapshot.rs, dirty_tab.rs, dirty_kind.rs, pending_state.rs, close_scope.rs, close_step.rs, close_decision.rs, close_guard.rs, save_changes_dialog.rs, waiting_dialog.rs, unknown_outcome_dialog.rs, open_transaction_dialog.rs, window_inhibitor.rs, inhibit_reason.rs, workspace_handle.rs.
  • Change: services/change_tracker.rs, tab_tracker_handle.rs, structure_tracker_handle.rs, ui/app/{mod,row_ops,structure,workspace_tabs}.rs, ui/browse_tab/mod.rs, ui/structure_tab/mod.rs, ui/error_text.rs.
  • Delete the polling fields and dec_close_after_save.
  • The dialogs are plain adw::AlertDialog code. Save Changes may use a .ui for its extra child.

Dependencies: none (futures is already added).

Tests: next_close_step_table, classify_maps_every_contract_case, frozen_tracker_rejects_begin_save, rolled_back_keeps_dirty, committed_clears_only_snapshot_edits, settle_idempotent, when_idle_resolves_after_last_settle, begin_refuses_duplicate_kind, cancel_scope_cancels_only_scoped_tokens, entry_holds_counting_hold_until_settle, commit_ack_lost_applied_reloads, not_applied_rolls_back, unresolvable_freezes, stop_saves_cancels_write_batch_token, cancel_keeps_window, discard_closes, rolled_back_save_represents, in_flight_save_waiting_dialog_force_closes_on_settle, unknown_reload_cancels_close_and_write_batch_count_stays_one, stop_saving_settles_and_reevaluates.

Depends on: W3-06, W4-08, W4-11, W6-12 (OpenTransactionChanged), W4-06 (ProcessHold, Session).

Stale: the line ranges mod.rs:37-125, 777-819 moved to mod.rs:39,136-156 and structure.rs:160-462. The plan's step 37 note mentions "workspace/close_blocker.rs for XW5", which is not in the task's file list; confirm whether it is needed.

Size: about 2,400.

W5-01 · Move the data grid into a ui/grid module (Step 35)

Status: not started. ui/grid.rs is 2414 lines; the spec says 2,337. The top-level items are at :17-2171. ui/row_object.rs (77) and ui/cell_editor.rs (239) sit at ui/.

Remaining work.

  • Use git mv and split into ui/grid/: mod.rs, grid_msg.rs (GridMsg at :17), cell_preset.rs (:69), tab_grid_context.rs (:79-160), column_factory.rs (build_column_view/build_column at :169-610), grid_menus.rs and context_menu.rs (:1327-1812), cell_popovers.rs (:1053-1258), display_text.rs (:2120-2170), row_json_dialog.rs (:1874), selection.rs (:1813-1873).
  • Move row_object.rs and cell_editor.rs into ui/grid/.
  • Update the super::grid:: and super::row_object:: paths in ui/browse_tab*, ui/editor/* and ui/mod.rs.
  • The unsafe WidgetSlot (:2032-2096) moves as-is; W5-02 deletes it.
  • The column-width binding (:92-120) moves with tab_grid_context or column_factory.
  • No Relm4 change here, because a move must stay behaviour-neutral. The 13 relm4::Sender<GridMsg> uses move as they are.

Dependencies: none.

Tests: the existing grid tests move and the cargo test -p tablepro --lib ui::grid list must match. There are about 19 tests at grid.rs:2171-2414.

Depends on: W1-09 (done), W3-11, W3-17 (done), W4-07, W4-09, W4-10, W6-12.

Stale: the line count. ui/editor.rs is now ui/editor/.

Size: about 2,900 moved lines, about 150 net.

W5-02 · Resolve grid cell events to row objects through the column view cell (Step 38)

Status: not started.

  • The qdata slots are present: POSITION_SLOT, COLUMN_SLOT, SNAPSHOT_SLOT, POPOVER_SLOT, SUPPRESS_SLOT, PREEDIT_SLOT (grid.rs:486-561,1063-1304,2032-2096).
  • is_cell_editable is at :2011, focused_cell_coords at :2097.
  • RowObject is mutable: cells: RefCell<Vec<Value>> and set_cell, cells_clone, with_cells (ui/row_object.rs:13,57-77).
  • Browse resolves rows by position: row_object_at, find_row_position_by_key (browse_tab.rs:771-790).
  • on_copy_row_as_insert is in ui/app/row_ops.rs:113.

Remaining work.

  • New in ui/grid/: grid_row_id.rs, grid_cell.rs (a gtk::Widget subclass with BinLayout, CSS name tp-grid-cell), edit_snapshot.rs, cell_lock.rs, cell_editability.rs.
  • Change: row_object.rs, cell_editor.rs, column_factory.rs, grid_menus.rs, context_menu.rs, cell_popovers.rs, grid_msg.rs, tab_grid_context.rs, selection.rs, ui/browse_tab/mod.rs, ui/app/row_ops.rs (remove copy-as-insert), ui/editor/* (RowObject::result).
  • Relm4 amendment: relm4::Sender<GridMsg> (13 uses) becomes signals on the grid widget or a tokio mpsc sender. That removes the grid's last Relm4 tie.

Dependencies: none. ColumnViewCell needs gtk4 v4_12; gnome_50 covers it.

Tests: edit_after_draft_insert_targets_edited_row, menu_set_null_after_draft_removal_targets_clicked_row, edit_started_before_rows_shift_commits_to_original_row, toggle_after_draft_insert_targets_toggled_row, grid_row_id_key_is_none_for_result_and_draft_rows, cell_editability_matrix, duplicate_row_copies_pending_edits_of_persisted_row, copy_row_as_insert_uses_row_below_draft.

Depends on: W2-11, W3-05, W3-17, W4-01, W4-06, W4-07, W5-01. Also FB C16 export::render_insert: check it exists in core export/sql.rs (151 lines; not verified).

Stale: none beyond the paths.

Size: about 1,500.

W5-03 · Notify bound cells of tracked row changes instead of swapping row objects (Step 39)

Status: not started. The swap trick is live: refresh_row_at uses store.splice(store_pos, 1, &[replacement]) (browse_tab.rs:731-770), called at :856.

Remaining work.

  • New: ui/grid/cell_binding.rs, ui/grid/grid_row_index.rs.
  • Change: ui/grid/row_object.rs (glib::Properties revision), grid_cell.rs (render()), column_factory.rs (bind/unbind), ui/browse_tab/mod.rs (apply_tracker_delta; delete refresh_row_at, refresh_row and the splice).

Dependencies: none.

Tests: tracked_edit_keeps_multi_row_selection, tracked_edit_keeps_row_object_identity, rejected_edit_restores_cell_display, unbind_disconnects_revision_handler, grid_row_index_drops_dead_rows_on_get.

Depends on: W4-07, W5-02.

Size: about 500.

W6-14 · Format only statements whose tokens survive sqlformat (Step 31)

Status: mostly done (7889f6f0b).

  • ui/editor/format_plan.rs (format_script per statement with a token-equality guard, uppercase: None) and significant_tokens.rs exist. They have 10 and 3 tests (format_plan.rs:74-166, significant_tokens.rs:210-225), covering every spec case under slightly different names.
  • Residual 1: Format writes with buffer.set_text (ui/editor/mod.rs:685), which GTK marks irreversible (finding 6). The spec's replace_all_undoable + CursorAnchor (from W6-04) does not exist.
  • Residual 2: the grammar comes from database_service (mod.rs:673-676), not the WorkspaceIdentity.

Remaining work.

  • A replace_all_undoable(buffer, text) helper using begin_user_action + delete + insert, with a cursor anchor, used by Format, ReplaceQuery and FileLoaded.
  • Take the grammar from W6-12's identity.
  • Add a test format_is_one_undo_step.

Dependencies: none. sqlformat 0.5 is already a dependency (crates/app/Cargo.toml:63).

Tests: the existing ones, plus format_is_one_undo_step.

Depends on: W6-12 (identity), W6-04 (the undoable-replace helper, not landed).

Stale: "format_plan.rs (new)" and "significant_tokens.rs (new)" already exist.

Size: about 60.

W6-15 · Present ScriptPlan statement boundaries through a shared editor cursor helper (Step 32)

Status: mostly done (097502359).

  • ui/editor/statement_cursor.rs:30-47 has statement_bounds_at and iter_for_byte.
  • RunAtCursor selects the executed range before sending (ui/editor/mod.rs:512-515).
  • The multibyte test exists (statement_cursor.rs:84-92).
  • Missing: the GTK test run_at_cursor_selects_executed_statement and the unit test iter_for_byte_at_end. plan_for uses LexicalSettings::default_for(grammar) (:7-9), not the editor session's settings; W6-12 owns that. Go to Error (W6-12) and completion (W6-17) do not use the helper yet; that is their work.

Remaining work: the two tests. Swap the relm4::gtk imports (statement_cursor.rs:1-2) to gtk4.

Dependencies: none.

Depends on: W6-12, for the session lexical settings.

Size: about 50.

W6-17 · Complete tables, columns and keywords from the schema catalog (Step 39)

Status: not started. Completion is sourceview5::CompletionWords over a shared schema_buffer holding SQL_KEYWORDS plus table names (ui/editor/mod.rs:284-290,1118-1143; ui/app/workspace_tabs.rs:1102-1114; reset at ui/app/connection.rs:131).

Remaining work.

  • New in ui/editor/completion/: mod.rs, sql_completion_provider.rs, sql_completion_provider_imp.rs, sql_proposal.rs, sql_proposal_imp.rs, proposal_kind.rs, completion_scope.rs (lexed with the core ScriptLexer per the W6-17 amendment), fuzzy.rs.
  • Delete: the schema_buffer plumbing in ui/app/{mod,workspace_tabs,connection}.rs and SQL_KEYWORDS, build_schema_buffer, update_schema_buffer in ui/editor/mod.rs. The spec's schema_words.rs does not exist.
  • Keywords need a per-engine source (W6-06). Until it lands, move SQL_KEYWORDS into core, keyed by SqlGrammar.

Dependencies: none new.

  • sourceview5::ffi is re-exported (sourceview5-0.11.2/src/lib.rs:20).
  • gtk_source_completion_fuzzy_match and _highlight are at sourceview5-sys-0.11.2/src/lib.rs:2101-2109.
  • glib::casefold is at glib-0.22.9/src/convert.rs:419.
  • CompletionProviderImpl::populate_future and refilter are at sourceview5-0.11.2/src/subclass/completion_provider.rs:49,65.

Tests:

  • Scope: after_from_suggests_objects, alias_dot_suggests_that_tables_columns, schema_dot_suggests_schema_objects, quoted_alias_is_unquoted, join_aliases_collected, dollar_quoted_text_is_not_analyzed.
  • Fuzzy: fuzzy_match_ranks_prefix_higher, non_ascii_needle_is_casefolded, highlight_returns_attrs.
  • GTK: populate_after_from_contains_users, populate_alias_dot_lists_columns_with_types, keyword_case_follows_prefix, activate_replaces_word_as_one_undo_step, identifier_needing_quotes_is_quoted_by_dialect.

Depends on: W3-06 (quote_identifier exists on SqlDialect, dialect/sql_dialect.rs:21), W4-06, W4-10 (Session::objects/columns), W6-06 (keywords: missing), W6-12.

Stale: schema_words.rs (deleted) does not exist. engine.capabilities.script.keywords does not exist (no script field in DriverCapabilities).

Size: about 1,500.

5. Implementation order

Step 30 (one stacked branch, squashed only when the tip is green). The app half starts after the core v2 traits from W2-11 compile.

  1. W6-11: error_text on category() (pure; needs only the core types).
  2. W1-11a, storage and SSH seams: SavedConnection.tls; build SshServices in lib.rs; ConnectServices, KeyringCredentials, CredentialSource; the GTK prompter pulled from W1-12.
  3. W4-06, bottom-up and GTK-free first:
    • ConnectionLink, lease and retired → OperationRunner, ReplayRule and DefaultDeadlines → Connector, DriverConnector and PendingSession → health monitor → Session → registry and claim → ProcessHold and WatchForwarder.
    • Then App adoption, disconnect and the call-site rewrites, including import.rs and ran_statements.rs.
  4. W6-13: the connect dialog module as a gtk-rs subclass, including W2-17's Security, SQLite file and read-only rows.
  5. W6-12: the editor on EditorSession as a gtk-rs subclass, with the results pane.
  6. W2-17: tls_remedy toasts and the remaining error surfaces.
  7. W1-11b, deletions: database_service.rs, connection_service.rs, connection_monitor.rs, russh_tunnel.rs, the russh dependencies and the deny/docs rescope. Add the app or session package to the Docker matrix.

Steps 31-39, one commit each:

  • W4-07, then the W6-14 residual (undoable Format)
  • the W6-15 residual tests
  • W4-08 and W4-09 (disjoint files)
  • W4-10, which converts BrowseTab and StructureTab to gtk-rs
  • W5-01 (pure move)
  • W4-11
  • W4-12
  • W5-02, which drops relm4::Sender from the grid
  • W5-03
  • W6-17

6. Open questions and risks that can block this lane

  1. Hard blocker: core contract v2 and the drivers (W2-11..W2-16) are not started in code. Every Step 30 task here waits on them. Two additions this lane needs from core:
    • EditorSession::lexical_settings()
    • an ordered ChangeSet { changes: Vec<RowChange> }, which W4-08 needs; core has the grouped form at save/change_set.rs:5-9
  2. Where does Session live? The plan puts it in crates/app/src/session/. Putting it in the GTK-free crates/session crate lets session_postgres.rs and tracker_save_postgres.rs run in the Docker matrix without GTK -dev packages (finding 8). Decide before writing W4-06.
  3. Pull W1-12's prompter into Step 30? Without it, OpenSSH refuses unknown host keys and keyboard-interactive, where the russh interim accepted them by TOFU (finding 3). The missing-password and imported-connection symptoms in problem.md also need W1-12's Attended path.
  4. Keyring-failure save rule: W4-06's "save no entry" or the shipped W6-09/W6-13 "save and toast" (finding 7)?
  5. Relm4 coexistence: the App root stays a Relm4 SimpleComponent under RelmApp (lib.rs:99-111) until W4-13/W4-14 at Steps 40-41. The children converted here (editor, connect dialog, browse and structure tabs, grid) must expose GObject signals that App forwards into its Relm4 sender. Two paradigms coexist for about 10 steps, which is the review's open risk. The migration task must own welcome_view, connection_row, sidebar_row, history_dialog, saved_queries_dialog, import_dialog, export_dialog, connection_list and lib.rs.
  6. Channel family: use tokio::sync (no new crate) instead of the plan's async-channel (finding 9). This needs sign-off because the W1-12, W4-11 and W6-12 texts name async_channel.
  7. W6-04's undoable replace is missing, and Format, ReplaceQuery, FileLoaded and the file drop all use irreversible set_text (finding 6). Either land W6-04's helper inside W6-14's step or accept the regression.
  8. W6-06 (script syntax and keywords in capabilities) is missing. W6-12's EngineProfile and W6-17's keywords fall back to grammar_for(driver_id) and a moved SQL_KEYWORDS list.
  9. cargo run outside meson: config::LIBEXECDIR defaults to /usr/libexec (config.rs:5). Only meson devenv points it at the target dir (meson.build:72-75). A plain cargo run therefore cannot find tablepro-askpass, and SSH fails with AskpassHelperMissing. Fall back to the binary's own directory when the helper is missing.
  10. OpenSSH stderr and prompt classification is not a stable API (review open risk). The fixtures in crates/ssh/tests/fixtures/stderr/ pin OpenSSH 9.6/10.x wording (55528b854 already had to handle both). This lane's switch makes it load-bearing for every SSH user.
  11. Bug outside the plan: ui/app/import.rs:58-66 polls a sqlx future on the GTK thread and, from sqlx-core rt/mod.rs:164-169, panics "this functionality requires a Tokio context" (finding 5). It is fixed by W4-06, or can be fixed alone sooner.
  12. Size: the Step 30 share of this lane is about 14,500 changed lines on top of the core and driver lanes. Steps 31-39 add about 14,000 (W5-01's moves account for about 2,900). Review load and rebase churn on the stacked branch are the schedule risk.

@datlechin

Copy link
Copy Markdown
Member Author

Analysis file .analysis/refactor-linux-driver-contract-v2/lanes/tests.md

Local, gitignored analysis from the old machine. Save the body inside the block to that path.

lanes/tests.md

Lane: tests (W9-07, W9-08, W9-09 and the test requirements of Steps 30-39)

Read-only investigation. Paths are relative to linux/ unless they start with .github/ or /. Every claim about current code cites a file:line I opened; crate API claims cite the registry or git checkout under _build/cargo-home/. "Confirmed" means I read it; "inferred" means it follows from what I read but I did not run it.

Summary

  • W9-07 (Connection v2 conformance): not started. No conformance module, no FaultTransport, no v2 Connection to test against. Most of the core vocabulary the cases assert on already exists (additive commits), and the container fixtures it needs exist (W9-02 landed in b0ce575).
  • W9-08 (TLS conformance): not started. The TLS types, the generated PKI, TLS-only fixtures for all five engines and the tablepro-net classifier exist. The only TLS driver test today is the v1 sqlx PostgreSQL one.
  • W9-09 (DDL conformance): not started, and blocked on W3-12 (no DdlDialect, TableDraft or fetch_table_definition in core).
  • Review amendments that touch this lane are mostly already applied: nextest per package instead of integration_matrix.py; app-local GTK helpers (SignalLog, assert_labelled) instead of the Relm4 harness, stock_internals and TestSessionBus; sshd fixtures for keyboard-interactive, @cert-authority and KRL plus a scripted askpass; testcontainers-modules kept. Three gaps remain:
    • The SSH crate's docker tests still build their own alpine sshd and use none of those fixtures.
    • The GTK-linked docker tests (app session_postgres.rs, tracker_save_postgres.rs) have no CI job that can build them.
    • The fixture crate lacks several knobs the Step 30-39 specs need.

Current state of the test infrastructure (evidence)

Fixtures that exist (crates/test-fixtures, confirmed)

  • Images pinned in src/fixture_image.rs:10-37:
    • postgres:17-alpine, mysql:8.4, mariadb:11.4
    • mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04, clickhouse/clickhouse-server:25.8
    • edoburu/pgbouncer:v1.25.2-p0, linuxserver/openssh-server:version-10.3_p1-r1
  • TestPki::generate() (src/test_pki.rs:22-50):
    • RSA-SHA256 CA, unrelated CA, server certificate with SANs localhost and db.tablepro.test (no IP SAN, test_pki.rs:19), client certificate CN tablepro_client (:20).
    • Private keys at 0600 (:64-73). Unit tests at :132-190.
  • PostgresFixture (src/postgres_fixture.rs:32-38):
    • start(HbaMode) and start_in(HbaMode, &TestNetwork).
    • HbaMode is Password, HostSslOnly or ClientCertificate (src/hba_mode.rs:2-28).
    • The certificate role is tablepro_client (postgres_fixture.rs:14-16, 133-135).
  • MysqlFixture::start(MysqlFlavour) (src/mysql_fixture.rs:25-55):
    • Always --require-secure-transport=ON (:57-65) and ALTER USER tablepro REQUIRE SSL (:71).
    • A REQUIRE X509 user tablepro_client (:72-73).
    • Root password is random and not exposed (:36). No start_in(network).
  • MssqlFixture::start() (src/mssql_fixture.rs:26-61): forceencryption = 1, sa only.
  • ClickHouseFixture::start() (src/clickhouse_fixture.rs:25-87): HTTP 8123 plus HTTPS 8443, and an ssl_certificates user with CN tablepro_client (:73-77). No network option.
  • PgBouncerFixture::start(PoolMode) (src/pgbouncer_fixture.rs:23-77):
    • Session, Transaction or Statement pooling (src/pool_mode.rs:2-6), max_prepared_statements = 0 (:69), upstream on a private network.
    • No pool-size option.
  • OpenSshFixture::start(SshAuthVariant) (src/openssh_fixture.rs:34-69):
    • Variants Password, PublicKey, KeyboardInteractive and HostCertificate (src/ssh_auth_variant.rs:4-9).
    • Drop-in AllowTcpForwarding yes plus kbd-interactive PAM settings (:71-86).
    • HostCertificate::known_hosts_line writes @cert-authority (src/host_certificate.rs:25-27).
    • HostKeyRevocation::revoke_host_key builds a KRL with ssh-keygen -k (src/host_key_revocation.rs:14-26).
    • ScriptedAskpass writes a 0700 script (src/scripted_askpass.rs:18-29).
    • No start_in(network) and no ssh-agent helper.
  • Docker self-tests (tests/fixtures.rs:169-419, each #[ignore = "requires docker"]):
    • TLS handshakes for PG, MSSQL and ClickHouse.
    • Password versus X509 principals for MySQL and MariaDB.
    • PgBouncer SELECT 1, sshd forwarding, keyboard-interactive only, @cert-authority trust and KRL refusal.
    • generated_pki_matches_the_files_the_fixtures_copy (:410-419) needs no docker but is still ignored (minor).

Test doubles that exist (crates/test-support, confirmed)

  • FakeConnection, FakeDriver, ConnectionScript and CallCounters are all v1:
    • FakeConnection implements list_tables, fetch_rows, query, execute, execute_in_transaction, ping and close (src/fake_connection.rs:41-101).
    • FakeDriver implements id, display_name, default_port and connect(ConnectOptions) (src/fake_driver.rs:28-53).
  • FakePrompter and FakeSecretVault exist (src/lib.rs:16-17).
  • No FakeEditorSession, FakeConnector, CountingHold, FaultTransport or conformance module (grep for FaultTransport|black_hole|conformance over the tree: no hits).
  • Cargo.toml:12-18 depends on core, async-trait, secrecy, uuid, futures and tokio (sync, time) only.

Core v2 vocabulary already present (additive, confirmed)

  • DriverCapabilities with endpoint, tls, read_only, dml_writes and ddl_writes (crates/core/src/capabilities.rs:60-73).
  • TlsCapabilities.client_identity (tls_capabilities.rs:4-9).
  • ErrorCategory, including Busy, ReadOnly, Timeout and ConnectionLost (error_category.rs:1-15).
  • DriverError v2 with category() (error.rs:24-125, 132): RolledBack at :61, Busy at :110.
  • CallOptions (call_options.rs:4-21), LinkWatch/LinkState (link_watch.rs), LivenessPolicy::DESKTOP with pub fields (liveness_policy.rs:3-37), RowLimit (row_limit.rs).
  • RunEnd::Failed (result_event.rs:64-69) and Truncation (:21-29).
  • WriteBatch { kind, table, steps } (write_batch.rs:10-15), WriteAtomicity (write_ledger.rs:9-12).
  • RowIdentity::UniqueKey (meta/row_identity.rs:8-21), ObjectScope::DefaultSchema (meta/schema_info.rs:15-16).
  • TlsConfig/TlsMode/TlsFailure/ClientIdentity, and ServerNameOverride { AnyRoute, SshForwardOnly, Unsupported } (server_name_override.rs:1-6) enforced by TlsConfig::validate (tls_config.rs:37-58, 76-90).
  • Transport trait with open() and route() (transport.rs:13-25); DirectTcpTransport (crates/net/src/direct_tcp.rs:11-60); SshForwardTransport::route returns UnixSocket (crates/ssh/src/forward.rs:99-101).
  • tablepro_net::tls::classify (crates/net/src/tls/classify.rs:6), tested in crates/net/tests/tls_handshake.rs:123-160, 296.

Missing (grep, confirmed absent):

  • The v2 Connection trait. connection.rs:64-80 is still v1, and driver.rs:7-48 has no capabilities().
  • WorkspaceSettings, list_objects, count_rows, query_bound, link_state, open_editor_session, stream_read, plan_save, fetch_table_definition, DdlDialect, TableDraft, DdlCapabilities, ForeignKeyMode, and any *POOL_MAX_SIZE constant.

Driver tests today (confirmed)

  • tests/integration.rs (v1, flat) in each of:
    • postgres: 341 lines, 6 tests, on testcontainers_modules::postgres 16-alpine (postgres/tests/integration.rs:16-33);
    • mysql: 388 lines, 7 tests;
    • mssql: 444 lines, 11 tests;
    • clickhouse: 380 lines, 12 tests.
  • PostgreSQL also has tests/tls.rs: 3 v1 tests on sqlx PgSslMode, the only consumer of tablepro-test-fixtures (postgres/tests/tls.rs:1-72).
  • SQLite has no integration target. It has csv_import.rs, script_plan_matches_sqlite3_complete.rs and values.rs. W9-01's integration.rs never landed (git log shows no add).
  • Only the postgres driver has tablepro-test-fixtures as a dev-dependency.
  • mysql and mssql still dev-depend on rust_decimal and testcontainers-modules; clickhouse on raw testcontainers (each Cargo.toml [dev-dependencies]).

SSH tests today (confirmed)

  • crates/ssh/tests/openssh_session.rs has 7 docker tests (:131-246):
    • password through prompter, unknown host declined, wrong password, forward socket 0600, master killed, last Arc drop, changed key.
    • It uses its own alpine:3.22 container that installs openssh with apk add at start (:20-24, 36-41). None of the W9-02 sshd variants are used.
  • crates/ssh/tests/askpass_helper.rs has 3 non-docker tests.

GTK test helpers (confirmed; the review's W9-05/W9-14 amendment is applied)

  • crates/app/src/test_support.rs:1-36 provides drain_main_context, wait_until, SignalLog (test_support/signal_log.rs:7-46, connect_local on any GObject signal), descendants, find_by_action_name, first_descendant_of_type, assert_labelled (targeted test_accessible_has_property/relation, accessible_label.rs:8-40), MemorySettings and test_runtime().
  • No test-support-gtk crate, no stock_internals, no TestSessionBus.
  • The gtk-rs subclass test pattern already exists: PreferencesDialog with CompositeTemplate (ui/preferences_dialog/imp.rs:10-11), tests via dialog.imp() template children (ui/preferences_dialog/mod.rs:175-284).
  • Relm4-driven GTK tests remain in:
    • ui/connect_dialog.rs (8 tests; controller.sender().send(ConnectDialogInput::...), :1120-1209);
    • ui/welcome_view.rs, ui/import_dialog.rs, ui/saved_queries_dialog.rs, ui/app/mod.rs and ui/preferences_dialog/mod.rs (via .launch(...)).
  • crates/app/clippy.toml still bans relm4::spawn*.

How tests run today (confirmed)

  • meson (crates/app/meson.build:43-68):
    • suite unit = cargo test --workspace --exclude tablepro, which runs every non-ignored test, including never-ignored tests in driver integration targets;
    • suite gtk = cargo test -p tablepro with GSK_RENDERER=cairo, GTK_A11Y=test and the build-tree schema dir.
  • CI fast job (git show HEAD:.github/workflows/build-linux.yml, lines 18-114):
    • ubuntu:26.04 container;
    • xvfb-run -a dbus-run-session -- meson test (:108);
    • a separate Secret Service step (:109-112).
  • CI integration job (:157-206):
    • ubuntu-24.04 host, apt only build-essential pkg-config plus openssh-client for two entries (:183-186);
    • a hand-listed package matrix: postgres, mysql, mssql, clickhouse, tablepro-ssh, tablepro-test-fixtures (:164-170);
    • cargo nextest run --locked --profile ci -p <pkg> --run-ignored only (:200) and a JUnit upload.
  • .config/nextest.toml: one test group per package with max-threads = 1; the ci profile is fail-fast = false with JUnit output.
  • Locally: .claude/skills/fix-issue/scripts/verify.sh --platform linux docker <package> (lib/platform-linux.sh:397-415) runs nextest if installed, otherwise cargo test -p <pkg> -- --ignored --test-threads=1.

This machine (confirmed)

  • Docker 29.1.3 linux/arm64, cgroup v2, no sudo needed. Local images: clickhouse 25.8, mysql 8.4, postgres 16-alpine, 17-alpine and 18.
  • docker manifest inspect:
    • arm64 manifests exist for edoburu/pgbouncer:v1.25.2-p0, linuxserver/openssh-server:version-10.3_p1-r1 and mariadb:11.4.
    • mcr mssql 2022-CU14 lists no arm64.
    • postgres 13/18-alpine timed out, inferred multi-arch as official images.
  • OpenSSH_10.2p1 and ssh-keygen are on the host. No cargo-nextest and no xvfb-run; GNOME Wayland provides a display for the gtk suite.

W9-07 · Connection v2 conformance module and suite

Status: not started (confirmed by grep). Prerequisites in place:

  • core v2 value, error, capability and session vocabulary (listed above);
  • the fixtures for all six targets, including MariaDB through MysqlFlavour::Mariadb (mysql_flavour.rs:6-9);
  • nextest per-package CI that picks up a new integration_mariadb target with no workflow change.

Remaining work

  • crates/test-support/Cargo.toml:
    • add tokio-util (workspace, lock 0.7.19) and tablepro-net (path, for FaultTransport over DirectTcpTransport);
    • add tokio features net, io-util and rt.
    • Keep sqlx and anything sqlite out of test-support: the host integration job has no libsqlite3-dev or libclang (build-linux.yml:183-186), and the review notes every sqlite build needs libclang.
  • New files under crates/test-support/src/conformance/:
    • mod.rs, conformance_engine.rs, network_conformance_engine.rs, pooled_conformance_engine.rs, editor_transaction_engine.rs, typed_conformance_engine.rs, bounded_ddl_engine.rs, unsupported_type_case.rs, conformance_failure.rs, test_liveness.rs, connection_suite_macro.rs;
    • cases/{metadata,deadlines,cancellation,pool,row_limit,read_only,writes,row_guards,statement_timeout,liveness,type_fallback,close,suite_flags}.rs;
    • plus cases/streaming.rs for the missing case below.
  • New crates/test-support/src/fault_transport.rs with black_hole(), and a write-pattern read stall that W2-12 commit_stall_deadline_commit_outcome_unknown_then_applied needs.
    • Design requirement found here: MySQL dials transport.route() through sqlx (TCP or the OpenSSH Unix socket per amendment FC-D2), so it cannot take an injected stream. FaultTransport must therefore also expose a route that points at a local proxy socket. A UnixSocket route works, because sqlx validates TLS against options.host (review "MySQL/MariaDB driver" row).
    • PG, MSSQL and ClickHouse use open().
  • crates/test-support/src/fake_connection.rs, fake_driver.rs and connection_script.rs: rewrite to v2 (owned by W2-11/W3-18/W3-19/W4-06, but W9-07's unit tests need them).
  • Per driver, crates/drivers/{postgres,mysql,sqlite,mssql,clickhouse}/tests/integration/main.rs (the switch converts the flat files) plus a new conformance_engine.rs.
  • crates/drivers/mysql/tests/integration_mariadb/{main.rs,mariadb_engine.rs} (new).
  • Dev-dependencies:
    • add tablepro-test-fixtures and tablepro-test-support to mysql, mssql, clickhouse and sqlite;
    • add rt-multi-thread to sqlite's tokio (sqlite/Cargo.toml has macros, rt only);
    • drop testcontainers-modules/rust_decimal from mysql and mssql.
  • Fixture extensions (see "Fixture gaps").
  • docs/testing.md:
    • add a Conformance section;
    • replace the stale v1 driver example (docs/testing.md:57-77, Postgres::default().with_tag("16-alpine") and conn.list_tables());
    • update the app | GTK4 + Relm4 components row (:10).

Dependencies: none new outside the lock. tokio-util 0.7.19, async-trait 0.1.92 and futures 0.3.34 are already locked.

Tests the spec requires

  • 14 cases plus suite_flags_match_driver_capabilities on six targets: postgres, mysql (8.4), integration_mariadb (11.4), mssql and clickhouse are docker; sqlite runs with docker: false.
  • 4 test-support unit tests against FakeConnection:
    • case_reports_expectation_on_wrong_outcome
    • deadline_case_fails_when_bound_exceeded (start_paused)
    • pool_case_fails_when_saturation_never_appears
    • suite_flags_test_fails_on_mismatched_endpoint
  • Also owed and not in the case list: the 50,000-row bounded-buffer case from the conflict resolution (plan.md:1069, details:12286, "per-driver conformance test runs a 50,000-row wide query and asserts raw-row buffering stays bounded").
    • Suggested: case 15 stream_read_backpressure_bounds_buffering with a stalled sink and an engine hook that proves the server is still sending (PG wait_event = 'ClientWrite', MySQL processlist, ClickHouse system.processes.read_rows).

How it runs

  • Docker cases carry #[ignore = "requires docker"] and run in the integration job through nextest --run-ignored only; locally through verify.sh --platform linux docker <pkg>.
  • The flags test and all SQLite cases are never ignored, so they run in meson unit (fast job) and in verify.sh --platform linux test tablepro-driver-sqlite.

Spec text that is stale or amended

  1. Files list build-aux/tests/test_integration_matrix.py and the matrix workspace test: stale.
    • The review amendment W9-03 deletes integration_matrix.py, and CI is nextest per package (build-linux.yml:157-200).
    • integration_mariadb needs no workflow change.
  2. crates/drivers/sqlite/tests/integration.rs (deleted): stale. The file never landed.
  3. "Only requires docker is accepted": stale. docs/testing.md:98-101 allows requires a Secret Service too, and clippy ignore_without_reason is denied workspace-wide (Cargo.toml lints).
  4. Case 12 and FaultTransport: under FC-D2/FC-D17 (system OpenSSH, MySQL on the ssh socket) injection is per engine, as above.
    • W1-11's keepalive_detects_black_hole "through SshServices.first_hop" cannot inject into the ssh binary. Use ContainerAsync::pause (testcontainers 0.27.3 async_container.rs:161) on the sshd fixture with a 1 s × 2 ServerAlive policy.
  5. Case 13 on PostgreSQL: point and int4range become decoded types under amendment W3-09/FB-D5 (postgres-protocol decoders), so they are no longer "unsupported". The case still holds (non-Null), but the type list should name types with no codec (composite, enum, tsvector, xml).
  6. Case 6 on PostgreSQL now exercises the review's cursor flight inside SAVEPOINT (amendment FC-D1). This is the right place for it.
  7. MSSQL invocation unsupported_types (sql_variant, hierarchyid, geography) and the cancel cases 2, 3 and 4:
  8. WriteBatch shape: the spec's constructions omit table, but write_batch.rs:10-15 has { kind, table, steps }. W3-19 adds foreign_keys: ForeignKeyMode in the same switch.

Size: about 3,000 changed lines:

  • module plus cases ~1,700
  • FaultTransport ~250
  • unit tests ~150
  • six engine impls ~800
  • docs ~80

W9-08 · TLS conformance suite

Status: not started (confirmed). In place:

  • TlsConfig, TlsMode, TlsFailure, ClientIdentity, TlsCapabilities.client_identity, and the classifier (crates/net/src/tls/classify.rs:6);
  • TLS-only fixtures with a tablepro_client certificate principal for PG, MySQL, MariaDB and ClickHouse; MSSQL with forceencryption.

Remaining work

  • New crates/test-support/src/conformance/{tls_conformance_engine.rs,client_identity_engine.rs,tls_suite_macro.rs,cases/tls.rs}.
  • A TLS engine per crates/drivers/{postgres,mysql,mssql,clickhouse}/tests/integration/ and mysql/tests/integration_mariadb/.
  • Delete the v1 crates/drivers/postgres/tests/tls.rs (72 lines, sqlx PgSslMode::Prefer; Prefer is no longer a mode, plan FC-D7). Keep tls_provider.rs if PG still builds a rustls config.
  • docs/testing.md TLS section.

Dependencies: none new.

Tests the spec requires: six cases (five where client_identity is false) plus tls_suite_flags_match_driver_capabilities, on postgres, mysql, integration_mariadb, mssql and clickhouse.

Spec gaps found here

  1. Case 3, wrong server name: the trait sets served_name and passes wrong.tablepro.test as TlsConfig.server_name.
    • Core rejects a server-name override on direct TCP for SshForwardOnly engines (tls_config.rs:85-90). The core test models MySQL that way (tls_config.rs:106-108); inferred, no driver declares it yet.
    • The engine trait needs a per-engine name_mismatch_connect_options(tls) hook. MySQL connects to 127.0.0.1, which is not a SAN (test_pki.rs:141-150 proves the mismatch); PG, MSSQL and ClickHouse use the override.
  2. Case 6, client identity: the identity principal is a different user from the password principal:
    • PG role tablepro_client (postgres_fixture.rs:133-135);
    • MySQL REQUIRE X509 user with its own password (mysql_fixture.rs:72, 109-111);
    • ClickHouse ssl_certificates user (clickhouse_fixture.rs:73-77).
    • ClientIdentityEngine needs certificate_connect_options(tls), not only client_identity().
  3. MSSQL cases 2 and 3: tiberius c1d2e74 turns the rustls failure into Error::Io { message: format!("{}", err) } (src/error.rs:110-116), so the driver can only classify UnknownIssuer and NameMismatch from rustls's Display text.
    • The texts come from rustls-0.23.45/src/error.rs:662-700, 986, which is fragile; the review calls this "coarser TLS classification" (amendment FC-D8).
    • Keep exact assertions, and add a driver unit test that pins the rustls text.
    • trust_cert_ca builds a store with only that CA (rustls_tls_stream.rs:108-148), not "plus system roots" as the review text says.
  4. The relay server-name test the spec keeps for W2 ("the relay server name") becomes an OpenSSH-socket server-name test (amendment FC-D2).
  5. served_name must be explicit wherever the override is allowed: in a container job, testcontainers resolves the host to the bridge gateway IP (W9-03 spec citing client.rs docker_hostname), which fails VerifyFull.

Size: about 800 changed lines (module ~330, five engines ~400, docs ~40), minus 72 deleted.

W9-09 · DDL execution conformance suite (Step 35)

Status: not started; blocked on W3-12 (confirmed):

  • crates/core/src/ddl/ has only mod.rs and referential_action.rs;
  • sql_ddl.rs is still present (core/src/lib.rs:47);
  • no DdlDialect, TableDraft, DdlCapabilities or fetch_table_definition.

Remaining work

  • New crates/test-support/src/conformance/{ddl_conformance_engine.rs,ddl_suite_macro.rs,cases/ddl.rs}.
  • ConformanceDdlEngine impls in the six driver integration targets.
  • docs/testing.md.

Dependencies: none.

Tests the spec requires: three cases (ddl_plans_apply_and_read_back, failed_ddl_batch_outcome_matches_ddl_atomicity, create_table_script_recreates_the_table) on six targets, with SQLite at docker: false.

Stale spec text

  • WriteBatch { kind, steps }: the real type has table (write_batch.rs:10-15). Use DdlPlan::into_write_batch() (W3-12 spec) instead of building it by hand.
  • "W3-16 later adds ForeignKeyMode to WriteBatch": W3-19 adds it in Step 30, so every batch here carries ForeignKeyMode::Enforced (W3-12 spec line "Every plan uses ForeignKeyMode::Enforced").
  • "global step 27": the landing sequence puts it at Step 35.

Size: about 700 changed lines.

Test requirements of every Step 30-39 task

The "Runs" column says where each suite runs:

  • local: this arm64 machine.
  • CI: GitHub only.
  • fast: the meson unit or gtk suite.
  • docker-host: the nextest host job.
  • docker-gtk: a job that does not exist yet.

Step 30

W2-11 (core contract)

  • Tests: OwnedCalls (11, paused time), write_ledger, error_category, row_limit, link_watch.
  • Status: partial. WriteLedger, LinkWatch and ErrorCategory exist with tests (write_ledger.rs, link_watch.rs:38-60). The conformance.rs sketch is superseded by W9-07.
  • Infra: none beyond tokio test-util.
  • Runs: fast, local.

W2-12 (PostgreSQL)

  • Tests: unit tests, plus integration/{tls,cancellation,editor_session,read_only,write_batch,metadata,pgbouncer,liveness}.rs, about 45 docker tests.
  • Infra: PostgresFixture, PgBouncerFixture (pool-size knob missing), FaultTransport, TestPki with ECDSA P-384 and Ed25519 server keys (missing: RSA only, test_pki.rs:26).
  • Amended by the review:
    • delete the pg_manager/pg_pooled tests;
    • select_inside_begin_uses_portal_row_limit becomes a cursor-in-SAVEPOINT test;
    • drop type_created_in_open_transaction_resolved_via_columns_resolved, since editor columns stay text-typed until Canceling connection does not actually cancel #1358;
    • add CALL/DO transaction control, COMMIT AND CHAIN, the SAVEPOINT probe, 0A000/42601 fallback, COPY FROM STDIN refused, and the 10,000-row enum/citext read_form guard;
    • tests/fixtures/pgbouncer.ini is superseded by PgBouncerFixture::files.
  • Runs: docker-host, local.

W2-13 (MySQL/MariaDB)

  • Tests: flavour_suite, non_transactional, row_limit, tls, editor_session.
  • Infra: MysqlFixture needs:
    • known root (mysql_root_in_transaction, SET GLOBAL);
    • a plaintext-allowed mode (disabled_mode_uses_rsa_not_cleartext);
    • start_in(network) with OpenSshFixture start_in (the tests re-pointed at the ssh socket by amendment FC-D2).
  • verify_identity_through_relay_with_server_name becomes an ssh-socket test.
  • Runs: docker-host, local.

W2-14 (SQLite)

  • Tests: integration/{open_semantics,sessions,metadata}.rs and unit tests.
  • Infra: tempfile only. W9-01's cases were never written, so they are new work.
  • Runs: fast, local.

W2-15 (SQL Server)

  • Tests: integration/{decode,tls,cancellation,read_only,sessions,write_batch}.rs; krb5_profile.rs stays.
  • Infra: MssqlFixture (pin CU14, not "2022-latest" as the spec says).
  • sql_variant, hierarchyid and cancel tests are gated on the tiberius pin (see W9-07 item 7). money_max_min_exact becomes the CAST interim (amendment W2-06/FB-D16).
  • Runs: docker-host, CI only.

W2-16 (ClickHouse)

  • Tests: integration/{keys_and_decimals,writes,tls,sessions}.rs and unit tests.
  • Infra: ClickHouseFixture. start_in(network) is missing, which the Unix-socket connector over SSH needs (amendment W2-16).
  • Runs: docker-host, local.

W2-17 (app surfaces)

  • Tests: unit error_text/tls_remedy/pem_validation, plus 7 connect_dialog #[gtk::test].
  • Infra: gtk-rs ConnectDialog (see GTK section) and Tasks over test_runtime().
  • Runs: fast (gtk), local.

W3-05 / W3-06 (core model and dialect)

  • Tests: about 40 core unit tests against test_dialect.rs, which exists (core/src/dialect/test_dialect.rs).
  • Runs: fast, local.

W3-07 (dialects)

  • Tests: golden tests, plus F-022 integration on PostgreSQL 18, MySQL, MariaDB, MSSQL, ClickHouse and SQLite.
  • Infra: PostgresFixture needs an image version parameter (only 17-alpine, fixture_image.rs:10-13).
  • Runs: docker-host, plus fast for SQLite.

W3-08 (catalog)

  • Tests: unit tests, plus catalog_matches_information_schema on PG 13 and 18.
  • Infra: the same version knob.
  • Runs: docker-host.

W3-09 / W3-10 / W3-11 (decode, typed defaults, app SQL)

  • Tests: unit and golden tests, integration on PG, MySQL and MariaDB, SQLite decode.rs/row_identity.rs, MSSQL alter_default..., and 2 #[gtk::test] structure tests.
  • Runs: docker-host, fast; MSSQL CI only.

W3-18 (table definitions)

  • Tests: unit tests, plus fetch_table_definition_* on 5 engines and SQLite catalog.rs.
  • Infra: v2 FakeConnection.
  • Runs: docker-host, fast.

W3-19 (FK suspension)

  • Tests: SQLite write_batch_foreign_keys.rs, contract tests, and MySQL workspace_session_strips_no_backslash_escapes.
  • Infra: MysqlFixture global sql_mode (root or a server arg; missing).
  • Runs: fast, docker-host.

W4-06 (Sessions)

  • Tests: about 30 unit and paused-time tests, 4 app #[gtk::test], and app/tests/session_postgres.rs (docker).
  • Infra: v2 FakeDriver/FakeConnection/FakeEditorSession, CountingHold. The docker test cannot build on the host job (GTK 4.22).
  • Runs: fast; docker-gtk (missing) or move to tablepro-session.

W1-11 (SSH, keyring, logging off)

  • Tests: ssh/tests/transport.rs (6), route.rs/authentication.rs (do not exist; the russh plan is void under FC-D17), driver statement_logging.rs (SQLite, MySQL), and core/storage/app unit tests.
  • Infra:
    • OpenSshFixture start_in(network) with PostgresFixture (TLS) on the same network;
    • pause-based black hole;
    • an ssh-agent host helper (missing);
    • tracing-subscriber 0.3.23 dev-dependency for drivers.
  • Migrate openssh_session.rs off its alpine container onto OpenSshFixture, and add kbd-interactive, @cert-authority, KRL and agent cases (DoD: "ssh-agent, keyboard-interactive, two-hop").
  • Runs: docker-host, local.

W6-11 (error mapping)

  • Tests: 8 pure tests.
  • Runs: fast.

W6-12 (EditorSession editor)

  • Tests: 9 pure tests and 17 #[gtk::test].
  • Infra: FakeEditorSession (missing), and an editor rewritten as a gtk-rs subclass.
  • Runs: fast, local.

W6-13 (connect dialog v2)

  • Tests: 11 pure tests, 8 #[gtk::test] with FakeDriver capabilities and FakeConnector, and the storage pending_save_store_connection_writes_file.
  • Infra: FakeConnector (missing). agent_is_default_with_ssh_auth_sock must inject the environment, not set_var.
  • Runs: fast, local.

W9-07 / W9-08: above.

Steps 31-39

W2-19: docs only; no test.

W4-07

  • Tests: 3 unit tests, 1 #[gtk::test], and a CI grep for thread_local!.
  • Status: not started. thread_local! is still in services/change_tracker.rs:724.
  • Runs: fast.

W6-14

  • Tests: 9 named format tests.
  • Status: done under other names. 7 of 9 match in ui/editor/format_plan.rs:75-165; clickhouse_slash_comment... and mysql_minus_minus..._kept_verbatim are named a_slash_comment_at_the_end_of_a_statement_still_ends_it (:113) and mysql_minus_minus_arithmetic_stays_arithmetic (:134).
  • Runs: fast.

W3-17

  • Tests: 8 export unit tests.
  • Status: largely done, renamed. json_non_finite_float_is_string is at core/src/export/json.rs:222; a_time_takes_its_precision_from_its_column at value_text.rs:311. The spec names are absent.
  • Runs: fast.

W6-15

  • Tests: 3 #[gtk::test].
  • Status: done, renamed (ui/editor/statement_cursor.rs:62-85).
  • Runs: fast.

W4-08

  • Tests: 10 tracker unit tests and app/tests/tracker_save_postgres.rs.
  • The docker test needs a docker-gtk job, or a move out of the app crate.
  • Runs: fast; docker-gtk.

W4-09

  • Tests: 5 WeakRef #[gtk::test].
  • closed_editor_frees_source_view "dropping Controller" becomes dropping the SqlEditor widget once Relm4 goes.
  • Runs: fast.

W4-10

  • Tests: RequestSequencer and SchemaCatalog unit tests; browse, structure and app #[gtk::test] with v2 FakeConnection (delays, cancel observation, ConnectionLink::replace); session_postgres.rs run_cancellable....
  • Runs: fast; docker-gtk.

W3-12

  • Tests: core unit tests, per-engine golden tests, and 3 #[gtk::test] in the structure tab.
  • Runs: fast.

W9-09: above.

W5-01

  • Tests: the existing grid tests move unchanged.
  • Status: not started (ui/grid.rs is still one file of 2,414 lines).
  • Runs: fast.

W3-13

  • Tests: 5 unit tests and integration on MySQL (GLOBAL NO_BACKSLASH_ESCAPES), MariaDB, PG and MSSQL.
  • Infra: MysqlFixture sql_mode knob.
  • Runs: docker-host.

W4-11

  • Tests: workspace_store and record tests (tempdir); persist_debouncer and editor #[gtk::test].
  • typing_emits_content_changed_without_text becomes a payload-less GObject signal checked with SignalLog::count.
  • Runs: fast.

W4-12

  • Tests: close-step, save-outcome and ledger units; FakeConnection workspace tests; close-guard #[gtk::test] through adw::AlertDialog response.
  • Infra: v2 FakeConnection whose write_batch blocks until cancelled.
  • Runs: fast.

W3-14

  • Tests: core and golden tests (one proptest; proptest 1.11.0 locked), integration on PG, MySQL, MSSQL and SQLite ddl.rs, and 1 #[gtk::test].
  • Runs: docker-host, fast; MSSQL CI only.

W5-02

  • Tests: 4 #[gtk::test] on ColumnView, unit tests, and BrowseTab FakeConnection tests.
  • Runs: fast.

W3-15

  • Tests: 5 unit tests and 3 MSSQL integration tests.
  • Runs: CI only.

W5-03

  • Tests: 4 #[gtk::test] (GridCell #[cfg(test)] render counter) and 1 unit test.
  • Runs: fast.

W6-17

  • Tests: pure scope and fuzzy tests, and 5 #[gtk::test] with a FakeConnection catalog.
  • Runs: fast.

Size of the test work across Steps 30-39 (estimated, not mine alone to write):

  • Step 30 driver test trees: about 6,000-8,000 lines, replacing ~1,630 v1 lines.
  • v2 fakes (FakeConnection/FakeDriver/FakeEditorSession/FakeConnector/FaultTransport): about 1,000.
  • Fixture extensions: about 300.
  • Steps 31-39 tests: about 3,000.

Fixture gaps (what the specs need that does not exist)

The amendment FC-D11/W9-02 sshd fixtures (keyboard-interactive, @cert-authority, KRL, scripted askpass) exist, as does PgBouncer. Missing:

  1. PostgresFixture image version: W3-07 needs PG 18 and W3-08 needs PG 13 and 18. Only 17-alpine is pinned (fixture_image.rs:10-13).
  2. MysqlFixture:
    • a known root password (W2-13 mysql_root_in_transaction; W3-13/W3-19 SET GLOBAL sql_mode). Today it is RANDOM_ROOT_PASSWORD (mysql_fixture.rs:36).
    • an option without --require-secure-transport (W2-13 RSA-without-TLS; the TLS conformance Disable case wants the TLS-only default). Today it is fixed in server_arguments (:57-65).
    • start_in(&TestNetwork) for the ssh-socket tests.
    • extra server arguments.
  3. OpenSshFixture::start_in(variant, &TestNetwork), so sshd reaches a DB container by alias and two-hop ProxyJump chains work. Only PostgresFixture has start_in (postgres_fixture.rs:36-38).
  4. ClickHouseFixture::start_in(&TestNetwork) for the Unix-socket connector over SSH (amendment W2-16).
  5. PgBouncerFixture default_pool_size (W2-12 transaction_pool_editor_leaves_no_session_state: pool size 1).
  6. TestPki key algorithm choice: W2-12 needs ECDSA P-384 and Ed25519 server certificates for the channel-binding tests (amendment FC-D23 keeps them). It is RSA only (test_pki.rs:26, 33, 77).
  7. An ssh-agent helper: a host ssh-agent -a <sock> plus ssh-add of an SshKeyPair (W1-11/W6-13, DoD "ssh-agent").
  8. Optional: un-ignore generated_pki_matches_the_files_the_fixtures_copy (tests/fixtures.rs:410-419), which needs no docker.

GTK tests when Relm4 goes

Already aligned with the review: no Relm4 harness was built. Tests use SignalLog (connect_local), find_by_action_name, WidgetExt::activate_action, and assert_labelled (targeted test_accessible_* under GTK_A11Y=test, meson crates/app/meson.build:55).

Per rewritten file in Steps 30-39:

  • A Relm4 Controller plus sender().send(Input::X) plus controller.model() becomes:
    • construct the subclass (with crate::register_test_resources() when it has a template, as at ui/preferences_dialog/mod.rs:175-186);
    • drive it through template children, properties or activate_action;
    • observe custom signals with SignalLog.
  • Output enums become GObject signals. Tests assert emissions, e.g. W6-13 connect_emits_submission_without_establishing, W4-11 typing_emits_content_changed_without_text.
  • WeakRef leak tests (W4-09) drop the widget, not a Controller.
  • Files whose existing GTK tests change in these steps:
    • ui/connect_dialog.rs (8 tests, W6-13/W2-17);
    • ui/editor/mod.rs (W6-12, W4-09, W4-11);
    • ui/structure_tab/mod.rs (W3-10, W3-12, W3-14, W4-09);
    • ui/browse_tab.rs (W4-10, W4-12, W5-02);
    • ui/app/mod.rs (W4-06, W4-10). Open question: does the root stay Relm4 until W4-13/W4-14?
  • Remove the relm4::spawn* bans from crates/app/clippy.toml only in the commit that deletes relm4.

Where tests can run

  • On this arm64 machine:
    • all fast-suite tests (meson unit and gtk, with the Wayland display);
    • every docker suite for PostgreSQL (any version), MySQL 8.4, MariaDB 11.4, ClickHouse 25.8, PgBouncer and OpenSSH;
    • the SSH and fixture packages (host ssh/ssh-keygen present).
    • Command: verify.sh --platform linux docker <pkg>. It falls back to cargo test -- --ignored --test-threads=1, since nextest is not installed.
  • Only in CI:
    • every SQL Server docker test: W2-15, W3-15, the MSSQL parts of W3-07/08/10/11/13/14/18, and W9-07/08/09 on mssql;
    • anything that needs the docker-gtk job.
  • A local verify.sh docker tablepro-driver-mssql fails on image pull, so the MSSQL debug loop runs through CI.

CI changes the tests need

  1. A docker job that can build the app for app/tests/session_postgres.rs (W4-06, W4-10) and tracker_save_postgres.rs (W4-08):
    • the ubuntu:26.04 container with -v /var/run/docker.sock:/var/run/docker.sock (W9-03 spec's integration-gtk design);
    • or move Session and the tracker save path into a non-GTK crate (tablepro-session exists, crates/session/Cargo.toml) and add that package to the host matrix.
    • The host job cannot compile GTK 4.22 (build-linux.yml:183-186).
  2. The host matrix is hand-listed (:164-170). Any new docker package (session, net) must be added there.
    • The review's alternative is --workspace --exclude tablepro, which would need libsqlite3-dev and libclang on the host if sqlite crates join the graph.
  3. Runtime: each conformance case starts its own container, and test groups run one at a time (.config/nextest.toml). MSSQL alone reaches about 50 starts at 30-60 s each.
    • Consider max-threads = 2..3 for driver groups, since each test owns its container, and a nextest slow-timeout.

Implementation order

  1. Fixture extensions (gaps 1-7) as one additive test(linux): commit before the switch.
  2. FaultTransport (route proxy plus stream wrapper) in test-support, additive before the switch. It needs only the core Transport and DirectTcpTransport, which exist.
  3. On the Step 30 stack, after W2-11: v2 fakes (FakeConnection, FakeDriver, FakeEditorSession, FakeConnector) with W2-11/W4-06; then the conformance traits, ConformanceFailure, macro and cases; then the 4 test-support unit tests.
  4. SQLite engine first (fast suite, local), then PG, MySQL plus MariaDB, and ClickHouse (local docker), then MSSQL (CI) with narrowed flags until the tiberius pin moves.
  5. W9-08 TLS module and engines once each driver's TLS lands in the stack.
  6. CI: the docker-gtk job (or relocation) for the app docker tests, in the same switch commit.
  7. docs/testing.md Conformance and TLS sections; replace the v1 example.
  8. Step 35: W9-09 inside W3-12's commit.

Open questions and risks

  • Blocker: there is no v2 Connection (core/src/connection.rs:64-80) and no DatabaseDriver::capabilities() (driver.rs:7-48). W9-07 and W9-08 cannot compile until W2-11.
  • Blocker for MSSQL cases: tiberius c1d2e74 has no cancel API and panics on sql_variant/UDT (type_info.rs:317). Either bump the pin past tiberius-rs feat: current statement highlighting and large document safety caps #442/Crashing on launch #443/feat: multi-select, bulk actions and reorder for welcome window #449 or ship MSSQL with unsupported_types: false and discard-based cancel, then re-enable.
  • Spec gaps (decide before writing): TLS case 3 on SshForwardOnly engines; the ClientIdentityEngine certificate principal; the missing 50,000-row streaming case; FaultTransport injection for MySQL.
  • Unanswered: where the GTK-linked docker tests run (new container job, or Session moved to tablepro-session).
  • Timing flakiness: the deadline and cancel_grace cases (1 s + 1 s bounds) run in parallel threads in the fast job (SQLite) and on shared runners. Keep test_liveness() margins and use start_paused where no real server is involved.
  • CI time from per-test containers with serial groups, especially MSSQL.
  • MSSQL TLS classification depends on rustls Display text through tiberius's stringified Io error.
  • Test-name drift: the W6-14, W6-15 and W3-17 tests exist under names that differ from the spec. The regression map (W9-16) must map the real names.
  • If golden SQL adopts insta (review "query/DDL building" row): pin insta 1.48.0. 1.49.0 was published 2026-10-03, under the two-week rule (crates.io API).
  • Policy tension: linux/AGENTS.md says "No mocks" while the plan puts fakes in crates/test-support. The fakes already exist (fake_connection.rs), so state the rule as "fakes only in test-support" in docs/testing.md:188-192.

In a debug build the tiberius connect future, copied into every caller up
to the pool, took 2.09 MB of a 2 MiB thread stack. Each MSSQL docker test
aborted, skipped Drop, and left its container running until the CI runner
ran out of memory.


tiberius passes @params to sp_executesql even when empty, and SQL Server
then rejects CREATE VIEW and CREATE TRIGGER with Msg 156.
chrono has no %.7f, so the formatter failed and the test panicked.
…hops

OpenSSH before 9.6 pastes a -J hop into a proxy command run by sh -c, so
an imported jump host such as $(cmd) ran on connect.
A ProxyJump hop prompts through the same askpass first, and was handed the
destination's password. The owner is the user@host ssh -G resolves; any
other password prompt asks the user and names the host asking.
After ALTER TABLE ADD COLUMN, SELECT * in the same tab panicked the sqlx
worker: the cached statement still listed the old columns.
On MyISAM or during DDL the server may have run a statement before the
connection dropped; the ledger called it refused and told the user nothing
applied.
tiberius sends datetime2(7), so a datetime key of .003 compared as
.0033333 and the save failed its row guard.
Drivers read XML back as text, but an edit became Value::Other, which a
SQL Server text parameter refuses, so no XML cell could be saved.
PostgreSQL has no LIKE for json, jsonb, xml or enums and no = for json or
xml; SQL Server has neither for xml. The dialect now names the text form
the filter compares.
Opening another connection tore the workspace down without the check
Disconnect makes. Both now also count open editor transactions, which the
server rolls back when the tab's connection closes.
…s#459

tiberius 0.13 drops a connection whose attention lands while the server is
still waiting. The fix is tiberius-rs/tiberius#460.
…nswer

A MariaDB max_statement_time stop is a reply, so the write did not apply;
only a write whose reply never came is unknown.
@datlechin
datlechin marked this pull request as ready for review October 13, 2026 06:57
@datlechin
datlechin merged commit 726c221 into linux Oct 13, 2026
29 of 31 checks passed
@datlechin
datlechin deleted the refactor/linux-driver-contract-v2 branch October 13, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform-linux About the Linux app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant