Skip to content

New Features Round 1, 2 & 3#8

Open
therecluse26 wants to merge 24 commits into
mainfrom
feature/ai-updates
Open

New Features Round 1, 2 & 3#8
therecluse26 wants to merge 24 commits into
mainfrom
feature/ai-updates

Conversation

@therecluse26

Copy link
Copy Markdown
Collaborator

PR: Round 1 + Round 2 agent-ergonomics & read-only hardening

Branch feature/ai-updates delivers the first two roadmap rounds scoped under REF-258, plus the earlier capability-hardening work, bringing Plenum's read-only enforcement, output contract, and connection ergonomics up to spec. 11 commits, +5,117 / −465 across 33 files.

Summary

This branch closes out the highest-risk correctness gaps in capability enforcement, hardens read-only mode at the session level for every engine, and adds the agent-facing quality-of-life features (connection listing, ping, byte budgets, versioned schemas) that make Plenum safer and more deterministic for autonomous callers.

Added

  • Versioned JSON Schemas for every output envelope (REF-265) — Draft-7 schemas in schemas/ (error_envelope, connect_success, introspect_success, query_success), generated from the Rust types via schemars (cargo run --bin generate-schemas). A tests/schema_drift.rs test fails CI if checked-in schemas diverge from live types.
  • meta.contract_version on all envelopes — agents can guard against silent breaking changes by checking a single field.
  • plenum connect --list (REF-268) — enumerate saved connections as JSON without opening a database.
  • plenum connect --test (ping) (REF-267) — validate a stored/explicit connection end-to-end; adds rusqlite as a dev-dependency for SQLite test fixtures.
  • plenum query --max-bytes (REF-269) — byte-budget guard that caps serialized result size to stay under MCP token limits, complementing --max-rows.
  • plenum introspect column comments + row estimates (REF-263) — richer schema output for all three engines.
  • plenum connect --password-env (REF-36) — wired end-to-end so credentials can be referenced from the environment instead of stored in plaintext.
  • CI live-db-tests job — runs the previously #[ignore]d Postgres + MySQL integration tests against service containers, isolated from the main test signal.

Changed

  • Session-level read-only enforcement for all engines (REF-261) — read-only is now enforced at the connection/session layer, not only by statement inspection, closing bypass vectors uniformly across PostgreSQL, MySQL, and SQLite.

Fixed

  • WITH-CTE DML bypass (REF-41) — statements that smuggle INSERT/UPDATE/DELETE inside a WITH CTE are now rejected across all engines.
  • SQLite read-only PRAGMA allowlist tightened (REF-44) — only safe introspection PRAGMAs are permitted.

Tests & docs

  • Capability-enforcement coverage expanded for the highest-risk gaps (REF-40) and SQLite type-coercion behavior pinned (REF-39).
  • New/updated snapshot tests for every error and success envelope (tests/output_validation.rs, insta snapshots).
  • README.md documents the schemas/ directory, the generate-schemas binary, the drift test, and the contract_version guard.

Compatibility notes

  • No breaking changes to the CLI surface — three commands (connect, introspect, query) unchanged; new behavior is additive flags.
  • Output envelopes now carry meta.contract_version; existing consumers that ignore unknown meta fields are unaffected.

Verification

  • cargo test (unit + snapshot + drift) — expected green in CI.
  • New live-db-tests CI job exercises Postgres 16 and MySQL 8.0 integration paths.

therecluse26 and others added 13 commits May 14, 2026 12:42
- Add per-variant ErrorEnvelope snapshot tests for every PlenumError variant
  (CapabilityViolation, ConnectionFailed, QueryFailed, InvalidInput,
  EngineError, ConfigError) so the agent-facing JSON wire format is
  pinned and any future code/message drift requires explicit review.

- Add multi-statement rejection tests through the public validate_query()
  API for all three engines (Postgres, MySQL, SQLite) plus a SELECT-then-DDL
  payload. Multi-statement preprocessing is engine-agnostic today; the
  parameterized tests will catch any future engine-specific bypass.

- Add a CI live-db-tests job that brings up Postgres 16 and MySQL 8 service
  containers and runs the 14 #[ignore]'d engine integration tests with
  --ignored. Kept as a separate job so a transient service outage doesn't
  poison the main test signal.

Items 4 (password_env tests) and 5 (panic handler test) from REF-40's plan
are already explicitly inside REF-36's and REF-37's own acceptance criteria
and need no separate child issues. Item 6 (MCP stdio blackbox) is already
covered: tests/mcp_protocol.rs spawns the compiled plenum binary, which
dispatches to plenum::mcp::serve() in src/main.rs — the actual MCP entry.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds four targeted tests to tests/edge_cases.rs covering audit checklist
items that were previously untested:

- test_numeric_affinity_promotes_real_to_integer: SQLite NUMERIC affinity
  stores 3.0 as INTEGER 3; verify JSON emits an integer, not a float.
- test_real_nan_and_infinity_become_null: divide-by-zero paths must not
  panic and must produce valid JSON.
- test_blob_with_valid_utf8_bytes_is_base64_encoded: a BLOB containing the
  UTF-8 bytes of "Hello" must still be base64-encoded, distinguishable
  from a TEXT column with the same string. Guards against the MySQL-style
  bug tracked in REF-46.
- test_dynamic_typing_all_storage_classes: smoke for every SQLite storage
  class (INTEGER/REAL/TEXT/BLOB/NULL) in one dynamic column.

Adds a create_test_db_named() helper to avoid the nanosecond timestamp
collisions observed in the shared create_test_db() under parallel test
execution.

Refs REF-39. Engine-level bugs tracked in REF-45 (Postgres), REF-46
(MySQL), and REF-47 (output marker).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
CLI accepted --password-env but dropped it on the floor:
non_interactive_connect ignored the value with a TODO, and
save_connection hardcoded password_env: None when constructing
StoredConnection. The flag silently no-op'd despite the
StoredConnection::resolve() machinery already being in place.

Changes:
- non_interactive_connect honors --password-env: validates the env
  var resolves at connect time (clear error if unset/empty), rejects
  --password and --password-env together, rejects --password-env for
  sqlite, and propagates the env var name to the caller.
- save_connection takes password_env: Option<String> and persists it
  on the StoredConnection so resolve_connection() can read it back.
- Unit tests for password_env JSON round-trip, omission when None,
  and env-var precedence over inline password on resolve.
- CLI integration test exercises end-to-end success, missing-var
  error, and mutual-exclusion error against the built binary;
  asserts no secret ever lands in stdout or the persisted config.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…-41)

The prior `is_read_only_*` allowlist accepted any query starting with
`WITH ` on the assumption that CTEs are read-only. PostgreSQL writable
CTEs (`WITH x AS (INSERT/UPDATE/DELETE ... RETURNING ...) SELECT ...`)
and MySQL/SQLite trailing-DML forms
(`WITH cte AS (SELECT ...) INSERT/UPDATE/DELETE ...`) both bypass this
prefix check while still executing writes server-side.

Replace the bare `sql.starts_with(\"WITH \")` admission with
`is_safe_cte_query`, which:

1. Requires the `WITH ` prefix, and
2. Scans the (already comment-stripped, uppercased) body with
   `scan_for_write_keyword` for any DML/DDL token
   (INSERT, UPDATE, DELETE, MERGE, REPLACE, COPY, TRUNCATE, DROP, ALTER,
   CREATE, GRANT, REVOKE, RENAME, ATTACH, DETACH, LOAD, VACUUM, REINDEX,
   LOCK, UNLOCK, CALL, INTO), respecting `'...'`, `\"...\"`, and
   `` `...` `` quoted regions so that keywords inside string literals or
   quoted identifiers do not cause false positives.

Coverage:
- Postgres writable CTE INSERT/UPDATE/DELETE/COPY: rejected
- MySQL/SQLite trailing-DML after CTE: rejected
- EXPLAIN-prefixed CTE-DML: rejected (via existing EXPLAIN strip)
- `WITH cte AS (SELECT ...) SELECT * FROM cte`: still allowed
- Recursive read-only CTEs: still allowed
- CTEs with DML keywords inside string literals / quoted identifiers /
  identifier substrings (e.g. `delete_at`): still allowed

15 new tests added covering all acceptance criteria plus regression
guards for the false-positive cases.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
The previous `is_read_only_sqlite` accepted any query starting with
`PRAGMA `, which admitted destructive PRAGMAs:

  - PRAGMA writable_schema = 1 (enables sqlite_master modification — the
    most dangerous bypass)
  - PRAGMA wal_checkpoint[(FULL|RESTART|TRUNCATE)]
  - PRAGMA optimize / incremental_vacuum
  - PRAGMA page_size = 4096 / auto_vacuum = FULL (structural mutation)
  - PRAGMA journal_mode = WAL (settable PRAGMAs in assignment form)

Replace the blanket allow with `is_safe_pragma`, a two-allowlist gate:

  1. READ_ONLY_SQLITE_PRAGMAS_WITH_ARGS — names whose `PRAGMA name(arg)`
     form is a pure read (table_info, index_list, foreign_key_list, …).
  2. READ_ONLY_SQLITE_PRAGMAS_BARE — names whose bare `PRAGMA name` form
     is read-only. Settable PRAGMAs (journal_mode, user_version, …) only
     appear here, so their `= value` and `(value)` setter forms are
     rejected.

Any query containing `=` is rejected unconditionally (the assignment
setter form). Unknown PRAGMAs fail closed, so future SQLite versions
that introduce new write PRAGMAs are rejected until the allowlist is
explicitly updated.

Defence-in-depth: connections still open with SQLITE_OPEN_READ_ONLY in
src/engine/sqlite/mod.rs, but the query-layer check gives clearer
errors before the driver call and prevents PRAGMAs (like
writable_schema) that are accepted on read-only connections.

This commit also lands the previously-uncommitted dirty state from the
parent REF-21 / REF-35 read-only enforcement audit work that the same
engineer agent left in the working tree (REF-41 CTE write-keyword scan,
REF-43 SELECT INTO OUTFILE/DUMPFILE/@var block), so the audit fixes
are now on the branch atomically.

Tests: 26 new SQLite PRAGMA tests covering every acceptance criterion:
writable_schema (= 1, = ON, = 0, paren-form, bare), wal_checkpoint
(bare, FULL, RESTART, TRUNCATE), optimize, incremental_vacuum,
journal_mode (bare allowed, = WAL rejected, (WAL) rejected),
user_version (bare allowed, = N rejected), page_size = 4096,
auto_vacuum = FULL, unknown-name fail-closed, empty PRAGMA, malformed
trailing token; plus regression tests that table_info(users),
database_list, table_list, index_list(users), foreign_key_list(users),
integrity_check, compile_options, schema_version still pass.

cargo test: 201/201 passing.
cargo build --release: clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…es (REF-261)

Defense-in-depth layer: the database itself now enforces read-only mode on every
execute() call, independent of the SQL parser.

- SQLite: open_connection(read_only=true) → SQLITE_OPEN_READ_ONLY at OS/VFS level;
  adds unit test proving DML and DDL fail on a raw connection bypassing the parser
- PostgreSQL: SET default_transaction_read_only = ON immediately after connect;
  adds ignored integration test verifying the session flag and write rejection
- MySQL: SET SESSION TRANSACTION READ ONLY immediately after connect;
  adds ignored integration test verifying transaction_read_only=1 and DML rejection
  (MySQL DDL is non-transactional and remains covered exclusively by the parser)

All existing tests pass; cargo build --release clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Extend plenum introspect with two new metadata fields across all three engines:

- `ColumnInfo.comment`: column-level comment/description
- `TableInfo.comment`: table-level comment/description
- `TableInfo.row_estimate`: engine statistics row count estimate

Both fields always serialize (explicit null, never omitted) per spec.

Engine behaviour:
- PostgreSQL: column comments via pg_description JOIN; table comment via
  obj_description(pg_class); row_estimate from reltuples (null when -1,
  meaning never analyzed)
- MySQL: column_comment from information_schema.columns; table_comment +
  table_rows from information_schema.tables; empty strings normalised to null
- SQLite: comment always null (no native comment storage); row_estimate from
  sqlite_stat1.stat after ANALYZE, null otherwise

Tests:
- SQLite: two inline unit tests covering null output before ANALYZE and
  non-null row_estimate after ANALYZE
- MySQL/PostgreSQL: ignored integration tests exercising full round-trip with
  COMMENT ON / CREATE TABLE COMMENT metadata

Also fixes pre-existing compilation errors introduced by the linter:
- Capabilities.offset field added to struct literals in mcp.rs and tests
- mcp.rs execute calls updated with empty params slice (&[])
- json_to_pg_value return type adds Send to unblock the Postgres execute future

All 218 tests pass; cargo build --release clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… (REF-268)

- plenum connect --list emits { connections:[...], default:"name" }
- Connections sorted alphabetically for deterministic output
- Secrets redacted: password field never emitted; password_env shows var name only
- Empty/missing project config returns empty list, not an error
- --project-path honored for cross-project listing
- Added list_connections_raw() to config module (raw StoredConnection, no resolve)
- Fixed pre-existing build failures: Deserialize removed from output-only envelope
  types (incompatible with &'static str field), contract_version snapshots updated,
  max_bytes/truncated_by fields added to test Capabilities/QueryResult initializers

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…F-269)

- Add max_bytes: Option<usize> to Capabilities struct
- Add apply_byte_budget() that trims rows at row boundaries when the
  cumulative serialized size would exceed the budget
- Add rows_truncated serialization (skip_serializing_if = false) and
  truncated_by: Option<String> to QueryResult so MCP callers see both
  flags directly in the response body
- Add truncated_by: Option<String> to Metadata for the CLI envelope's
  meta section; set to "bytes" when max_bytes fires, omitted otherwise
- Wire --max-bytes CLI arg into handle_query; wire max_bytes JSON param
  into MCP tool_query; both apply apply_byte_budget post-engine
- Add unit tests in engine::tests and integration tests in
  tests/output_validation.rs covering: truncation, no-truncation, zero
  budget, metadata signalling, and per-engine SQLite path

All 176 lib + 27 integration tests pass; cargo build --release clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add 5 integration tests in tests/cli_connect.rs covering:
  - Reachable SQLite connection returns ConnectionInfo envelope
  - Unreachable/missing file returns CONNECTION_FAILED (no credential leakage)
  - --test with --name uses a saved connection by name
  - --test does not write config to disk
  - --test and --save are mutually exclusive (clap-level rejection)
- Add rusqlite to dev-dependencies so tests can create valid SQLite fixtures
- All 8 cli_connect tests pass; all 265+ tests pass; cargo build --release clean

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…s (REF-265)

- Add `JsonSchema` derive to all output types (ErrorEnvelope, SuccessEnvelope,
  Metadata, ConnectionInfo, QueryResult, IntrospectResult and sub-types)
- Add `generate-schemas` binary (cargo run --bin generate-schemas) to regenerate
  schemas/error_envelope.json, connect_success.json, introspect_success.json,
  and query_success.json from live Rust types via schemars
- Add drift test in tests/schema_drift.rs that fails CI when types change
  without a schema regen
- Export IntrospectResult, IndexSummary, ViewInfo from crate root
- Fix rows_truncated to carry #[serde(default)] so the schema marks it optional
- Remove stale hand-written success_envelope.json (superseded by command-specific schemas)
- Document schemas/ in README with table, regen instructions, and drift test note

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…(REF-272)

- Add src/dsn.rs: per-engine DSN/URL parsing (postgres://, postgresql://, mysql://, sqlite:)
- Engine inferred from scheme; invalid schemes fail fast with structured error
- Credentials redacted in all error output via redact_dsn()
- --dsn is mutually exclusive with --name and explicit connection flags; never mutates stored config
- Wire --dsn into CLI (query, introspect) and MCP tool schemas (dsn field)
- Fix pre-existing duplicate field errors in config/mod.rs test fixtures
- 27 new unit tests: all parsers, redaction, scheme errors, edge cases
- cargo test --no-default-features --features postgres,sqlite: 215 passed, 1 pre-existing keychain failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ces (REF-271)

- StoredConnection gains password_command (shell command) and keychain_entry
  (OS keychain via keyring crate) fields; exactly one indirect source per
  connection is enforced in resolve()
- password_command runs via sh -c; non-zero exit or empty stdout fails fast
  with no secret in the error message
- keychain_entry looks up via keyring::Entry; runtime uses the real OS
  keychain (macOS/Windows/Linux Secret Service)
- Test isolation: cfg(test) thread-local MOCK_KEYCHAIN replaces keyring's
  EntryOnly mock (which shares no state across Entry::new calls) so
  keychain tests are deterministic and require no external services
- save_connection signature extended with new optional fields; all call
  sites in main.rs updated; KeychainEntry re-exported from lib.rs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@therecluse26 therecluse26 changed the title New Features Round 1 & 2 New Features Round 1, 2 & 3 Jul 14, 2026
therecluse26 and others added 11 commits July 14, 2026 20:20
CI Rustfmt and Clippy (-D warnings) were failing. Apply rustfmt and resolve
the pedantic/style lints:

- doc_markdown: backtick identifiers in doc comments (regenerates schemas/,
  whose descriptions are derived from Rust doc comments via schemars)
- uninlined_format_args, manual_string_new, redundant_clone, approx_constant,
  needless_debug_formatting: mechanical fixes
- trivially_copy_pass_by_ref: allow on is_false (serde skip_serializing_if
  requires fn(&T) -> bool)
- type_complexity: extract RawConnectionListing alias
- similar_names / needless_pass_by_value: rename bindings; take ssl_mode as
  Option<&str> in build_tls_config
- update stale benches/query.rs to the current execute(config, query, params,
  caps) signature

No behavior change; contract_version and schema structure unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new live-db-tests CI job (running the #[ignore]d Postgres+MySQL integration
tests against service containers) was failing. Root causes and fixes:

Real code bugs:
- MySQL wildcard NULL panic: DATABASE() returns SQL NULL in wildcard mode;
  get::<String>() panics converting NULL (mysql Row::get returns None only for
  out-of-range indices, not NULL values). Read as Option<Option<String>> in
  validate_connection and determine_target_schema. Fixes the wildcard tests.
- Timeout backstop race (Postgres + MySQL): the client-side tokio backstop used
  the same duration as the server-side statement_timeout / MAX_EXECUTION_TIME,
  so the client guard could win the race and surface QUERY_FAILED instead of
  QUERY_TIMEOUT. Add a fixed grace (CLIENT_TIMEOUT_BACKSTOP_GRACE) so the server
  cancels first; the client guard now only fires if the server never responds.

Test corrections:
- MySQL REF-263 test used an empty root password; the mysql:8.0 container
  requires MYSQL_ROOT_PASSWORD. Use 'password'.
- MySQL timeout test used SELECT SLEEP(10), which MAX_EXECUTION_TIME interrupts
  but still returns success (SLEEP returns 1) -> no error. Use a heavy
  information_schema cartesian join, which is cancelled with error 3024.
- MySQL verify-full TLS test could never pass against the container's
  auto-generated self-signed certs (unknown CA + CN != localhost). Retarget to
  sslmode=require, which establishes TLS accepting the self-signed cert.
- Postgres capability tests written against a removed write-capability model:
  deleted the obsolete *_with_capability tests (they asserted writes succeed,
  contradicting the strictly read-only spec) and updated *_without_capability
  assertions to the actual read-only rejection message.
- Postgres fixture setup (introspect/max_rows/REF-263) ran DDL/DML through the
  now read-only engine and silently failed. Seed fixtures via a raw writable
  client (setup_client helper), mirroring the MySQL raw-Conn pattern.

Verified locally against postgres:16 and mysql:8.0 containers: 20 passed,
0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
native-tls (pulled in only by the postgres feature) linked against the
system OpenSSL, so cargo build --release panicked in openssl-sys when
OPENSSL_LIB_DIR pointed at an invalid path (e.g. a Nix -bin output with
no lib/ dir). Enable native-tls's 'vendored' feature to compile OpenSSL
from source, removing the dependency on system OpenSSL and the fragile
OPENSSL_LIB_DIR/OPENSSL_DIR env vars. Produces portable binaries.

Verified: cargo build --release succeeds with a deliberately-broken
OPENSSL_LIB_DIR still set; cargo test --lib -> 225 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y; timeout-as-error (REF-258)

Four MySQL bugs surfaced by live testing against MySQL 8.0, all rooted in the
binary prepared-statement protocol and statement classification:

- Bug 1: transaction-control statements (BEGIN/START TRANSACTION) failed with
  error 1295 under the prepared protocol. execute_query now routes
  unparameterized statements through the text protocol (query/query_iter) and
  only uses the prepared protocol when bound params are present.
- Bug 2: EXPLAIN was classified as a non-row statement, silently dropping its
  plan rows. EXPLAIN is now treated as row-returning.
- Bug 3: EXPLAIN variants (FORMAT=..., ANALYZE, EXTENDED, parenthesized option
  lists, SQLite QUERY PLAN) were wrongly rejected as writes. strip_explain_prefix
  now strips all option/modifier forms before read-only validation, fixing this
  centrally for all three engines while still rejecting writes hidden behind an
  EXPLAIN decoration.
- Bug 4: SELECT SLEEP() swallows the server-side MAX_EXECUTION_TIME interrupt and
  returned partial data as success. The client-side tokio timeout is now the
  authoritative deadline (exactly timeout_ms -> QUERY_TIMEOUT); MAX_EXECUTION_TIME
  is set longer as a server-side cleanup backstop only.

Postgres/SQLite derive row-vs-affected dynamically from the prepared statement,
so they do not share Bugs 1/2/4; they shared Bug 3, fixed by the capability
change (added SQLite EXPLAIN QUERY PLAN and Postgres paren-option coverage).

Adds deterministic capability tests for all EXPLAIN variants (per engine, plus
hidden-write rejection) and ignored MySQL integration tests for transaction
control, EXPLAIN row capture, and timeout-as-error.

Verified live against MySQL 8.0: all four repros fixed; writes/DDL/CTE-DML and
EXPLAIN-hidden writes still rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, test-live.sh (REF-275)

Docker Compose provisions seeded MySQL 8.0/8.4 and PostgreSQL 16 with
healthchecks; vendor-specific init-script seeds (no shared SQL between
engines) cover the REF-274 dataset. New live_mysql/live_postgres suites
drive the compiled plenum binary end-to-end, are #[ignore]d for offline
runs, and fail fast when PLENUM_TEST_*_DSN is missing under
--include-ignored. scripts/test-live.sh wraps up --wait → test → down,
with --keep for iteration.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Testing Expectations now covers spin-up-as-needed via scripts/test-live.sh
for engine/JSON-contract changes, --keep iteration with explicit teardown,
the offline cargo test default, and the PLENUM_TEST_*_DSN env contract.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
35 tests covering the full SQLite coverage matrix per the REF-274 plan:

- connect: valid path; nonexistent file → CONNECTION_FAILED
- introspect: tables, columns + declared type affinity, single/composite
  PK, FK, composite FK, named UNIQUE index, view list, view details,
  ListIndexes per table, stable deterministic JSON shape
- query allowed: SELECT (type_matrix w/ emoji/BLOB/NULL, customers,
  view), EXPLAIN, EXPLAIN QUERY PLAN, PRAGMA table_info/index_list,
  transaction control (BEGIN, COMMIT/ROLLBACK/SAVEPOINT/RELEASE not
  rejected as CAPABILITY_VIOLATION)
- query denied: INSERT/UPDATE/DELETE/CREATE/DROP/ALTER all produce
  CAPABILITY_VIOLATION; re-query after each proves DB state unchanged
- safety: max_rows truncates 1500-row bulk_rows table, rows_truncated
  flag set; no truncation when max_rows > count; timeout_ms completes
  fast queries; sqlite3_interrupt fires on 1B-row CTE → QUERY_TIMEOUT
- envelope: QueryResult/IntrospectResult JSON shape, execution_ms
  excluded from determinism check, error code + message present

Fixture mirrors MySQL/Postgres logical dataset: type_matrix, customers,
orders, orders/order_items with composite PK+FK, bulk_rows (1500 rows),
v_order_totals view. Explicit CREATE UNIQUE INDEX used instead of inline
UNIQUE constraint so the index is visible in Plenum's introspect output
(auto-created sqlite_autoindex_* names are filtered by the engine).

Runs fully offline — no Docker. Plain cargo test includes all 35 tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Expand tests/live_mysql.rs from the harness smoke test to the full
approved REF-274 matrix, run against both MySQL 8.0 and 8.4 via a
mysql_matrix! macro (39 tests total):

- connect: valid creds surface server version/metadata; wrong password
  -> normalized CONNECTION_FAILED envelope with no credential leak;
  --password-env and --password-command sources
- introspect: table list, ENUM/SET/JSON/generated column types,
  composite PK/FK (incl. composite FK order_items -> orders), indexes,
  views (list + details), databases (wildcard "*" connection)
- query allowed: SELECT (exact rows), EXPLAIN, SHOW, DESCRIBE,
  transaction control
- query denied: INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/TRUNCATE ->
  CAPABILITY_VIOLATION with follow-up probes proving state unchanged
- safety: --max-rows truncation meta (rows_truncated/has_more/
  next_offset) on the 1,500-row table; SLEEP() + --timeout-ms ->
  structured QUERY_TIMEOUT naming the budget (locks in REF-258)
- envelope: top-level key sets match schemas/*.json; deterministic
  with execution_ms redacted; stdout single JSON doc, empty stderr
- cross-version: canonical invocations byte-identical across 8.0/8.4
  after redacting version strings, row estimates, and timing

Verified: scripts/test-live.sh green (39 mysql + 17 postgres); plain
cargo test green with all live tests ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ew definitions (REF-277)

Three engine bugs surfaced by the new PostgreSQL live matrix:

- NULL detection probed columns as Option<String>, which fails with a type
  error (not Ok(None)) on NULL non-text columns such as boolean. Replaced
  with a type-agnostic NullProbe FromSql impl that accepts every type.
- Foreign-key introspection joined constraint_column_usage without ordinal
  correlation, so composite FKs degenerated into the cross-product of their
  columns. The referenced side now comes from key_column_usage on the unique
  constraint, correlated via position_in_unique_constraint. Grouping now
  uses BTreeMap so multi-FK output order is deterministic.
- View details selected a nonexistent "definition" column; the
  information_schema.views column is view_definition.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ty, envelope (REF-277)

Implements the approved REF-274 plan section 4 for postgres16, on top of the
REF-275 harness. 17 tests, all #[ignore]d so plain cargo test stays offline:

- connect: server metadata contract, wrong password -> CONNECTION_FAILED
  without echoing the credential, password_env and password_command sources
  (including proof the plaintext never lands in the saved config, and a
  missing env var fails with CONFIG_ERROR)
- introspect: full type matrix (arrays, JSONB, enum), composite PK/FK with
  column-order fidelity, unique/non-unique indexes, view listing + details,
  multi-schema listing and --schema scoping
- query allowed: SELECT round-trip of seeded unicode/NULL/enum/array/jsonb
  values, EXPLAIN, EXPLAIN ANALYZE, transaction control statements
- query denied: INSERT/UPDATE/DELETE/TRUNCATE/CREATE/DROP/ALTER ->
  CAPABILITY_VIOLATION, then re-queried to prove DB state unchanged
- safety: max_rows truncation + pagination meta on the 1,500-row table,
  timeout_ms via pg_sleep -> QUERY_TIMEOUT
- envelope: required-field checks against schemas/*.json, identical output
  across runs with execution_ms redacted, JSON-only stdout

Verified: scripts/test-live.sh green (mysql 39, postgres 17), plain
cargo test green, cargo build --release clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace the broken inline-services stub with a job that delegates the
full DB lifecycle to scripts/test-live.sh:

- Removes the GitHub services: block (wrong credentials, no seeding,
  missing MySQL 8.4) in favour of docker compose driven by the script.
- scripts/test-live.sh handles compose up --wait (seeded, healthchecked),
  exports the PLENUM_TEST_*_DSN env vars, runs
  `cargo test --test live_mysql --test live_postgres -- --include-ignored`,
  and tears down via trap EXIT — containers never leak on test failure.
- Host ports set explicitly in env (43306 / 43307 / 45432) to avoid
  runner collisions and keep the job self-documenting.
- Adds cargo registry + build caches to the job.
- Offline test job is unchanged; the two jobs run independently.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant