Skip to content

Add dbtool GUI plus diff/exec/fold/migrate-to-release and broader migration tooling#474

Draft
christianparpart wants to merge 48 commits intomasterfrom
feature/migrations-gui
Draft

Add dbtool GUI plus diff/exec/fold/migrate-to-release and broader migration tooling#474
christianparpart wants to merge 48 commits intomasterfrom
feature/migrations-gui

Conversation

@christianparpart
Copy link
Copy Markdown
Member

@christianparpart christianparpart commented Apr 23, 2026

The original goal of this branch — and still its main visible deliverable — is a Qt 6 GUI on top of dbtool. While building it, the supporting infrastructure (profile / secrets handling, ODBC enumeration, cross-backend migration semantics) grew large enough to be useful on its own, and the CLI grew several new commands (exec, diff, fold, hard-reset, unicode-upgrade-tables, rewrite-checksums, migrate-to-release) that the GUI either drives directly or shares code with. Hence the size of the diff.

Headline change — dbtool GUI

src/tools/dbtool-gui/ (opt-in via LIGHTWEIGHT_BUILD_GUI=ON): Qt 6 / QML front-end for dbtool against any configured connection profile. Includes:

  • Migration plan view grouped by LIGHTWEIGHT_SQL_RELEASE markers, with per-migration SQL preview and one-click apply / revert / backup-restore actions (MigrationRunner, BackupRunner running on QThread workers).
  • Connection panel backed by Config::ProfileStore + Secrets::SecretResolver, with ODBC DSN auto-discovery and an optional qtkeychain-based credential backend.
  • Ad-hoc SQL query panel (SqlQueryRunner, SqlSyntaxHighlighter, SqlResultModel) so the GUI is not migration-only.
  • Light/dark ThemeController and a small reusable QML component set (Card, StatusPill, KineticListView, WheelScrollAmplifier, ...).

Reusable Lightweight infrastructure

  • Config::ProfileStore: YAML-backed store for named connection profiles, used by both dbtool and the GUI so credentials no longer live in plaintext on disk.
  • Secrets::SecretResolver with EnvBackend / FileBackend / StdinBackend: pluggable indirection for credentials referenced from profiles. FileBackend refuses files with mode wider than 0600.
  • Odbc::DataSourceEnumerator: wraps SQLDataSources and driver enumeration so callers can populate a DSN dropdown.
  • MigrationFold/: shared engine that the GUI and dbtool fold use to collapse a chain of migrations into a single C++ or SQL artifact (CppEmitter, SqlEmitter, Folder).
  • CodeGen/SplitFileWriter: shared codegen helper used by lup2dbtool and MigrationFold to emit one C++ file per migration with a shared CMake snippet.
  • tui/: vendored markdown-table / SGR / unicode-width helpers (also pulls in libunicode via FindOrFetchLibunicode.cmake) — used by the new dbtool diff renderer.
  • Structured MigrationException: carries operation / timestamp / title / step index / failed SQL / driver message, so both the CLI and GUI can show actionable error context without parsing what().
  • Bounded migration apply/preview: MigrationManager::ApplyPendingMigrationsUpTo / PreviewPendingMigrationsUpTo / FindReleaseByVersion — drive forward migration up to a named release symmetrically with the existing rollback path.
  • Cross-backend migration semantics for legacy SQL corpora: composite WhereExpression / SetExpression on Update / Delete, idempotent variants of AddColumn / DropColumn / AddForeignKey / DropIndex across SQLite / PostgreSQL / MSSQL, deterministic FK constraint names, a SQLite ALTER TABLE rebuild path for AddForeignKey / DropForeignKey, and per-migration compat policy with a lup-truncate renderer.
  • HardReset cross-engine table matching: preserved-tables list compares by unqualified name only, so an unqualified plan resolves correctly against engine-specific default schemas (dbo / public / none).
  • MSSQL UTF-8 string literals: NCHAR concatenation so non-BMP characters survive round-trip.
  • SqlSchema cross-engine introspection fixes: SQLForeignKeys row grouping now keys on FK_NAME (with a SQLite PRAGMA fallback), plus assorted reader gaps that previously caused identical migrations to look like drift across engines.
  • SqlSchemaDiff / SqlDataDiff: cross-engine logical equivalence — pairs tables / columns by name, projects to LogicalKind / LogicalType, and folds engine-specific incidental detail (Char vs NChar, TinyintSmallint, Real precision drift, unbounded text, VarBinary size loss). Per-side table descriptors so a Postgres-vs-MSSQL data diff issues the right SELECT against each side.
  • Robustness fixes: SqlConnection now surfaces the original driver diagnostic when Connect() fails; SqlStatement tolerates SQL_NO_DATA from SQLExecDirect; SqlConnectInfo::EnsureSqliteDatabaseFileExists bootstraps a missing file-based SQLite database.

dbtool — new commands and reworks

  • Migrated onto Config::ProfileStore + Secrets::SecretResolver (drops the direct yaml-cpp link and bespoke ~/.config/dbtool/dbtool.yml parsing); adds --profile <name> and surfaces the latest applied release in status. An explicit --connection-string without --profile no longer silently merges profile defaults (schema, pluginsDir, ...) — the typed connection string pins the backend.
  • dbtool exec <QUERY> — ad-hoc SQL execution against a profile.
  • dbtool diff <profileA> <profileB> — schema + data diff between two databases, rendered through the new DiffRenderer (markdown tables, schema labels, canonical type variants).
  • dbtool fold — collapse a migration chain into a single artifact via MigrationFold.
  • dbtool hard-reset — drop all tables in batched transactions (used by the GUI and CI).
  • dbtool unicode-upgrade-tables — bulk UTF-8 upgrade for legacy MSSQL schemas.
  • dbtool rewrite-checksums — rewrite stored migration checksums after an authorised content change, with plugin policy propagation.
  • dbtool migrate-to-release <VERSION> — forward-direction counterpart of rollback-to-release; resolves the version to its declared highestTimestamp and applies pending migrations up to that point.
  • dbtool status — aligned label column and reports the latest available release alongside the latest applied one.
  • --show-examples — long-form examples moved out of the default --help.

lup2dbtool and LupMigrationsPlugin

  • New LupSqlParser, WhereClauseParser, expanded SqlStatementParser and CodeGenerator to lift the legacy init_m_*.sql / upd_m_*.sql corpus into one C++ file per migration (lup_{version}.cpp), with --emit-cmake for the shared CMake snippet.
  • Per-file encoding detection (UTF-8 / UTF-16 / Windows-1252) with strict explicit-mode flags for reproducible CI runs.
  • --force-unicode is now the default (with --no-force-unicode opt-out).
  • LupMigrationsPlugin installs the lup-truncate compat policy by timestamp (cutoff extended to all LUP migrations), scrubs proprietary paths from placeholder comments, and now cold-start bootstraps lup2dbtool itself when invoked from a parent build that is still mid-configure (with CONFIGURE_DEPENDS glob refresh).

Documentation

  • docs/migrations-gui-plan.md, docs/migrations-gui-mockup.html — design + clickable mockup that drove the GUI work.
  • docs/lup-legacy-migration-plan.md — updated to reflect the as-shipped design.
  • docs/dbtool.md — covers the new subcommands and profile flow.

Risk

  • Surface area: large, but the GUI is opt-in (LIGHTWEIGHT_BUILD_GUI=ON) and the new dbtool subcommands are additive.
  • Behaviour change: dbtool no longer reads ~/.config/dbtool/dbtool.yml directly — existing users must migrate to a profile. --connection-string alone no longer pulls profile defaults; users who relied on that need to also pass --profile. The new schema-diff is intentionally lossier within a single engine (size-only drift on a column is no longer flagged); intra-engine size drift is still caught via the canonical type variant for sized types.
  • Public API: DiffTableData signature changed (single Table const& → per-side descriptors); only the in-tree dbtool caller and four tests are affected and have been updated.

Coverage

  • New unit tests: ConfigProfileStoreTests, SecretResolverTests, DataSourceEnumeratorTests, SqlSchemaDiffTests, SqlDataDiffTests, plus large additions to Lup2DbtoolTests, MigrationTests, and QueryBuilderTests.
  • MigrationTests now verifies HardReset / UnicodeUpgradeTables post-conditions through SqlSchema::ReadAllTables instead of sqlite_schema, so the cases run on every backend the suite is parameterised over.
  • Cross-engine end-to-end: 679-table LUP schema migrated against SQLite, PostgreSQL, and MSSQL all return "schemas match" through dbtool diff.

Test plan

  • Build with LIGHTWEIGHT_BUILD_GUI=OFF (default) and confirm CLI-only build is unchanged.
  • Build with LIGHTWEIGHT_BUILD_GUI=ON, launch dbtool-gui, exercise apply / revert / backup-restore against a SQLite profile.
  • Run dbtool diff between SQLite and PostgreSQL of the same migrated schema — expect zero differences.
  • Run dbtool exec, dbtool fold, dbtool hard-reset, dbtool unicode-upgrade-tables, dbtool rewrite-checksums, dbtool migrate-to-release against a throwaway profile.
  • Run the full unit test suite under each enabled backend (sqlite3, mssql2022, postgres).

@christianparpart christianparpart requested a review from a team as a code owner April 23, 2026 22:55
@github-actions github-actions Bot added documentation Improvements or additions to documentation CLI command line interface tools Query Builder tests Core API Query Formatter SQL dialect implementations CMake labels Apr 23, 2026
@christianparpart christianparpart marked this pull request as draft April 23, 2026 22:55
@christianparpart christianparpart force-pushed the feature/migrations-gui branch 5 times, most recently from 103a585 to 156d6a7 Compare April 26, 2026 11:54
@christianparpart christianparpart changed the title Add Qt 6 migrations GUI and supporting infrastructure Add dbtool GUI plus diff/exec/fold and broader migration tooling Apr 27, 2026
@christianparpart christianparpart force-pushed the feature/migrations-gui branch 3 times, most recently from 69c4852 to 2fcf48c Compare April 29, 2026 20:04
@github-actions github-actions Bot added the Data Binder SQL Data Binder support label Apr 29, 2026
@github-actions github-actions Bot removed the Data Binder SQL Data Binder support label Apr 30, 2026
@github-actions github-actions Bot added the Data Binder SQL Data Binder support label Apr 30, 2026
@christianparpart christianparpart force-pushed the feature/migrations-gui branch 2 times, most recently from 49cd796 to 301326c Compare April 30, 2026 12:24
@github-actions github-actions Bot removed the Data Binder SQL Data Binder support label Apr 30, 2026
@christianparpart christianparpart force-pushed the feature/migrations-gui branch 2 times, most recently from abcded1 to 8860114 Compare April 30, 2026 17:58
Registers a `CompatPolicy` on the singleton `MigrationManager` at
plugin load time: every LUP migration with timestamp strictly less
than the first 6.0.0 migration (`20'000'000'060'000`) is rendered
with `lup-truncate` active; everything at or above that cutoff gets
strict, standards-compliant behaviour.

The threshold is intentionally hard-coded here, not exposed as a
runtime knob — compat scope is a property of the legacy *code*, not
of the deployment, so operators should never need to configure it.

Signed-off-by: Christian Parpart <christian@parpart.family>
`CollectMigrations` now propagates each plugin's installed `CompatPolicy`
onto the central manager via `ComposeCompatPolicy`, so multiple plugins
can contribute their own policies in the same dbtool process.

Signed-off-by: Christian Parpart <christian@parpart.family>
Adds a `rewrite-checksums` admin command that prints the drift diff in
dry-run mode and requires explicit `--yes` to write. Used after a regen
of generated migrations changes byte shape but not logical effect (the
Unicode-default flip in lup2dbtool is the canonical case). Adds a
`--yes` / `-y` flag to confirm destructive actions.

Signed-off-by: Christian Parpart <christian@parpart.family>
- Section 1: Unicode-default generator is now the lup2dbtool default
  rather than a per-CMake flag.
- Section 2: compat policy moved to plugin-side ownership rather than
  per-profile YAML config — the LUP plugin installs a timestamp-keyed
  policy on its own `MigrationManager` singleton, and `dbtool`
  composes it onto the central manager via `CollectMigrations`. The
  operator never sees this knob.
- Section 3: documents the shipped `dbtool rewrite-checksums`
  recovery tool and the verification run against the staging MSSQL
  database (rewrote 150 drifted checksums; the migrate run proceeds
  past the 4_07_05 boundary that originally blocked it).
- Out-of-scope: defers logical-equivalence checksum hashes and
  documents that Unicode-regen FK shape mismatches need
  per-database remediation, not a generic compat flag.

Signed-off-by: Christian Parpart <christian@parpart.family>
Extracts the line-budget bin-packing and chunked file emission logic
into src/Lightweight/CodeGen/SplitFileWriter.{hpp,cpp}, plus an
EmitPluginCmake helper that drops a CMakeLists.txt and Plugin.cpp
suitable for a drop-in migration plugin.

lup2dbtool's GroupBlocksByLineBudget becomes a thin wrapper around
the shared helper, so the existing split-on-large-migration behaviour
stays exactly the same. The new module is also picked up by the fold
emitter in a follow-up commit.

Signed-off-by: Christian Parpart <christian@parpart.family>
The N'...' prefix tells SQL Server to *store* a literal as Unicode but
does not change how the bytes between the quotes are decoded — those
go through the connection's narrow code page (CP-1252 by default). A
UTF-8 byte pair like 0xC3 0xBC ('ü') was decoded as two separate
Latin-1 characters ü, both garbling the stored value and inflating
the perceived character count. Legitimate 100-codepoint German strings
overflowed NVARCHAR(100) with 'String or binary data would be
truncated' (error 2628), even after lup-truncate clipped them to the
column's declared width.

Override SqlServerQueryFormatter::StringLiteral to UTF-8-decode
client-side and emit each non-ASCII codepoint as an NCHAR(N) call
glued onto the surrounding ASCII runs with `+`:

    "für" -> N'f' + NCHAR(252) + N'r'

ASCII fast-paths through unchanged. BMP non-ASCII becomes one NCHAR().
Supplementary-plane codepoints (>U+FFFF) become a UTF-16 surrogate
pair (two NCHAR() calls). Per-codepoint encoding lives in three small
private statics so escaping rules stay in one place.

Tests cover ASCII runs, empty literals, single-quote escaping, BMP
non-ASCII, pure-non-ASCII inputs, the single-char overload, and a
sanity check that SQLite/PostgreSQL still emit plain '...' literals.
The existing UTF-8-multibyte lup-truncate test asserts on the new
NCHAR-concatenated emission. Migration Insert/Update expectations
updated to use N'...' literals on MSSQL.

Verified end-to-end on lastrada-mssql: PROBEN_PRUEFUNGEN row 1008
(106-codepoint German content clipped to 100) now stores correctly
and round-trips German umlauts intact.

Signed-off-by: Christian Parpart <christian@parpart.family>
Adds a pure plan-walk primitive,
MigrationManager::FoldRegisteredMigrations(formatter, upToInclusive),
that folds the effect of every registered migration into a per-table
view of the final shape plus a chronological list of data steps,
indexes, and releases. Never executes SQL or opens a database
connection.

Used by:
  - dbtool fold (extracted to feature/dbtool-fold) — emits a
    self-contained baseline (.cpp plugin or .sql script).
  - hard-reset (added in a follow-up commit) — to know which tables
    the migrations would have created.
  - unicode-upgrade-tables (added in a follow-up commit) — to know
    which char/varchar columns the migrations now declare wide.

Also adds a `RunAdminCommand<Result>` template in dbtool that factors
the shared dry-run / --yes / diff UX, and rewrites `rewrite-checksums`
on top of it for net-negative lines. The two upcoming admin commands
will reuse this template.

Tests: fold unit tests cover create/altercolumn/drop-table cleanup,
data-step chronological order, --up-to truncation, RawSql passthrough,
column rename FK propagation, release-range filtering.

Signed-off-by: Christian Parpart <christian@parpart.family>
Adds an admin command that drops every migration-owned table
(preserves user-created tables) plus schema_migrations, in reverse
creation order with cascade=true ifExists=true. Pair with `migrate`
for a clean re-deploy.

The implementation walks `MigrationManager::FoldRegisteredMigrations`
to compute the migration-owned set, intersects with the live schema
via `SqlSchema::ReadAllTables`, then drops the matching live tables
inside a single transaction. Tables present in the live DB but not in
the migration plan are reported under `preservedTables` so operators
spot them.

Wired into dbtool through the shared `RunAdminCommand<Result>`
template introduced earlier — dry-run prints the diff, `--yes`
confirms the destructive action.

Tests: SQLite end-to-end coverage for dropping migrated tables +
schema_migrations, preserving user tables, and dry-run being
observationally pure.

Signed-off-by: Christian Parpart <christian@parpart.family>
Adds an admin command that rewrites legacy VARCHAR/CHAR columns to
NVARCHAR/NCHAR where the registered migrations now declare wide
types. Drops + re-adds touched FKs, with a SQLite-specific path via
`RebuildSqliteTable` for in-place column-type rewrite.

Compares the folded plan's intended column types against
`SqlSchema::ReadAllTables` output; an upgrade is triggered iff
intended is `NVarchar`/`NChar` AND live is `Varchar`/`Char` with the
same `size`. Foreign keys touching any upgrade column are dropped
before the alter and re-added afterwards. Cross-backend.

Wired into dbtool through the shared `RunAdminCommand<Result>`
template — dry-run prints the diff, `--yes` confirms the destructive
action.

Tests: SQLite coverage for dry-run drift reporting plus an idempotent
roundtrip (running unicode-upgrade-tables twice in a row produces no
second-run drift).

Signed-off-by: Christian Parpart <christian@parpart.family>
The original cutoff at the first 6.0.0 timestamp assumed post-6.0.0
LUP SQL files would be cleaned up before generation. They aren't:
upd_m_6_01_12.sql writes a 61-char value into TEXTBAUSTEINGRUPPEN.NAME
which is NCHAR(60), and similar over-long inserts appear elsewhere in
the 6.x.x range. LUpd silently client-side-truncated those historically;
without lup-truncate active for these migrations, MSSQL strict NCHAR
sizing rejects them with error 2628.

Raise the cutoff to 20'000'070'000'000 so every LUP-sourced migration
(timestamp range < 7.00.00) gets the legacy truncation policy. The
9999_99_99 sentinel and any future native modern migrations remain
strict. Comment block updated to record the rationale and the concrete
case.

Signed-off-by: Christian Parpart <christian@parpart.family>
Thin diagnostics helper for inspecting INFORMATION_SCHEMA, sanity-
checking row counts, or running one-off queries from CI / shell
scripts. Streams the result set as tab-separated values to stdout;
row count goes to stderr.

The query is read from the command-line argument or, when no argument
is supplied (or `-` is passed), from stdin until EOF. Multi-statement
scripts pass through to ExecuteDirect — the ODBC driver advances
through subsequent result sets automatically.

  dbtool --profile X exec "SELECT * FROM INFORMATION_SCHEMA.COLUMNS"
  echo "SELECT COUNT(*) FROM users" | dbtool --profile X exec

Used during the MSSQL UTF-8 literal investigation to confirm that
MSSQL was decoding raw `N'für'` as four CP-1252 codepoints rather
than three Unicode codepoints, which led to the NCHAR-concatenation
fix shipped in the previous commit.

Signed-off-by: Christian Parpart <christian@parpart.family>
The Examples block bloated the bottom of `dbtool --help`, pushing the
options listing off the screen. Hoist it behind a dedicated
`--show-examples` flag and leave a one-line pointer at the end of help
output so discoverability is preserved without forcing the whole
example wall on every `--help` reader.

Signed-off-by: Christian Parpart <christian@parpart.family>
Ship a Qt 6 / QML desktop GUI for dbtool's migration workflow alongside
the CLI. Users get a connection-aware view of pending vs applied
migrations, a release-grouped timeline, one-click apply / rollback /
backup, and an SQL preview before any destructive action.

Architecture:
- AppController is the top-level QML-exposed model: owns the
  SqlConnection, drives MigrationRunner / BackupRunner workers off the
  GUI thread, and exposes Q_PROPERTY surfaces consumed by the QML side.
  Persists profile selection, view-mode (Simple vs Expert) and window
  geometry via QSettings.
- MigrationRunner / BackupRunner are QObject workers running on
  dedicated QThread instances. Both forward structured progress and
  log lines via Qt signals so the UI stays responsive during long
  operations. MigrationRunner consumes the structured
  MigrationException surface (timestamp, step index, driver message,
  failed SQL) for actionable failure reporting.
- QmlProgressManager bridges Lightweight's IProgressManager interface
  to QML, mirroring the StandardProgressManager used by the CLI.
- Models (MigrationListModel, ReleaseListModel, ProfileListModel,
  OdbcDataSourceListModel) expose typed list interfaces for the QML
  views; each is backed by domain data from Lightweight + ProfileStore
  + DataSourceEnumerator.
- ThemeController owns palette + accent state and persists theme
  preference via QSettings. The Theme.qml singleton consumes it.

Views:
- Simple view: single centred column (connection -> status -> run ->
  progress -> success/failure) targeting downstream operators who just
  want "bring my DB up to date". Status card surfaces current/target
  release labels in large type, with a green up-to-date pill when no
  work is pending. Run card offers one primary button plus a
  "Back up first" checkbox that chains backup -> apply.
- Expert view: full three-pane timeline with per-migration controls,
  bulk operations, release groupings, log panel, SQL preview, and
  backup/restore dialog.
- ToolBar exposes a single Simple/Expert toggle button labelled with
  the destination view ("Switch to Expert view" / "Switch to Simple
  view"). FontMetrics reserves width for the longer label so the
  toolbar layout never reflows on toggle.
- Per-view window sizing: each view stores its own preferred window
  geometry so toggling does not yank the user into a layout that
  doesn't fit their content.

Build:
- cmake/FindQt.cmake locates a Qt 6 install, top-level CMakeLists
  optionally descends into the GUI when Qt is found, and
  src/tools/CMakeLists.txt only registers the migrations-gui subdir
  when the optional dependency is satisfied. The CLI build is
  unaffected when Qt is absent.

Docs:
- docs/migrations-gui-plan.md captures the design rationale.
- docs/migrations-gui-mockup.html is the static mockup the QML layout
  was derived from.

Signed-off-by: Christian Parpart <christian@parpart.family>
A single transaction over hundreds of tables exhausts Postgres's
per-transaction lock pool: each `DROP TABLE ... CASCADE` takes an
AccessExclusiveLock (plus more from CASCADE-triggered drops), capped by
`max_locks_per_transaction * (max_connections + max_prepared_transactions)`.
Resetting a 678-table schema surfaced as `ERROR: out of shared memory`
(SQLSTATE 53200).

Drop in chunks of 32 via `std::views::chunk`, committing each batch so
locks are released between batches. `schema_migrations` gets its own
small transaction. Drops are idempotent (`IF EXISTS`) and ordered in
reverse creation order, so a partial-failure re-run still completes.

Signed-off-by: Christian Parpart <christian@parpart.family>
Switches the `status` summary to a uniform 26-char label column so
"Registered/Applied/Pending migrations:", "Unknown applied:" and the
"Latest applied/available release:" lines all line up the same way.
Also surfaces the highest declared release alongside the latest applied
one, making "are we caught up?" answerable at a glance.

Signed-off-by: Christian Parpart <christian@parpart.family>
…w status wording

`dbtool status` now prints "Latest applied release:" instead of the old
"Latest release:". Bring the WriteReleaseMarker doxygen example in line
so future readers do not chase a label that no longer exists.

Signed-off-by: Christian Parpart <christian@parpart.family>
Replaces the single global --input-encoding switch (which silently
double-encoded whichever group of files disagreed with the flag) with
per-file detection. New default mode is `auto`: each file is classified
by validating its SQL payload (with `--` and `/* … */` comments stripped
from the detection signal so author notes in legacy banners do not skew
the result). Files that validate as UTF-8 are passed through unchanged;
the rest are converted from Windows-1252.

The explicit `utf-8` and `windows-1252` modes now act as assertions: a
mismatch is reported as a per-file error (with the offending byte and
its offset) and propagates to a non-zero exit code, so a stale flag in
the build cannot quietly mangle a UTF-8 file or vice versa. The plugin
CMake default is flipped to `auto` to match.

ParseSqlFile now returns std::expected<ParsedMigration, std::string>;
the lup2dbtool driver collects per-file errors, emits all of them, and
still exits non-zero if any file failed even when the surviving inputs
generated cleanly.

Tests cover auto-mode pass-through of UTF-8, auto-mode conversion of
Windows-1252, the strict-mode rejections in both directions, and the
"non-ASCII only inside comments" case that motivated the
comment-stripped detection signal.

Signed-off-by: Christian Parpart <christian@parpart.family>
On a fresh checkout (or after a clean of the generated directory) the
configure-time `file(GLOB lup_*.cpp)` produced no inputs, the plugin
was linked with an empty placeholder, and dbtool reported "nothing to
migrate" against an empty database — even though hundreds of SQL
migrations were sitting in LUPMIGRATION_SQL_DIR. Reconfiguring after
the first build was the documented escape hatch but a real footgun.

Configure-time bootstrap now drives lup2dbtool synchronously when the
generation stamp is missing: the tool is built, located in the build
tree, and invoked with the same arguments as the build-time custom
command, so `lup_*.cpp` files exist before the glob runs. A sentinel
file guards against the re-entrant `cmake --regenerate-during-build`
pass that ninja issues before each build (without it the inner
configure would recurse into `cmake --build`, deadlocking the tree).

The placeholder source and the post-bootstrap reconfigure step are
gone. The glob now uses CONFIGURE_DEPENDS, so adding a new SQL release
(or splitter changes that produce extra `_partNN` files) lights up the
new sources on the next build without a manual re-run of cmake.

Signed-off-by: Christian Parpart <christian@parpart.family>
…uild is mid-configure

The previous cold-start bootstrap called `cmake --build ${CMAKE_BINARY_DIR}
--target lup2dbtool` from inside the parent's first configure, but `build.ninja`
is only emitted after configure finishes — so on a true fresh build directory
ninja aborted with "loading 'build.ninja': The system cannot find the file
specified" and the configure failed before any migrations could be generated.

Detect whether the parent has a usable build script (`build.ninja` or
`Makefile`) and split the path:

  * Warm cold-start (parent build present, only `generated/` wiped) keeps the
    original `cmake --build ${CMAKE_BINARY_DIR}` invocation. The binary search
    now also covers `CMAKE_RUNTIME_OUTPUT_DIRECTORY` and per-config subdirs so
    Windows (where DLLs/EXEs land in `target/`) and multi-config generators
    resolve the freshly-built binary.

  * True cold-start spins up a self-contained scratch sub-build at
    `${CMAKE_BINARY_DIR}/_lup_bootstrap`. The scratch configure inherits the
    toolchain file, compilers, generator, build type, vcpkg triplet, and make
    program from the parent, but disables every optional component (tests,
    examples, GUI, benchmark, large-db tool, docs, clang-tidy, pedantic-werror)
    and clears LUPMIGRATION_SQL_DIR to prevent the bootstrap from recursing
    into itself. The scratch dir persists so subsequent cold-starts (after
    `rm -rf generated/`) are incremental.

Verified end-to-end against `D:/Lastrada/src/model4_JP` (408 SQL files): fresh
configure on an empty build dir generates all 408 migrations and produces
`build.ninja`, a follow-up wipe of `generated/` reuses the warm path, and
`cmake --build --target LupMigrationsPlugin` links the resulting DLL with
every generated TU compiled in.

Signed-off-by: Christian Parpart <christian@parpart.family>
…stamp

Adds `ApplyPendingMigrationsUpTo(target, cb)` and
`PreviewPendingMigrationsUpTo(target, cb)` to MigrationManager. Both honor
the same dependency-respecting topological order as their unbounded
counterparts and thread a single render context across the run so column-
width state from earlier CREATE TABLEs is visible to later compat-aware
INSERT/UPDATE rendering.

A new anonymous `FilterPendingUpTo` helper filters the topo-sorted pending
list to migrations with `ts <= targetInclusive` and refuses partial states
whose included migrations declare a dependency on an excluded (`ts >
target`) pending migration — applying such a set would silently violate
the dependency contract.

Covered by five new unit tests: boundary apply, off-boundary target,
no-op when already at target, preview SQL identity, and dependency-
cross-boundary refusal.

Signed-off-by: Christian Parpart <christian@parpart.family>
…elease

Adds `dbtool migrate-to-release <VERSION>`, the forward-direction
counterpart of the existing `rollback-to-release`. Resolves the version
to its declared `highestTimestamp` via `FindReleaseByVersion` and
delegates to `MigrationManager::ApplyPendingMigrationsUpTo`.

Forward-only: when the database is already at or past the target, the
command is a no-op and prints a hint pointing at `rollback-to-release`
rather than silently reverting. Honors `--dry-run` (`-n`) and `--no-lock`
like the other migration commands.

Wires the command into `DispatchDbCommand`, adds it to `PrintUsage` and
two examples in `PrintExamples`, documents it in `docs/dbtool.md`, and
extends `test_dbtool.py` with four integration scenarios (dry-run,
unknown release, already-at-target no-op, happy path).

Signed-off-by: Christian Parpart <christian@parpart.family>
…e across engines

Compare live tables to the migration plan by name only. The engine
resolves an unqualified plan (`schemaName=""`) into its default schema
(`dbo` on MSSQL, `public` on Postgres, none on SQLite), so the live
row's schema is engine-specific while the migration plan keeps its
declared schema. Matching the dropped-tables half of this same
function, which already keys off the unqualified table name.

Signed-off-by: Christian Parpart <christian@parpart.family>
An explicit `--connection-string` without `--profile` is the user's
way of saying "don't auto-apply any profile defaults" — the connection
string they typed pins them to a specific backend, and silently
merging another profile's `schema` / `pluginsDir` / etc. is at best
surprising and at worst wrong (e.g. picking up `schema: dbo` from a
SQL Server profile while pointing at SQLite, which then injects
`"dbo"."<table>"` into every introspection query).

Signed-off-by: Christian Parpart <christian@parpart.family>
…lTables

Replace post-migration verification queries against `sqlite_schema`
with `SqlSchema::ReadAllTables`, so HardReset and UnicodeUpgradeTables
test cases run on every backend the suite is parameterized over and
not just SQLite.

Signed-off-by: Christian Parpart <christian@parpart.family>
Apply clang-format-22 across all modified C++ files. No behavioral
changes — purely whitespace/wrapping normalisation against the project
.clang-format style.

Signed-off-by: Christian Parpart <christian@parpart.family>
- Add Doxygen comments for new public members (HardResetResult,
  UnicodeUpgradeResult, ColumnUpgradeEntry, PlanFoldingResult inner
  structs, ColumnDiff, TableDiff, RowDiff, TableDataDiff,
  DiffProgressEvent, DataSourceInfo, DriverInfo, SecretResolver move
  ops, MigrationRenderContext::ColumnKey/TableKey, SplitFileWriter
  CodeBlock).
- Replace `\ref` with backticks in SqlSchemaDiff/SqlDataDiff comments
  so doxygen stops complaining about unresolved references.
- Reword `@overload` directive in MigrationPlan.hpp so doxygen does
  not interpret the trailing words as a symbol name.
- Move `@copydoc` recursion in SqlStatement.hpp by giving the primary
  declarations their own brief docstrings.
- Exclude `src/Lightweight/tui/` from doxygen — vendored code that
  follows the upstream comment style.
- Migrate `.find(x) != std::string::npos` to `.contains(x)` (and the
  inverse) across all PR-introduced code, satisfying clang-tidy 22's
  `readability-container-contains` check.
- Convert positional aggregate initializers to designated form for
  `StatementWithComments` and `MigrationRenderContext::ColumnKey`,
  satisfying `modernize-use-designated-initializers`.

No behavioral changes — all transformations are mechanical.

Signed-off-by: Christian Parpart <christian@parpart.family>
@christianparpart christianparpart force-pushed the feature/migrations-gui branch from 742a13a to da47294 Compare May 6, 2026 16:07
@christianparpart christianparpart changed the title Add dbtool GUI plus diff/exec/fold and broader migration tooling Add dbtool GUI plus diff/exec/fold/migrate-to-release and broader migration tooling May 6, 2026
The diff feature (`SqlSchemaDiff`, `SqlDataDiff`, the vendored `tui/`
markdown renderer + libunicode dependency, the `dbtool diff` subcommand
plus its `DiffRenderer`, and the matching test files) is owned by
the in-flight #484 and was originally pulled into this branch as a
prerequisite for the GUI work. The GUI does not need it; carrying it
here only causes merge churn against #484 and bloats the surface area
under review.

Removed:
- `src/Lightweight/SqlSchemaDiff.{hpp,cpp}`, `SqlDataDiff.{hpp,cpp}`
- `src/Lightweight/tui/*` and `cmake/FindOrFetchLibunicode.cmake`
- `src/tools/dbtool/DiffRenderer.{hpp,cpp}` and the `diff` subcommand
  in `dbtool/main.cpp` (help text, `--no-color`, `--max-rows`,
  `secondArgument`, `DispatchDbCommand` gate, `DiffSharedTables`,
  `ResolveDiffSource`, `DiffDatabases`, the `MakeProgressEvent`
  helper used only by diff)
- `src/tests/SqlSchemaDiffTests.cpp`, `src/tests/SqlDataDiffTests.cpp`
- The `## Diff` section in `docs/dbtool.md`

Kept (the GUI / dbtool consume them via Lightweight directly):
- `src/Lightweight/Config/ProfileStore.{hpp,cpp}` + tests
- `src/Lightweight/Secrets/*` + tests
- The cross-engine SqlSchema introspection fixes (`1ffface5`) — they
  are real driver-level bug fixes that benefit any consumer doing
  schema reflection, not just the diff feature.

The diff feature will land via PR #484.

Signed-off-by: Christian Parpart <christian@parpart.family>
Three independent regressions were reported by the previous CI run on
this branch (see PR #474). Each is fixed at its root cause:

1. **`std::views::chunk` not in libc++ on the C++26 reflection job.**
   `SqlMigration.cpp:HardReset` used the C++23 `std::views::chunk`
   adapter to batch DROP TABLEs. The reflection-job toolchain ships
   an older libc++ that does not yet provide `__cpp_lib_ranges_chunk`
   (≥ 202202L) and the build aborted with "no member named 'chunk'
   in namespace 'std::ranges::views'". Gate the modern path behind
   the standard feature-test macro and fall back to a `std::span`
   subspan-based loop on stdlibs that lack it. The Linux-clang-debug
   job's newer libc++ keeps the modern path.

2. **SQLite FK-rebuild path silently disabled.**
   `SqlMigration::ExecuteScriptRespectingSqliteGuards` consults
   `SqlQueryFormatter::RequiresTableRebuildForForeignKeyChange()` to
   decide whether a sentinel-prefixed script triggers the table
   rebuild or is run as-is. The SQLite override of that hook was
   missing from `SQLiteQueryFormatter` on this branch, so the base
   class's `false` was returned and the sentinel script (which is
   intentionally a *commented-out* ALTER + sentinel) was executed
   verbatim. No FK was added but no error was raised either, which
   is exactly the failure the suite reported:
       MigrationTests.cpp:1064: CHECK( found ) — false
       MigrationTests.cpp:1145/1146: foundPa/foundPb — false
   Restore the override (returns `true`) and the rebuild path takes
   over again.

   While here, restore the canonical FK constraint name builder in
   `SQLiteQueryFormatter::BuildForeignKeyConstraint` — it had drifted
   to a hand-rolled `FK_{table}_{column}` while the runtime rebuild
   side (`SqliteRebuildAddForeignKey`) still calls
   `BuildForeignKeyConstraintName`. Now CREATE-table and ALTER-table
   produce identical names.

3. **Windows-cl `/WX` errors C4251 / C4275 on new DLL-exported
   classes.** `MigrationException` (this branch) and the new
   `Config::ProfileStore`, `Secrets::SecretResolver` and the three
   `Secrets::backends/*` classes carry `LIGHTWEIGHT_API` because
   their non-inline methods cross the DLL boundary. They expose
   STL-typed members (`std::string`, `std::vector`,
   `std::filesystem::path`) or derive from a non-DLL-interface base
   (`ISecretBackend`), which triggers the standard
   "STL-across-the-DLL-boundary" warnings under `/W4 /WX`. Suppress
   both warnings on the `Lightweight` target when built shared on
   MSVC. Producer and consumer always share the same runtime in our
   matrix, so neither warning indicates an ABI bug.

Verified locally against the full test suite on:
- sqlite3:   590 cases, 589 passed, 1 skipped, 5505 assertions
- mssql2022: 590 cases, 589 passed, 1 skipped, 5493 assertions
- postgres:  590 cases, 589 passed, 1 skipped, 5495 assertions

Risk: low. The chunk fallback is observationally identical to the
view-based path. The FK rebuild override restores prior behaviour.
The MSVC pragma is the standard idiom and only narrows two specific
warnings on a single target.

Signed-off-by: Christian Parpart <christian@parpart.family>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI pipeline CLI command line interface tools CMake Core API documentation Improvements or additions to documentation Query Builder Query Formatter SQL dialect implementations tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant