Connection boundaries + cross-backend migration semantics#483
Merged
christianparpart merged 2 commits intomasterfrom Apr 30, 2026
Merged
Connection boundaries + cross-backend migration semantics#483christianparpart merged 2 commits intomasterfrom
christianparpart merged 2 commits intomasterfrom
Conversation
eb3bb4a to
5945907
Compare
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>
5945907 to
abee6e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two foundation-layer commits extracted from the larger
feature/migrations-guibranch so they can land independently:[Lightweight] Robust connection/statement boundaries— three small, independent fixes:SqlConnection: surface the DBC-handle diagnostic from a failed value-formConnect()before subsequent ODBC calls clobber it (previously the optional ctor silently discarded the failure).SqlStatement: tolerateSQL_NO_DATAfromSQLExecDirect(per ODBC spec, a searched UPDATE/DELETE that affected zero rows; SQLite also returns it forINSERT … SELECTcopying 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:WhereExpressiononUpdate/Delete(pre-rendered composite WHERE bodies: AND/OR/NOT, IS NULL, IN/EXISTS subqueries).SetExpressiononUpdate(column-to-column copies and arithmetic, e.g.SET CTR = CTR + 1).BuildForeignKeyConstraintso PostgreSQL preserves case across CREATE/DROP; centralisedBuildForeignKeyConstraintNameonSqlQueryFormatter.SqlMigrationruntime: SQLite-rebuild paths forAddForeignKey/DropForeignKey, parsing of theLIGHTWEIGHT_SQLITE_GUARD:sentinel, andExecuteScriptRespectingSqliteGuardswrapper that routes guarded SQL through the rebuild logic on SQLite.These were originally
b8789068andb00ab692onfeature/migrations-gui; they are prerequisites for the GUI/tooling stack that follows.Risk assessment
SqlStatementno longer raises onSQL_NO_DATAfromSQLExecDirect— callers that relied on the exception to detect zero-row UPDATE/DELETE need to inspect the row count instead. Internal usage already does.EnsureSqliteDatabaseFileExistsis opt-in (callers must invoke it); zero impact on existing call sites.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=sqlite3LightweightTest --test-env=mssql2022LightweightTest --test-env=postgreslinux-clang-debugbuild (PEDANTIC + ASan/UBSan + clang-tidy)