Skip to content

fix(editor): bind an Oracle DATE and TIMESTAMP edit through TO_DATE, TO_TIMESTAMP, or FROM_TZ - #1818

Merged
cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/1807-oracle-inline-edit-temporal
Oct 19, 2026
Merged

cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/1807-oracle-inline-edit-temporal

Conversation

@Aditya-XR

Copy link
Copy Markdown
Contributor

Description

When a user edits an Oracle DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, or TIMESTAMP WITH LOCAL TIME ZONE cell in the Results Grid (src/hooks/use-inline-editing.ts), the inline editor previously emitted a bare "COL" = :1 placeholder and bound the edited cell string directly.

Because OracleProvider returns DATE and zoneless TIMESTAMP columns as wall-clock strings ("YYYY-MM-DD HH:MM:SS[.ffffff]", #1131 / PR #1225 and #1498 / PR #1612) and TIMESTAMP WITH [LOCAL] TIME ZONE columns arrive over POST /api/db/query JSON as ISO-8601 UTC strings ("YYYY-MM-DDTHH:MM:SS.sssZ", #1224 / PR #1500), node-oracledb binds :1 as DB_TYPE_VARCHAR. Oracle then parses :1 through the session's NLS_DATE_FORMAT (DD-MON-RR), NLS_TIMESTAMP_FORMAT (DD-MON-RR HH.MI.SSXFF AM), or NLS_TIMESTAMP_TZ_FORMAT (DD-MON-RR HH.MI.SSXFF AM TZR), refusing all three formats (ORA-01861 / ORA-01843).

Following @kaya-abdullah's notes on #1807 (and the SQL Server MSSQL_DATETIME_TEXT pattern from #1452 / PR #1639 and temporalLiteral in src/lib/sql/values.ts):

  • Shared masks and regexes (src/lib/export/result-export.ts): ORACLE_TIMESTAMP_TYPE, ORACLE_ZONED_TIMESTAMP_TYPE, ORACLE_DATE_MASK, ORACLE_BC_DATE_MASK, ORACLE_ZONELESS_TEXT, and ISO_INSTANT_TEXT are exported while keeping the export literal helpers (oracleDateLiteral, oracleTextLiteral, oracleZonedTextLiteral) unchanged.
  • Inline editing (src/hooks/use-inline-editing.ts): oracleBoundEdit / boundEditValue reads the column's declared type from the result.columnTypes map (declaredTypeOf(result, c.columnId)) and wraps :n in the matching conversion expression when the edited text still matches the column's form:
    • DATE matching [-]?YYYY-MM-DD HH:MM:SS → TO_DATE(:n, '[S]YYYY-MM-DD HH24:MI:SS')
    • TIMESTAMP[(p)] matching [-]?YYYY-MM-DD HH:MM:SS[.ffffff] → TO_TIMESTAMP(:n, '[S]YYYY-MM-DD HH24:MI:SS[.FF]')
    • TIMESTAMP[(p)] WITH [LOCAL] TIME ZONE matching Date#toISOString (YYYY-MM-DDTHH:MM:SS.sssZ, verified via round-trip new Date(text).toISOString() === text) → FROM_TZ(TO_TIMESTAMP(:n, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC'), binding "YYYY-MM-DD HH:MM:SS.sss" in :n
    • Any other text shape (such as a session NLS literal typed by the user), NULL, an undeclared column, or any other dialect continues to bind as plain :n.

Live measurement (Before / After on Oracle 26ai Free)

Measured 2026-10-18 against gvenzl/oracle-free:slim (Oracle AI Database 26ai Free Release 23.26.3.0.0 - Develop, Learn, and Run for Free), oracledb 26.0.0, Bun 1.4.2, process TZ=Europe/Istanbul.

Session NLS defaults read from NLS_SESSION_PARAMETERS:

  • NLS_DATE_FORMAT: 'DD-MON-RR'
  • NLS_TIMESTAMP_FORMAT: 'DD-MON-RR HH.MI.SSXFF AM'
  • NLS_TIMESTAMP_TZ_FORMAT: 'DD-MON-RR HH.MI.SSXFF AM TZR'

Source table read through OracleProvider.query() and passed through JSON.parse(JSON.stringify(read)) as POST /api/db/query returns it (columnTypes map: {"ID":"NUMBER","D":"DATE","TS":"TIMESTAMP","TTZ":"TIMESTAMP WITH TIME ZONE","TLTZ":"TIMESTAMP WITH LOCAL TIME ZONE"}):

Column (columnTypes[col]) Edited cell text main (UPDATE & params) main result on Oracle This PR (UPDATE & params) This PR result on Oracle
D (DATE) "2026-09-02 09:15:00" SET "D" = :1 WHERE "ID" = :2 ["2026-09-02 09:15:00", 1] ORA-01861: literal does not match format string SET "D" = TO_DATE(:1, 'YYYY-MM-DD HH24:MI:SS') WHERE "ID" = :2 ["2026-09-02 09:15:00", 1] D_EQ = EQUAL ("2026-09-02 09:15:00")
TS (TIMESTAMP) "2026-09-02 10:30:00.345" SET "TS" = :1 WHERE "ID" = :2 ["2026-09-02 10:30:00.345", 1] ORA-01843: An invalid month was specified. SET "TS" = TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF') WHERE "ID" = :2 ["2026-09-02 10:30:00.345", 1] TS_EQ = EQUAL ("2026-09-02 10:30:00.345")
TTZ (TIMESTAMP WITH TIME ZONE) "2026-08-25T17:11:12.345Z" SET "TTZ" = :1 WHERE "ID" = :2 ["2026-08-25T17:11:12.345Z", 1] ORA-01843: An invalid month was specified. SET "TTZ" = FROM_TZ(TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC') WHERE "ID" = :2 ["2026-08-25 17:11:12.345", 1] TTZ_EQ = EQUAL ("2026-08-25T17:11:12.345Z")
TLTZ (TIMESTAMP WITH LOCAL TIME ZONE) "1999-01-01T18:04:05.006Z" SET "TLTZ" = :1 WHERE "ID" = :2 ["1999-01-01T18:04:05.006Z", 1] ORA-01843: An invalid month was specified. SET "TLTZ" = FROM_TZ(TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC') WHERE "ID" = :2 ["1999-01-01 18:04:05.006", 1] TLTZ_EQ = EQUAL ("1999-01-01T18:04:05.006Z")
D (DATE, BC) "-0044-03-15 00:00:00" SET "D" = :1 WHERE "ID" = :2 ["-0044-03-15 00:00:00", 1] ORA-01861 / ORA-01843 SET "D" = TO_DATE(:1, 'SYYYY-MM-DD HH24:MI:SS') WHERE "ID" = :2 ["-0044-03-15 00:00:00", 1] D_EQ = EQUAL ("-0044-03-15 00:00:00")
TS (TIMESTAMP, BC) "-0044-03-15 10:30:00.5" SET "TS" = :1 WHERE "ID" = :2 ["-0044-03-15 10:30:00.5", 1] ORA-01843 SET "TS" = TO_TIMESTAMP(:1, 'SYYYY-MM-DD HH24:MI:SS.FF') WHERE "ID" = :2 ["-0044-03-15 10:30:00.5", 1] TS_EQ = EQUAL ("-0044-03-15 10:30:00.5")

Verbatim run log against gvenzl/oracle-free:slim:

BANNER: Oracle AI Database 26ai Free Release 23.26.3.0.0 - Develop, Learn, and Run for Free
oracledb: 26.0.0 | Bun: 1.4.2 | TZ: Europe/Istanbul
NLS_SESSION_PARAMETERS: [{"PARAMETER":"NLS_DATE_FORMAT","VALUE":"DD-MON-RR"},{"PARAMETER":"NLS_TIMESTAMP_FORMAT","VALUE":"DD-MON-RR HH.MI.SSXFF AM"},{"PARAMETER":"NLS_TIMESTAMP_TZ_FORMAT","VALUE":"DD-MON-RR HH.MI.SSXFF AM TZR"}]
INITIAL ROW OVER HTTP: {"ID":1,"D":"2026-09-01 00:00:00","TS":"2026-09-01 10:30:00.345","TTZ":"2026-08-24T17:11:12.345Z","TLTZ":"2026-08-24T17:11:12.345Z"}
COLUMN TYPES: {"ID":"NUMBER","D":"DATE","TS":"TIMESTAMP","TTZ":"TIMESTAMP WITH TIME ZONE","TLTZ":"TIMESTAMP WITH LOCAL TIME ZONE"}

=== BEFORE (main: bare :1 bind) ===
BEFORE D (DATE):
  sql:    UPDATE LIBREDB_IE_27C2C7A3 SET "D" = :1 WHERE "ID" = :2
  params: ["2026-09-02 09:15:00",1]
  error:  ORA-01861: literal does not match format string
BEFORE TS (TIMESTAMP):
  sql:    UPDATE LIBREDB_IE_27C2C7A3 SET "TS" = :1 WHERE "ID" = :2
  params: ["2026-09-02 10:30:00.345",1]
  error:  ORA-01843: An invalid month was specified.
BEFORE TTZ (TIMESTAMP WITH TIME ZONE):
  sql:    UPDATE LIBREDB_IE_27C2C7A3 SET "TTZ" = :1 WHERE "ID" = :2
  params: ["2026-08-25T17:11:12.345Z",1]
  error:  ORA-01843: An invalid month was specified.
BEFORE TLTZ (TIMESTAMP WITH LOCAL TIME ZONE):
  sql:    UPDATE LIBREDB_IE_27C2C7A3 SET "TLTZ" = :1 WHERE "ID" = :2
  params: ["1999-01-01T18:04:05.006Z",1]
  error:  ORA-01843: An invalid month was specified.

=== AFTER (this PR: wrapped :n bind) ===
AFTER D (DATE):
  sql:          UPDATE LIBREDB_IE_27C2C7A3 SET "D" = TO_DATE(:1, 'YYYY-MM-DD HH24:MI:SS') WHERE "ID" = :2
  params:       ["2026-09-02 09:15:00",1]
AFTER TS (TIMESTAMP):
  sql:          UPDATE LIBREDB_IE_27C2C7A3 SET "TS" = TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF') WHERE "ID" = :2
  params:       ["2026-09-02 10:30:00.345",1]
AFTER TTZ (TIMESTAMP WITH TIME ZONE):
  sql:          UPDATE LIBREDB_IE_27C2C7A3 SET "TTZ" = FROM_TZ(TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC') WHERE "ID" = :2
  params:       ["2026-08-25 17:11:12.345",1]
AFTER TLTZ (TIMESTAMP WITH LOCAL TIME ZONE):
  sql:          UPDATE LIBREDB_IE_27C2C7A3 SET "TLTZ" = FROM_TZ(TO_TIMESTAMP(:1, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC') WHERE "ID" = :2
  params:       ["1999-01-01 18:04:05.006",1]

ROW AFTER UPDATES OVER HTTP: {"ID":1,"D":"2026-09-02 09:15:00","TS":"2026-09-02 10:30:00.345","TTZ":"2026-08-25T17:11:12.345Z","TLTZ":"1999-01-01T18:04:05.006Z"}
SERVER COMPARISON: {"D_EQ":"EQUAL","TS_EQ":"EQUAL","TTZ_EQ":"EQUAL","TLTZ_EQ":"EQUAL"}

=== BC & WHOLE-SECOND TIMESTAMP CASES ===
BC AFTER D: UPDATE LIBREDB_IE_27C2C7A3 SET "D" = TO_DATE(:1, 'SYYYY-MM-DD HH24:MI:SS') WHERE "ID" = :2 | params=["-0044-03-15 00:00:00",1]
BC AFTER TS: UPDATE LIBREDB_IE_27C2C7A3 SET "TS" = TO_TIMESTAMP(:1, 'SYYYY-MM-DD HH24:MI:SS.FF') WHERE "ID" = :2 | params=["-0044-03-15 10:30:00.5",1]
BC SERVER COMPARISON: {"D_TEXT":"-0044-03-15 00:00:00","TS_TEXT":"-0044-03-15 10:30:00.5","D_EQ":"EQUAL","TS_EQ":"EQUAL"}

tests/live/oracle-zoneless-values.ts also passed unchanged against the same container (replayed id=1..4: D EQUAL, TS EQUAL, TTZ EQUAL, TLTZ EQUAL).

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Related Issue

Closes #1807

(I also opened issue #1807 reporting this bug; builds on my earlier Oracle and SQL Server temporal PRs #1225, #1500, and #1639, plus issue #1498.)

Changes Made

  • src/lib/export/result-export.ts: Export the Oracle temporal type regexes (ORACLE_TIMESTAMP_TYPE, ORACLE_ZONED_TIMESTAMP_TYPE), format masks (ORACLE_DATE_MASK, ORACLE_BC_DATE_MASK), and text regexes (ORACLE_ZONELESS_TEXT, ISO_INSTANT_TEXT). The export literal helpers remain unchanged.
  • src/hooks/use-inline-editing.ts: Add oracleBoundEdit and boundEditValue using the shared masks and regexes from result-export.ts so edits on Oracle DATE, TIMESTAMP, and TIMESTAMP WITH [LOCAL] TIME ZONE columns bind :n through TO_DATE, TO_TIMESTAMP, or FROM_TZ(TO_TIMESTAMP(:n, 'YYYY-MM-DD HH24:MI:SS.FF3'), 'UTC').
  • tests/hooks/use-inline-editing.test.ts: Add 20 unit tests covering all 4 Oracle temporal column types (DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE), BC years (SYYYY), whole-second vs fractional-second TIMESTAMP, parameterized and lowercase padded declarations, fallback to plain :1 (fractional DATE, bare date without time, session NLS literal, numeric offset, impossible calendar date 2026-02-30, out-of-range month 2026-13-01, six-digit signed year, VARCHAR2, undeclared column, absent columnTypes), non-Oracle dialects, and NULL.
  • docs/providers/oracle.md: Update §5.3, §5.5, and §10 (supportsInlineRowEdit) with the inline-editing behavior and live measurement on 26ai Free.

Testing

  • Tested locally with bun run dev

  • Tested Docker build (docker build -t libredb-studio .)

  • Added/updated unit tests

  • Tested with multiple database types (if applicable)

  • bun test --coverage result-export use-inline-editing: 394 pass, 0 fail (1,120 expect() calls), 100.00% function and 100.00% line coverage on both src/hooks/use-inline-editing.ts and src/lib/export/result-export.ts.

  • bun run format, bun run only:check, bun run lint:oxc, bunx eslint src/lib/export/result-export.ts src/hooks/use-inline-editing.ts tests/hooks/use-inline-editing.test.ts (0 errors, 0 new warnings), bun run typecheck, bun run knip, bun run readme:check, bun run channels:showcase:check, bun run distribution:check, bun run security:check, and bun run chart:check all pass.

  • Live verification on gvenzl/oracle-free:slim (Oracle AI Database 26ai Free Release 23.26.3.0.0): both the before/after inline-edit UPDATE probe above and tests/live/oracle-zoneless-values.ts pass.

Test Environment

  • LibreDB Studio Version: 1.0.0 (rebased on main at 15411230)
  • Browser: N/A (hook & SQL generation change)
  • OS: Ubuntu 24.04 LTS (WSL2) and Windows 11
  • Node.js/Bun Version: Node.js v22.22.2 / Bun 1.4.2
  • Database Type: Oracle AI Database 26ai Free 23.26.3.0.0 (gvenzl/oracle-free:slim), oracledb 26.0.0

Checklist

  • My code follows the project's code style and conventions
  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas
  • I have updated the documentation (if applicable)
  • My changes generate no new warnings or errors
  • I have tested my changes thoroughly
  • All new and existing tests pass

Additional Notes

  • WHERE clause primary-key parameters remain plain :n placeholders because useInlineEditing requires an id / *_id primary key column, whereas the SET clause can target any editable column in the result set.

…TO_TIMESTAMP, or FROM_TZ

The Oracle provider reads DATE and TIMESTAMP as their wall clock (libredb#1131), and TIMESTAMP WITH [LOCAL] TIME ZONE arrives over HTTP as its Date#toISOString text (libredb#1224). The grid opens its editor on those three text forms, and the hook bound an edit of them as a bare :n parameter, which oracledb binds as DB_TYPE_VARCHAR and Oracle reads through the session NLS format.

Measured on Oracle AI Database 26ai Free 23.26.3.0.0 (gvenzl/oracle-free:slim, oracledb 26.0.0, TZ=Europe/Istanbul, default NLS_DATE_FORMAT=DD-MON-RR): a bare :1 bind was refused with ORA-01861 on DATE and ORA-01843 on TIMESTAMP, TIMESTAMP WITH TIME ZONE, and TIMESTAMP WITH LOCAL TIME ZONE, while wrapping :n in TO_DATE, TO_TIMESTAMP, or FROM_TZ(TO_TIMESTAMP(:n, ...), UTC) stored every value equal on the server, BC dates included.

result-export.ts now exports the masks and regexes (ORACLE_TIMESTAMP_TYPE, ORACLE_ZONED_TIMESTAMP_TYPE, ORACLE_DATE_MASK, ORACLE_BC_DATE_MASK, ORACLE_ZONELESS_TEXT, ISO_INSTANT_TEXT), and use-inline-editing.ts wraps :n through them in oracleBoundEdit / boundEditValue. Any other text shape, NULL, or another column/dialect still binds as :n untouched.

Closes libredb#1807
Copilot AI balanced review requested due to automatic review settings October 18, 2026 07:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Resolve the oracle.md capability row against libredb#1823 (createTable) and replace the two new em dashes in the inline-editing paragraph.
@cevheri
cevheri merged commit 055013c into libredb:main Oct 19, 2026
35 of 36 checks passed
cevheri added a commit that referenced this pull request Oct 19, 2026
…placeholder, inline edit and export rules beside its provider (#1850)

* test(sql-writing): pin today's literal, identifier and placeholder output per engine

* feat(sql-writing): the SqlWritingSpec contract and its shared building blocks

* feat(sql-writing): declare each engine's literal, identifier and placeholder rules beside its provider

* refactor(sql-writing): quoteLiteral, positionalPlaceholder and quoteIdentifier read the engine's module

LITERAL_ESCAPE, BACKSLASH_ESCAPES and the placeholder and identifier switches are gone; each reader asks sqlWritingFor for the engine's spec, and every public signature is unchanged. The search docs and their citation pins name the engine modules instead of the deleted table.

* test(sql-writing): one engine's module cannot reach another engine's output

* docs(sql-writing): each provider doc names its literal, identifier and placeholder rules

Every docs/providers/<id>.md gains a SQL writing subsection that links the
engine's module and names its literal, identifier and placeholder building
blocks, with the measurements the module's comments carry written out as
prose. tests/unit/db/sql-writing/docs.test.ts holds each subsection to the
module's functions by name.

ADDING_A_PROVIDER.md replaces the LITERAL_ESCAPE and positionalPlaceholder()
items with the placement rule, the SQL_WRITING row and the UPDATE_GOLDEN
step; ARCHITECTURE.md names src/lib/db/sql-writing/ beside QUERY_DIALECTS.
The oxia and s3 doc tests list the new subsection and module row, and the
search citation list measures SQL_WRITING.

* fix(sql-writing): an empty or null type reads the no-connection spec, as every writer did before

The old writers tested `dialect ? ... : "standard"`, so the empty string a published component's `databaseType?: string` can carry got the standard literal. sqlWritingFor tested only undefined and sent it to the unknown-type spec, which doubles the backslash. The new empty-string golden is rendered from origin/main.

* test(sql-writing): the contract's types and shared building blocks import nothing but types

* docs(sql-writing): point the stale LITERAL_ESCAPE and values.ts escape citations at the engine's module

* test(guard): drop the SQL writing debt entries this branch moves into per-engine modules

* test(sql-writing): pin today's inline edit output per engine

Every golden file gains one line per inline edit case and per key case:
the key check request, the executeQuery calls and the toasts of one
apply through useInlineEditing. Lines are only appended; src/ is
unchanged.

The isolation subprocess spreads the real engine module under the
sentinel, gives the sentinel an inlineEdit field, and writes its output
with an awaited Bun.write, because console.log of this much output into
the pipe was cut at 64 KiB. The fixtures import the hook after the
sonner mock, since a static import links the real sonner first in a
subprocess without preload.

* feat(sql-writing): the inline edit contract and its shared type-name sets

InlineEditSpec states the key-column type names and the optional bound
edit one engine answers for. The instant, float64 and exact decimal sets
and the unwrappedType and firstWord helpers are copied into shared.ts
with their docblocks; the hook keeps its own copies until it reads the
contract.

* feat(sql-writing): each engine states the key column kinds and bound edit its inline editor reads

SqlWritingSpec gains inlineEdit. Every engine but sqlite and libsql, and
both named specs, reads the shared instant, exact decimal and float64
sets. SQLite and libSQL each state empty instant and exact decimal sets
and float64 "all" in their own file, with the measurements that were on
the hook's dialect sets.

MSSQL_DATETIME_TEXT moves into the mssql module and the six Oracle date
constants into the oracle module, each with its docblock; the exports
and the hook import them from there. The mssql and oracle bound edits
are copies of the hook's, which keeps its own until Task 3.4.

* refactor(sql-writing): the inline editor reads key column kinds and bound edits from the engine's module

keyColumnKind reads instantTypeNames, float64TypeNames and
exactDecimalTypeNames from sqlWritingFor(dialect).inlineEdit, and the SET
right-hand side comes from the engine's boundEdit, falling back to the
bound text. The hook's own type-name sets, the three sqlite/libsql dialect
sets, the never-read FLOAT64_ONLY_NAMES, the mssql and oracle bound edit
helpers and their engine module imports are gone; unwrappedType and
firstWord come from the shared module. Their measured docblocks already
live in the shared and engine modules.

The guard drops the five pr 3 debt entries, the seam test holds the hook
to no export module and no engine module import, and the Oracle provider
doc points at the module that now holds the bound edit. No golden moved.

* test(sql-writing): one engine's inline edit rules cannot reach another engine's output

The isolation mock now gives the inline edit field a sentinel of its own,
because the quoted names and placeholders already put SENTINEL into every
edit's SQL. For every engine whose committed edits reach their UPDATE, each
non-NULL edit in the replaced engine's own rendering must carry the bound
edit sentinel, so the hook demonstrably read the mocked inlineEdit.

* docs(sql-writing): each provider doc names its inline edit rules

Every provider doc's SQL writing subsection now names the module's
inlineEdit: the shared INSTANT_TYPE_NAMES, FLOAT64_TYPE_NAMES and
EXACT_DECIMAL_TYPE_NAMES on 40 engines, the SQLite affinity facts with
their measurements on sqlite and libsql, and the boundEdit of mssql
(DATEFORMAT, 2026-10-12) and oracle (NLS, #1818). docs.test.ts holds
each subsection to the module.

ADDING_A_PROVIDER.md and ARCHITECTURE.md name inlineEdit and its
reader. The backlog's EXACT_DECIMAL_TYPE_NAMES citations and the
sqlite-provider test comment point at src/lib/db/sql-writing/shared.ts.

* docs(backlog): R4 is done, the inline editor reads its engine facts from each SqlWritingSpec

The three per-engine sets are inlineEdit fields of each engine's module, their measurements sit in the sqlite and libsql modules and provider docs, and the guard holds no use-inline-editing.ts entry.

* test(sql-writing): pin today's SQL export output per engine

Every golden gains the SQL INSERT, CREATE TABLE and numeric CSV output of
buildResultExport for its key, rendered under Asia/Tokyo so Oracle's local
reading of a naive Date is the same on every machine. Only lines are appended:
no value or inline edit line moves, and src/ is unchanged.

* feat(sql-writing): the export building blocks and the engine-neutral export helpers leaf

types.ts gains the export contract types (InferredKind, ScalarLiteral, TypedWriter,
ColumnWriter, ExportTypeVocabulary, ExportSpec); SqlWritingSpec is unchanged, so no
engine module moves yet.

shared.ts gains the default export tables and the type vocabulary patterns, copied with
their docblocks and values from result-export.ts, which keeps reading its own copies
until its tables become reads of each engine's spec.

export-shared.ts is the leaf of engine-neutral export helpers: UnwritableValue, asBytes,
binaryText, jsonText, one function per binary literal form, and the parsers the
typed-literal grammars share. binary.ts and json.ts re-export what moved by name, and
typed-literals.ts imports from the leaf and re-exports UnwritableValue and ScalarLiteral,
so no importer changes.

shared.test.ts also reads BARE_TYPE_FAMILY, splitArguments and ExportSpec, which have no
other reader until the export cutover and would otherwise fail knip.

* refactor(sql-writing): split typed-literals.ts by engine

Each engine's typed writer moves whole, with its tables, regexes and
docblocks, into a module beside its provider: postgres-typed-literals.ts
and <id>/typed-literals.ts for clickhouse, trino, duckdb, cassandra,
databend, snowflake and spark. The SQL Server writer moves into
mssql-sql-writing.ts next to MSSQL_DATETIME_TEXT. Each quoteLiteral call
becomes the escape function the engine's own module names as
escapeLiteral, and cassandra's quoteIdentifier becomes
identifierDoubleQuoted, so no writer imports @/lib/sql/values or
@/lib/sql/identifier. typed-literals.ts keeps typedLiteral and
TYPED_WRITERS over the nine imported writers; its unused
@/lib/non-finite import is gone. No expected value changed.

mssqlLiteral is exported for now because TYPED_WRITERS imports it; it
becomes module-local when the export spec lands in the module.

tests/unit/db/sql-writing/seam.test.ts: ALLOWED gains
@/lib/db/sql-writing/export-shared, the engine-neutral export leaf spec
5.3 [T1] lets an engine module import, since mssql-sql-writing.ts now
reads normalized from it.

Known, left to the orchestrator: the databend and snowflake seam guards
report rule 7 for their new typed-literals.ts (same class as
sql-writing.ts), and the spark seam guard's isolation test now also
names src/lib/export/typed-literals.ts importing spark/typed-literals.

* feat(sql-writing): each engine states its export facts beside its provider

SqlWritingSpec gains a required export: ExportSpec, and every engine module
and both named specs write one, value for value from today's result-export
tables: DIALECT_TYPES over STANDARD_TYPES, STANDS_ALONE, DIALECT_BARE_SPELLING,
BINARY_LITERAL, NON_FINITE_LITERALS, INSERT_SELECT_DIALECTS, TYPED_WRITERS,
DECLARED_TYPE_REWRITE and the snowflake, oracle and cassandra branches. Engines
on a default import the shared object by name; no base object is spread. The
per-engine comments of those tables, and the sentence of each docblock about
an engine, are copied beside the field they explain. result-export.ts keeps
reading its own tables, with their comments, until they are deleted.

Moved whole with their docblocks: the Oracle date helpers into
oracle-sql-writing.ts, whose masks and patterns become module-local;
snowflakeVectorDeclaration into snowflake/typed-literals.ts, where
snowflakeVectorType becomes module-local; CASSANDRA_PRIMARY_KEY_NOTE into
cassandra/sql-writing.ts. result-export.ts imports them from there. mssqlLiteral
moves above MSSQL_SQL_WRITING, which now reads it. Em dashes in moved
comments became a colon or a comma.

census.test.ts pins every export fact per id and the two named specs, and
exercises the oracle, snowflake and cassandra hooks against today's output.
seam.test.ts lets an engine module import its own typed-literals sibling
(spec 5.3) and holds that sibling to the same rule. No expected value changed.

* refactor(sql-writing): the SQL export reads the engine's module

buildResultExport now asks sqlWritingFor(dialect).export for every type name, literal and statement shape instead of reading tables keyed by type id.
Every engine's golden output is unchanged.
Three goldens move on purpose, none of them an engine:
"constructor" read Object.prototype through the old tables; Object.hasOwn now gives it the unknown-type spec, so its lines equal not-an-engine's.
"" and "not-an-engine" threw "TypeError: undefined is not an object (evaluating 'standsAlone.includes')" on every ddl line of a column with a bare declared name; they now get the portable spelling a missing connection already gets.
The app passes activeConnection?.type, so only an embedding host with a cast could reach either.

* test(sql-writing): one engine's export facts cannot reach another engine's output

The seam test now holds each typed writer sibling to the engine module's import rule, checks that only its owner imports it under src/, keeps the export path off the provider tree (spec 5.4), and loads every sibling and export-shared.ts first in a fresh process. The isolation sentinel gains an export field, and the replaced engine's ddl(inferred) line must show the sentinel type.

* docs(sql-writing): each provider doc names its export rules

Every provider doc's SQL writing subsection gains an export paragraph
naming the binary literal form, each export fact by the shared constant
it holds or by its field name, every export hook the module defines and
its typed-literals sibling; docs.test.ts holds each paragraph to the
module.

The measurements the deleted result-export.ts tables carried are now
prose beside each engine: the stands-alone probe (Postgres 18.4, MySQL
26.7.0, Oracle Free 23ai, SQL Server 2022 CU26, SQLite through
bun:sqlite, ClickHouse 26.7.1, Trino 476, Cassandra 5.0.9) and each
named absence, the binary literal measurements, the non-finite replays,
Cassandra's frozen and primary key refusals, Snowflake's PARSE_JSON and
VECTOR refusals, MySQL's bare temporal rounding and Db2's BX form.

ADDING_A_PROVIDER.md drops the result-export.ts checklist item, which
SQL_WRITING now covers, and states the module import rule with
export-shared.ts and the typed-literals sibling. Stale citations of the
deleted tables in provider docs, BACKLOG X12, U143 and U144 and two code
comments now name where each lives.

* test(sql-writing): the export reads the engine's own type vocabulary

The isolation sentinel passed the engine's real vocabulary through, which
is the shared one for every engine, so an export that imported the shared
vocabulary directly passed every test. The sentinel now states a
vocabulary that disagrees with the shared one on each fixture type, and
the replaced engine's ddl("varchar2") and csv(numeric) lines must show
it. Measured: importing any one of BARE_TYPE_FAMILY, FLOAT_TYPE,
EXACT_NUMERIC_TYPE or CLICKHOUSE_WRAPPER in result-export.ts fails all 42
cases.

* test(sql-writing): spell the sentinel wrapper without an escape oxlint refuses

* test(sql-writing): a key outside the union exports what an unknown type exports

The prototype keys, a miscased or padded type id and null used to throw
or write [object Object] in the export; the registry's Object.hasOwn
lookup now sends them to the named specs. Pin that output against the
committed not-an-engine export (and null against no connection), so the
move is held by a test and not only by registry membership.

* docs(sql-writing): each JSON engine's binary literal comment states its own fact

Drop the cross-engine count and the pointer to values.ts, which no longer
holds that rationale.

* docs(sqlite): the REAL row editor measurement is historical

SQLite's module declares float64TypeNames "all", so the row editor no
longer reads the shared set for it; the comment named shared.ts as its
source.

* docs(architecture): the result export reads the row's export field

* test(seam-guard): the Databend and Snowflake SQL writing modules import their contract's building blocks

sql-writing.ts and its typed-literals.ts sibling join migration-ddl.ts in CONTRACT_VALUES, so rule 7 admits @/lib/db/sql-writing/* for those two files only; a planted import of it from another file still fails by name.

* test(seam-guard): each InfluxDB SQL writing row is a layer of its own

influxdb-sql-writing.ts and influxdb3-sql-writing.ts may import values only from @/lib/db/sql-writing/* and from none of the directory's modules, the shape the migration DDL and SQL grammar layers already have.

* test(seam-guard): the etcd module table names sql-writing.ts

* test(seam-guard): the SQL writing registry may import the Spark SQL writing row

CONTRACT_REGISTRIES admits src/lib/db/sql-writing/index.ts importing spark/sql-writing and nothing else of the directory.

* test(sql-writing): parse src/ once for the typed writer sibling importer checks

Each of the eight "is imported under src/ by <owner>'s module only" tests re-parsed every file under src/, about 0.6 to 0.9s each on an idle machine and close to the 5000ms per-test timeout under contention.
The import index is now built once in beforeAll and every sibling check reads it.
Three copies on one core (taskset -c 0) now pass in 8.6s each with no timeout; a planted importer in cassandra/objects.ts still fails the cassandra check.

* test(sql-writing): replace engines in separating groups and size the child pool to the machine

The isolation test ran one child per engine, eight at once, whatever the core count.
It now uses the separating groups and the pool of a quarter of the cores from 3dc6166: 12 children instead of 42, each replacing a group of engines while every engine outside it is held to its golden.
Every ordered pair of engines is separated by some group, which a test asserts, and each engine's own sentinel checks run in the first child that replaces it.
tests/unit/db/isolation-children.ts is byte-identical to the copy on refactor/engine-contract-7-connection-form, so the two branches merge cleanly.
A planted leak (mysql's quoteIdentifier reading postgres's row) fails every group that replaces postgres and holds mysql real.
Measured on two cores (taskset -c 0,1): 35.9s before, 11.4s after.

* test(sql-writing): hold each typed writer's escape to its engine's own row

A typed writer sibling names its engine's escape from shared.ts rather than reading the engine's row, and binaryTextBackslash hard-codes the backslash escape.
The census now asserts each literal or identifier helper a sibling imports is the same function as its engine's escapeLiteral or quoteIdentifier, and that bigquery and couchbase, the two engines writing binary as backslash-escaped text, escape their literals with literalBackslash.
A planted change of spark's and bigquery's escapeLiteral fails both checks.

* docs(engine-contract): name the SQL writing registry and the empty debt list

With every engine-contract PR merged, CLAUDE.md lists src/lib/db/sql-writing/ among the per-engine contract registries, docs/ARCHITECTURE.md says the guard's debt list is empty, and the isolation children docblock names the sql-writing isolation test.

* docs(adding-a-provider): drop the parser scheme item the connection string registry now holds

The scheme is the new engine's ConnectionStringSpec, which the required checklist item already names, and CONNECTION_STRING is total over DatabaseType, so a missing one no longer compiles.

* refactor(sql-writing): state each fact the SQLite family shares once, by name

SQLite and libSQL held the same inline edit sets and the same standsAlone list, each with a
copy of the measured docblock. Each fact now lives once in shared.ts under a name that states
it (NO_INSTANT_TYPE_NAMES, NO_EXACT_DECIMAL_TYPE_NAMES, EVERY_TYPE_IS_FLOAT64,
VERBATIM_DECLARED_TYPES_STAND_ALONE), with its measurement beside it, and both modules
reference it; the sentences that hold for libSQL only stay in its module.

The same rule takes the type vocabulary every engine spelled out field by field
(STANDARD_TYPE_VOCABULARY) and the quoted non-finite words PostgreSQL and DuckDB both write
(QUOTED_WORD_NON_FINITE). No value changes and no golden file moves; the provider docs name
the new constants, and docs.test accepts any shared constant a field holds.
cevheri added a commit that referenced this pull request Oct 20, 2026
… budgets

The seam guard's `descriptor readers` rule walked every file of the
repository through `spellings`, which collects every identifier as well
as every string and computes a line for each. It ran out of bun's
five-second default on busy runners: on main at 6dcc67f and 9c983bb,
on #1818's macOS job, and on this branch's coverage job. The rule now
walks string nodes alone, reads no lines, and builds the module-name set
only for a file holding a match; it compares the node's own text as
before, so an escaped spelling is still caught. Measured with coverage:
1.20 s to 0.29 s alone, and under four parallel copies beside 16 busy
loops the old rule reached 3.66 s while the new file's slowest test took
1.68 s, 12 runs without a failure.

The connection-options certificate hook makes RSA keys with openssl and
carried bun's default; it now has a 60-second budget, as the seam
guard's own parse hook does, and passed the same 12 loaded runs.

D153 recorded both and is removed; the three entries that pointed at it
now say what they meant without the pointer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oracle inline row editing binds a DATE or TIMESTAMP cell as a plain string, and Oracle refuses the UPDATE

3 participants