Skip to content

tests: cover every SQL column type's ddl2cpp mapping on all three DBMS - #579

Merged
Yaraslaut merged 3 commits into
masterfrom
chore/191-ddl2cpp-column-type-coverage
Aug 21, 2026
Merged

tests: cover every SQL column type's ddl2cpp mapping on all three DBMS#579
Yaraslaut merged 3 commits into
masterfrom
chore/191-ddl2cpp-column-type-coverage

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Closes #191

What & why

The ddl2cpp column-type mapping was covered two ways, both partial:

  • src/tests/CxxModelPrinterTests.cpp feeds CxxModelPrinter::MakeType a hand-written SqlSchema::Column, so it cannot notice a column type that fails to survive the DDL or the catalog round trip.
  • The one end-to-end check in CI (ddl2cpp job) runs ddl2cpp over the Chinook schema, which exercises only the handful of column types Chinook happens to use.

Issue #191 proposed a DDL script for the missing types, and then noted the problem with it: a hand-written script is pinned to one dialect, so it could only ever run against SQLite.

This PR takes the route the issue suggests instead. src/tests/Ddl2CppColumnTypeTests.cpp never speaks SQL: every probe table is created through the dialect-agnostic migration query builder, read back out of the live catalog via SqlSchema::ReadAllTables, and only then handed to CxxModelPrinter::MakeType — the very function ddl2cpp emits record members with. The whole file runs unchanged against SQLite, MS SQL Server and PostgreSQL.

Note on the issue's other question ("would it make sense to move the actual logic of ddl2cpp into the core library?"): that has already happened — CxxModelPrinter lives in src/Lightweight/Tools/, and src/tools/ddl2cpp.cpp is only a CLI front end. So no library restructuring was needed here.

Coverage added

Column types: tinyint, smallint, integer, bigint, decimal(10,2), real, double, boolean, date, time, datetime, timestamp, char(8), varchar(30), text, binary(16), varbinary(16), nchar(8), nvarchar(30), guid.

Column properties: nullable vs. not-null (std::optional wrapping), auto-increment primary key, GUID primary key, VARCHAR primary key.

7 test cases / 88 assertions per database.

Per-DBMS divergences are recorded, not hidden

Where a backend stores something other than what was asked for, the deviation is a DialectException carrying its reason, so the divergences are written down in one place and continuously verified. The reason string is printed as Catch2 INFO when an assertion fails, so a future breakage explains itself.

Legitimate divergences captured: PostgreSQL has no 1-byte integer (Tinyintint16_t); the PostgreSQL Unicode driver reports every character column as its wide ODBC type; SQLite has a single dynamically typed TEXT class, so CHAR(n) padding and the Unicode distinction are both lost; BYTEA carries no declared length; the SQL Server formatter emits VARBINARY(n) for Binary{n}; the schema reader deliberately widens float/real to Real{53}.

⚠️ Two genuine defects this coverage surfaced

Both are asserted as-is and labelled KNOWN DEFECT in the test, so the behaviour is recorded rather than going unnoticed. I did not fix either — each is a separate change with its own risk, and the second involves choosing a replacement type. Happy to split off follow-up issues/PRs if you want them handled.

  1. PostgreSQL double precision generates float. The schema reader's float fixup (src/Lightweight/SqlSchema.cpp:1439) matches only the dialect type names float/real. PostgreSQL names its types float4/float8, so float8 misses the fixup and the driver-reported width narrows the column to 4 bytes. PostgreSqlFormatter::ColumnType already carries a comment about exactly this hazard on the write side ("or restore silently narrows it to float32"); the read side still has it. Generated records therefore lose precision on every double precision column.

  2. MS SQL Server Timestamp{} generates Light::SqlDynamicBinary<8>. SqlServerFormatter::ColumnType emits TIMESTAMP verbatim, but on SQL Server TIMESTAMP is a deprecated synonym for rowversion — an 8-byte, server-generated, non-writable binary counter, not a point in time. So a migration asking for a timestamp gets a read-only binary column, and ddl2cpp then generates a binary member for it. DATETIME2 is the natural replacement.

Testing

Databases Result
clang-debug (PEDANTIC + ASan + UBSan) sqlite3, mssql2022 (Docker, 16.00.4265), postgres (Docker 16.4) full suite green — 1409 cases / ~13.7k assertions each
clang-tidy clean on the new file (no NOLINT used); two findings fixed at the source (cppcoreguidelines-pro-type-member-init, readability-qualified-auto)
clang-format applied

Compilers: Clang only, locally. The gcc-release preset is Linux-gated and CMake refuses it on this macOS host (Cannot use disabled configure preset), so per AGENT.md I am flagging that explicitly rather than claiming coverage I do not have — CI's ubuntu_build_cc_matrix / dbms_test_matrix legs are the GCC check for this branch. The change is a single self-contained test translation unit with no namespace-scope entities in public headers, so the modules and reflection configurations are unaffected.

  • Performance impact: none on library code. Adds one test TU and ~5 short-lived probe tables per database run.
  • Risk: low. Test-only; no production source touched. The one externally tunable expectation (PostgreSQL TEXT, whose reported length is the driver's MaxLongVarcharSize rather than a property of the column) is asserted by shape via StartsWith, not by exact number, so it will not break on a different psqlODBC build.

The ddl2cpp column-type mapping was only covered two ways, both partial:
`CxxModelPrinterTests.cpp` feeds `CxxModelPrinter::MakeType` a hand-written
`SqlSchema::Column`, so it cannot notice a type that fails to survive the DDL
or the catalog round trip; and the one end-to-end check in CI runs ddl2cpp over
the Chinook schema, which exercises only the handful of types Chinook uses.

Issue #191 proposed a DDL script for the missing types but noted it would be
pinned to one dialect. Take the route the issue suggests instead: drive the
round trip from C++ through the dialect-agnostic migration query builder, read
each column back out of the *live* catalog via `SqlSchema::ReadAllTables`, and
assert the type `MakeType` generates. The whole file is therefore
dialect-agnostic and runs unchanged against SQLite, MS SQL Server and
PostgreSQL.

Covered: tinyint, smallint, integer, bigint, decimal, real, double, boolean,
date, time, datetime, timestamp, char, varchar, text, binary, varbinary,
nchar, nvarchar, guid; nullable vs. not-null; auto-increment, GUID and VARCHAR
primary keys.

Where a backend stores something other than what was asked for, the deviation
is recorded as a `DialectException` carrying the reason, so the per-DBMS
divergences are written down in one place and continuously verified. Two of
them are marked KNOWN DEFECT rather than dressed up as correct behaviour:

  - PostgreSQL `double precision` generates `float`. The schema reader's
    float fixup matches only the type names `float`/`real`, so PostgreSQL's
    `float8` misses it and an 8-byte column silently narrows to float32.
  - MS SQL Server `Timestamp{}` generates `Light::SqlDynamicBinary<8>`,
    because `TIMESTAMP` on SQL Server is a synonym for `rowversion` — a
    server-generated binary counter, not a point in time.

Closes #191

Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
@Yaraslaut
Yaraslaut requested a review from a team as a code owner August 19, 2026 08:28
@github-actions github-actions Bot added the tests label Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Yaraslaut and others added 2 commits August 19, 2026 18:33
Applying /code-review findings on this branch.

INFO() was used as the unbraced substatement of an if, so the Catch2
ScopedMessage it creates was destroyed at the end of that if -- before the
assertion it was meant to annotate ran. Every dialect-exception reason was
therefore dropped from the failure report, in both the primary-key check
and the main probe-table loop.

Extract a FailureContext(testCase, serverType) helper that builds the
message unconditionally and hoist a single scoped INFO above the
assertions. Confirmed against a deliberately broken expectation: the
failure now prints the dialect reason the previous form suppressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows SQLite3 leg failed 7 assertions across 4 cases: every character
column generated `Light::SqlDynamicUtf16String<N>` where the table expected the
narrow `Light::SqlAnsiString<N>` (and `SqlDynamicAnsiString<0>` for GUID and
unbounded TEXT).

The expectation was not wrong about SQLite, it was wrong about the driver.
SQLite has one TEXT storage class, and which C++ type ddl2cpp emits for it is
decided by the ODBC driver build: the unixODBC drivers on the Linux and macOS
legs report the narrow ODBC types, while the Windows build is the Unicode one
and reports SQL_WCHAR / SQL_WVARCHAR - the same divergence already recorded for
the PostgreSQL Unicode driver. The macOS leg passing while Windows failed is
exactly that split.

Route every SQLite character expectation through a `SqliteText(narrow, wide)`
selector, and record the two columns that only deviate on a wide-reporting
driver (varcharColumn, varcharPK) as dialect exceptions of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Yaraslaut
Yaraslaut merged commit c8546de into master Aug 21, 2026
30 checks passed
@Yaraslaut
Yaraslaut deleted the chore/191-ddl2cpp-column-type-coverage branch August 21, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add better ddl2cpp test coverage of column types

1 participant