Repository navigation
fix(mssql): read DATETIME and SMALLDATETIME as the engine's own text, and export them with a T - #1639
Conversation
libredb#1178 made time, date and datetime2 read as the engine's own text, because none of them holds a zone. datetime and smalldatetime are zoneless in the same way and were left out, so a DATETIME beside a DATETIME2 holding the same reading rendered as 2026-10-04T12:34:56.123Z against 2026-10-04 12:34:56.123. Both are added to ZONELESS_VALUE_DECLARATIONS. The driver sends no scale for them (tedious reads one only for time, datetime2 and datetimeoffset), so their fraction digits come from the type: three for datetime, whose 1/300 s ticks tedious already rounds to the millisecond the engine prints, and none for smalldatetime, which is stored to the minute. The live guard reads both against the engine's style 121 and 120 text, since their default CONVERT style prints "Sep 1 2026 10:30AM". Refs libredb#1452
The provider now reads datetime and smalldatetime as the engine's yyyy-mm-dd hh:mi:ss text. Unlike datetime2 and date, those two types read that form through the session's DATEFORMAT. Measured on SQL Server 2022 CU27: under british, Deutsch and Francais (dmy) '2026-10-04 12:34:56.123' replays as April 10th and '2026-10-25 23:59:59.997' is refused as out of range. The export used to write the Date's ISO text with a Z, which replays the same under every language, so writing the new text as it is would have broken replay on those servers. mssqlLiteral now writes that text with its space as a T, the ISO 8601 form every DATEFORMAT reads the same. Any other cell falls through to the generic writer as before, and datetime2 and date text is left as it is. Refs libredb#1452
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cevheri
left a comment
There was a problem hiding this comment.
I checked the provider change, the export commit, the doc sections and the tests: the new tests fail without the fix and the docs match the code. One write path is still open.
- Make an inline grid edit of a
datetimeorsmalldatetimecell write the same date under every DATEFORMAT.
Cause: the edit box opens with the cell text (ResultsGrid.tsx:820), anduse-inline-editing.tsbinds it as an nvarchar@pNparameter, so it goes through the same DATEFORMAT conversion your §5.5 measurement shows. Before this PR the box held the...Zform, which replays the same under every language; now it holds2026-10-04 12:34:56.123, so a user on abritishlogin who changes only the time gets April 10th stored.
Change: inuse-inline-editing.ts, when the edited column's declared type (fromresult.columnTypes, read the waykeyColumnKindreads it) isdatetimeorsmalldatetimeand the new value matches the provider's text form, send it with the space as aT. ExportMSSQL_DATETIME_TEXTfromtyped-literals.tsand reuse it, so the export and the edit share one definition.
Done when: a test intests/hooks/shows an edit of adatetimeand asmalldatetimecell binding2026-10-04T12:34:56.123while adatetime2cell's text is bound unchanged, anddocs/providers/mssql.md§5.3 or §5.5 mentions the edit path next to the export.
The grid opens its editor on the text the provider now reads these two types as, and the hook bound an edit of it as an nvarchar parameter, which SQL Server reads into them through the session's DATEFORMAT. Measured on SQL Server 2022 CU27 with the hook's own UPDATE under SET LANGUAGE british: 2026-10-04 12:34:57.123 was stored as April 10th and 2026-10-25 23:59:58.997 was refused as out of range, while the same text with a T was stored as written. The provider's own sessions read mdy today: tedious opens each one with set dateformat mdy and set language us_english, whatever the login's default language, and a SET LANGUAGE a user runs ends with its own sp_executesql call. So this keeps the edit from depending on that driver default rather than fixing a date stored wrong today. An edit of a datetime or smalldatetime column that is still in that form is now bound with its space as a T, matched by the MSSQL_DATETIME_TEXT the export uses, which typed-literals.ts now exports. The declared type is read the way keyColumnKind reads it, through two helpers split out of it. A datetime2 edit, and text in any other shape, is bound as typed. Refs libredb#1452
|
Thanks for the review. Done in 6822b4c, added on top of the two commits you reviewed (no rebase, so the diff is just this change):
One thing I found while testing this live on SQL Server 2022 CU27 that you should know: through LibreDB's own connection the wrong date doesn't happen today. tedious starts every connection with The mechanism you described is real, though. The hook's own Full suite and coverage pass on Linux, and so do format, lint, typecheck and knip. |
cevheri
left a comment
There was a problem hiding this comment.
Thanks @Aditya-XR. Checked 6822b4c: the new hook tests fail without the change and pass with it, the merge with main is clean, and tedious 20.3.0 does open with set dateformat mdy, so the docs say it right. Approving.
Description
SQL Server
DATETIMEandSMALLDATETIMEcells now read as the engine's own wall-clock text, the same waytime,dateanddatetime2have since #1178. ADATETIMEnext to aDATETIME2holding the same reading used to render as2026-10-04T12:34:56.123Zagainst2026-10-04 12:34:56.123. Both now show2026-10-04 12:34:56.123.Two commits:
fix(mssql): the display fix the issue asks for.datetimeandsmalldatetimeare added toZONELESS_VALUE_DECLARATIONS. The driver sends noscalefor these two types (tediousmetadata-parser.jsreads one only fortime,datetime2anddatetimeoffset), so the fraction digits come from the type through a newFIXED_FRACTION_DIGITSmap.datetimegets 3 digits, and its 1/300 s ticks already arrive rounded to the millisecond the engine prints (.000/.003/.007).smalldatetimegets 0 and always shows:00seconds.datetimeoffsetstill stays an instant.fix(export): keeps the SQL INSERT export replaying correctly. I mentioned this in my claim comment. Unlikedatetime2anddate,DATETIMEandSMALLDATETIMEreadyyyy-mm-dd hh:mi:ssthrough the session's DATEFORMAT. Before commit 1, the export wrote these cells as theDate's ISO text with aZ, which replays the same under every language. After commit 1 the cell is the engine text with a space, and under a dmy language (british,Deutsch,Français)'2026-10-04 12:34:56.123'replays as April 10th, and'2026-10-25 23:59:59.997'is refused as out of range. SomssqlLiteralnow writes that exact text with its space as aT(N'2026-10-04T12:34:56.123'), the ISO 8601 form every DATEFORMAT reads the same. Only a cell declareddatetime/smalldatetimewhose text is exactly the provider's form takes this path. Everything else goes to the generic writer as before, anddatetime2/datetext is not touched.I kept them as separate commits so the export change can be dropped or split out if you'd rather handle it separately.
Live measurement
SQL Server 2022 CU27 (16.0.4295.3),
mcr.microsoft.com/mssql/server:2022-latest,mssql12.7.2 /tedious20.3.0, Bun 1.4.2.tests/live/mssql-zoneless-values.tsnow also guardsdt DATETIME,dtTick DATETIMEandsdt SMALLDATETIMEagainst the engine's ownCONVERTtext (styles 121 and 120, because their default style printsSep 1 2026 10:30AM):mainDATETIME2026-09-01T10:30:00.1232026-09-01 10:30:00.1232026-09-01T10:30:00.123Z2026-09-01 10:30:00.123DATETIME2026-09-01T23:59:59.9982026-09-01 23:59:59.9972026-09-01T23:59:59.997Z2026-09-01 23:59:59.997SMALLDATETIME2026-09-01T10:30:29.9992026-09-01 10:31:002026-09-01T10:31:00.000Z2026-09-01 10:31:00The guard fails on
mainand passes with this PR. The existingtime/date/datetime2columns and thedatetimeoffsetcontrol pass unchanged.Export replay (a throwaway script, not committed): two rows read through the provider, sent through
JSON.parse(JSON.stringify(...))the wayPOST /api/db/queryreturns them, exported withbuildResultExport("sql-insert", …), replayed into an empty table underSET LANGUAGE, and compared on the server:Rows:
DATETIME,SMALLDATETIME,DATETIME2(3)= (2026-10-04 12:34:56.123,2026-10-04 12:35:00,2026-10-04 12:34:56.123) and (2026-10-25 23:59:59.997,2026-10-25 08:15:00,2026-10-25 23:59:59.997).us_englishbritishmain:N'2026-10-04T12:34:56.123Z'N'2026-10-04 12:34:56.123'dtandsdtstored as 2026-04-10; row 2 refused:The conversion of a nvarchar data type to a datetime data type resulted in an out-of-range value.N'2026-10-04T12:34:56.123'(theDATETIME2cell staysN'2026-10-04 12:34:56.123')Type of Change
Related Issue
Closes #1452
Changes Made
src/lib/db/providers/sql/mssql.ts:datetimeandsmalldatetimeadded toZONELESS_VALUE_DECLARATIONS, newFIXED_FRACTION_DIGITSmap, andconvertZonelessValuesuses the type's digits when the column has noscale. Docblock updated.src/lib/db/providers/sql/column-types.ts: theMssqlColumnMetadata.scaledocblock now saysscaleis absent fordatetimeandsmalldatetimetoo.src/lib/export/typed-literals.ts:mssqlLiteralwrites adatetime/smalldatetimecell that matchesMSSQL_DATETIME_TEXTwith aT. The BIT handling is unchanged.tests/integration/db/mssql-provider.test.ts: 3 tests in the SQL Server time values arrive as a date on 1970-01-01 #1132 zoneless block:datetimewith 3 digits (incl. a whole second and a 1/300 s tick),smalldatetimeto the minute, and the issue's ownDATETIMEbesideDATETIME2repro withdatetimeoffsetstill an instant.tests/unit/lib/export/typed-literals.test.ts: 2 tests. TheTform fordatetime/smalldatetime(incl. a padded mixed-case declaration). Everything else falls through:datetime2, an undeclared column, ISO text withZ, 7 fraction digits, no seconds, leading text, aDate.tests/unit/lib/export/result-export.test.ts: 1 test, a full INSERT withdatetime,smalldatetimeanddatetime2side by side.tests/live/mssql-zoneless-values.ts:dt,dtTickandsdtcolumns guarded against styles 121/120.docs/providers/mssql.md: §5.3 bullet with the measurement table, §5.5 explains why the export writes theT, with the replay measurement, and §13.4 lists the new guarded types.Testing
I have tested this locally
I have added/updated tests
All existing tests pass
5 of the 6 new tests fail on
main. The 6th pins the fall-through cases, whichmainalready leaves to the generic writer. Targeted:mssql-provider.test.ts254 pass,typed-literals.test.ts37 pass,result-export.test.ts188 pass.Linux (Ubuntu 24.04, WSL2, Node 24.21, Bun 1.4.2), CI's coverage command:
bun tests/run-tests.ts --coverage --merge-into=coverage/lcov.info --jobs=8gave1133 files: 1133 passed | 44018 tests: 44002 pass, 16 skip, andbun run coverage:checkgavecheck-coverage: OK — 121844/121844 lines (100.00%). 15 of the 16 skips name a tool this machine does not have (zip,7z) or are the runner's own skip-path self-test. The 16th is atest.skipinagent-investigation.test.ts. Helm wasn't on that machine, so the chart tests ran separately after installing Helm 4.1.3 andhelm dependency build charts/libredb-studio:LIBREDB_REQUIRE_HELM=1overtests/unit/helm-chart-*.test.tsgave 17 files, 251 pass.format,lint(0 errors, no warnings in the touched files),typecheck,knip,chart:check,channels:showcase:check,readme:check,security:check,build,build:libandattwall pass.Live:
tests/live/mssql-zoneless-values.tspasses. Withmain'smssql.tsandtyped-literals.tsit fails on exactlydt,dtTickandsdt. Results are in the tables above.Test Environment
mainat aad0c73)mssql12.7.2 /tedious20.3.0Checklist
bun run test:coverageandbun run coverage:check)src/lib/db/providers/, I updated the matchingdocs/providers/documentation andtests/integration/db/tests in the same PR (provider triad)Additional Notes
The coverage-gate box is left for CI's Linux runner to confirm. It passed locally (see Testing).