Skip to content

Connection boundaries + cross-backend migration semantics#483

Merged
christianparpart merged 2 commits intomasterfrom
feature/lightweight-cross-backend-foundation
Apr 30, 2026
Merged

Connection boundaries + cross-backend migration semantics#483
christianparpart merged 2 commits intomasterfrom
feature/lightweight-cross-backend-foundation

Conversation

@christianparpart
Copy link
Copy Markdown
Member

@christianparpart christianparpart commented Apr 30, 2026

Summary

Two foundation-layer commits extracted from the larger feature/migrations-gui branch so they can land independently:

  • [Lightweight] Robust connection/statement boundaries — three small, independent fixes:

    • SqlConnection: surface the DBC-handle diagnostic from a failed value-form Connect() before subsequent ODBC calls clobber it (previously the optional ctor silently discarded the failure).
    • SqlStatement: tolerate SQL_NO_DATA from SQLExecDirect (per ODBC spec, a searched UPDATE/DELETE that affected zero rows; SQLite also returns it for INSERT … SELECT copying zero rows).
    • SqlConnectInfo::EnsureSqliteDatabaseFileExists(connStr) — bootstraps a missing file-based SQLite DB (creates parent dir + empty .sqlite3); no-op for in-memory and non-SQLite drivers.
  • [Lightweight] Cross-backend migration semantics for legacy SQL corpora — lets a linear sequence of legacy ALTER scripts apply against SQLite, PostgreSQL, and SQL Server without dialect carve-outs:

    • WhereExpression on Update/Delete (pre-rendered composite WHERE bodies: AND/OR/NOT, IS NULL, IN/EXISTS subqueries).
    • SetExpression on Update (column-to-column copies and arithmetic, e.g. SET CTR = CTR + 1).
    • Foreign-key constraint names are now double-quoted in BuildForeignKeyConstraint so PostgreSQL preserves case across CREATE/DROP; centralised BuildForeignKeyConstraintName on SqlQueryFormatter.
    • SqlMigration runtime: SQLite-rebuild paths for AddForeignKey / DropForeignKey, parsing of the LIGHTWEIGHT_SQLITE_GUARD: sentinel, and ExecuteScriptRespectingSqliteGuards wrapper that routes guarded SQL through the rebuild logic on SQLite.

These were originally b8789068 and b00ab692 on feature/migrations-gui; they are prerequisites for the GUI/tooling stack that follows.

Risk assessment

  • SqlStatement no longer raises on SQL_NO_DATA from SQLExecDirect — callers that relied on the exception to detect zero-row UPDATE/DELETE need to inspect the row count instead. Internal usage already does.
  • EnsureSqliteDatabaseFileExists is opt-in (callers must invoke it); zero impact on existing call sites.
  • Foreign-key name quoting changes the constraint identifier observed by PostgreSQL. Existing databases that were created before this PR will retain unquoted (folded-to-lowercase) names; new schemas will preserve case. No backfill needed for in-place upgrades that don't drop and recreate FKs.

Databases tested

Per the project policy this should be exercised against sqlite3, mssql2022, postgres. The original commits were validated on the parent branch; please re-run the matrix on this extracted branch before merge.

Test plan

  • LightweightTest --test-env=sqlite3
  • LightweightTest --test-env=mssql2022
  • LightweightTest --test-env=postgres
  • linux-clang-debug build (PEDANTIC + ASan/UBSan + clang-tidy)

@christianparpart christianparpart requested a review from a team as a code owner April 30, 2026 12:34
@christianparpart christianparpart changed the title [Lightweight] Connection boundaries + cross-backend migration semantics Connection boundaries + cross-backend migration semantics Apr 30, 2026
@christianparpart christianparpart force-pushed the feature/lightweight-cross-backend-foundation branch 3 times, most recently from eb3bb4a to 5945907 Compare April 30, 2026 13:39
Three small, independent fixes that the new GUI/tooling stack needs to
behave deterministically:

- SqlConnection: when the value-form constructor's `Connect()` fails,
  throw `SqlException` with the DBC-handle diagnostic read *before*
  any subsequent ODBC call clobbers it. Previously the optional ctor
  silently discarded the failure and the caller got an unconnected
  handle, with the real driver message lost to the next
  `SQLAllocHandle(STMT, …)`. Regression test added in CoreTests.

- SqlStatement: tolerate `SQL_NO_DATA` from `SQLExecDirect` — per the
  ODBC spec it just means a searched UPDATE/DELETE affected zero rows
  (and the SQLite driver returns it for an INSERT … SELECT that copied
  zero rows). That is not a failure; do not raise.

- SqlConnectInfo: add `EnsureSqliteDatabaseFileExists(connStr)` which
  parses a connection string, and when it targets a file-based SQLite
  database creates the parent directory + an empty `.sqlite3` file
  (a valid zero-table DB) if missing. In-memory and non-SQLite drivers
  are no-ops. Lets callers bootstrap a fresh SQLite deployment without
  asking the user to pre-create the file. Exported via Lightweight.cppm.

Signed-off-by: Christian Parpart <christian@parpart.family>
A bundle of complementary additions that, together, let a linear
sequence of legacy ALTER scripts apply against SQLite, PostgreSQL,
and SQL Server without dialect-specific carve-outs:

- WhereExpression on Update/Delete: pre-rendered composite WHERE body
  (AND/OR/NOT, IS NULL, IN/EXISTS subqueries) emitted verbatim so
  callers can express conditions that don't fit the simple
  `(col, op, value)` triple. Update and Delete share a single
  `FormatWhereClause` helper for the precedence rule (raw expression
  wins, structured triple is the fallback).

- SetExpression on Update: column-to-column copies and arithmetic
  (e.g. `SET A = B`, `SET CTR = CTR + 1`) — emitted verbatim after
  literal `setColumns`. Avoids the codegen having to treat a bareword
  RHS as a string literal.

- AddColumnIfNotExists / AddNotRequiredColumnIfNotExists,
  DropColumnIfExists, DropIndexIfExists: idempotent variants of the
  ALTER TABLE primitives. Each backend gets a native expansion (PG/
  MSSQL native `IF [NOT] EXISTS`, SQLite a sentinel comment that the
  migration executor presence-checks via `pragma_table_info`).

- AddForeignKey is now idempotent on every backend (PG wraps in
  `DO $$ … EXCEPTION WHEN duplicate_object …`, MSSQL guards with
  `IF NOT EXISTS (sys.foreign_keys WHERE name=…)`, SQLite rebuilds the
  table). Lets migrations re-add the same FK across release-line
  overlaps without aborting.

- Deterministic FK constraint naming: SQLite-formatter `CreateTable`
  emits `CONSTRAINT "FK_<table>_<col1>_<col2>…"` for inline composite
  FKs (single-column inline FKs already had the name); the constraint
  name is now double-quoted in `BuildForeignKeyConstraint` so PG
  preserves case and a later quoted `DROP CONSTRAINT "FK_…"` matches.
  Centralised `BuildForeignKeyConstraintName` helper on
  `SqlQueryFormatter` is the single source of truth — every emission
  and rebuild path routes through it (single-column callers wrap the
  column in a one-element array).

- SqlMigration runtime: SQLite-rebuild paths for AddForeignKey /
  DropForeignKey, parsing of the `LIGHTWEIGHT_SQLITE_GUARD:` sentinel,
  and `ExecuteScriptRespectingSqliteGuards` wrapper. The wrapper
  dispatches via a new `RequiresTableRebuildForForeignKeyChange()`
  capability hook on `SqlQueryFormatter` (true on the SQLite
  formatter, false everywhere else) so per-DBMS branching stays
  inside the formatter hierarchy. The SQLite rebuild reuses one
  `SqlStatement` across the sqlite_schema fetch, PRAGMA, and four
  DDL/DML steps to keep the round-trip count down.

Tests: QueryBuilderTests and RelationTests updated for the quoted
constraint names + new PG/MSSQL guard wrappers; MigrationTests adds
end-to-end coverage for the SQLite ALTER-TABLE-AddForeignKey rebuild
path (using the project's `UNSUPPORTED_DATABASE` macro to skip on
non-SQLite backends).

Signed-off-by: Christian Parpart <christian@parpart.family>
@christianparpart christianparpart force-pushed the feature/lightweight-cross-backend-foundation branch from 5945907 to abee6e4 Compare April 30, 2026 16:37
@christianparpart christianparpart merged commit 1f37c64 into master Apr 30, 2026
24 checks passed
@christianparpart christianparpart deleted the feature/lightweight-cross-backend-foundation branch April 30, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant