Skip to content

driver-sql: introspectSchema() returns only { tables } — the spec IntrospectedSchema contract's dialect and (required) introspectedAt are never emitted #10998

Closed

Description

@os-warren

Found while fixing the introspection seam for #10676. Same seam, different key — filed separately so it does not ride that PR.

What

packages/spec/src/contracts/schema-diff-service.ts declares:

export interface IntrospectedSchema {
  tables: Record<string, IntrospectedTable>;
  dialect?: ...;
  introspectedAt: string;   // required
}

SqlDriver.introspectSchema() returns { tables } and nothing else. plugin.ts hands that value straight to ExternalDatasourceService, which is typed against the spec contract — so the service reads two keys the producer never sets.

Measured

Object.keys() of a real SqlDriver.introspectSchema() result, live in-memory SQLite at 368e7a06f:

["tables"]

dialect and introspectedAt are both undefined.

Consequences

Two, both silent:

  1. Type mapping runs with no dialect. ExternalDatasourceService.generateObjectDraft and refreshCatalog both call suggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect). With dialect undefined every remote column is mapped by the dialect-agnostic fallback, so any dialect-specific mapping the function implements is unreachable on the federation path — exactly the path that needs it.
  2. The persisted catalog records no dialect. refreshCatalog writes dialect: schema.dialect into the external_catalog metadata record that Studio's schema browser and the boot gate read back.

introspectedAt being absent while the contract declares it required is the sharper half: consumers are entitled to read it without a guard.

Relationship to #10676

Same root shape as the isPrimary / primaryKey collision that card is about: SqlDriver implements the IntrospectedSchema in packages/objectql/src/util.ts (which declares neither key), while everything downstream of plugin.ts is typed against the packages/spec one. #10676's fix repairs the primary-key column of that table; this is another column of it.

Whether the repair belongs in the driver (emit the spec shape) or in the spec (reconcile the two IntrospectedSchema declarations) is the same routing question #10676 raises for the column spelling, and the two should probably be decided together.

Not measured here

Only the SQLite arm was executed. introspectSchema builds its return value in one place for every dialect, so the omission is dialect-independent by construction — but Postgres/MySQL were read, not run.

Filed unassigned as a finding awaiting first-touch grading. Discovered from #10676.

Activity

  1. added theissue type on Aug 22, 2026
  2. added
    bugSomething isn't working
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 22, 2026
  3. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Blocked-by: #10676

    Triage: lands in packages/drivers/driver-sql (with the contract face in packages/spec/src/contracts/schema-diff-service.ts) ⇒ domain:engine, type Bug (a required contract key — introspectedAt — is never emitted by the producer: declared≠enforced), parked pm:blocked rather than queued.

    Why blocked, not queued: the card's own routing question — repair in the driver (emit the spec shape) vs. reconcile the two IntrospectedSchema declarations (packages/objectql/src/util.ts vs. packages/spec) — is the same seam question #10676 (already in the decision inbox) raises for the primary-key column spelling, and the filer is right that they should be decided together. Dispatching this before #10676 rules risks landing a driver-side shape the ruling then moves.

    For the unlock scan: when #10676 closes/rules, this card returns to queue with the standard merged-ref re-verification — expect #10676's fix to have possibly absorbed part of this (same seam, same service), so re-measure Object.keys() of a live introspectSchema() before dispatching.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 22, 2026
  5. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Unlock (triage seat, 2026-08-22): the blocker was "the #10676 seam ruling" — that ruling now exists (maintainer, live session, 「同意所有」 item 9: the driver emits the spec shape), so pm:blocked → pm:queue. This card is the dialect/introspectedAt half of the same driver-side alignment; #10676 is the primaryKey half. Both halves are now adjudicated with one modality and one file surface — the engine seat's five-gate fold test plausibly passes for dispatching them as one claim. Standard merged-ref re-verification applies at claim (re-measure Object.keys() of a live introspectSchema() first — the paired fix may land together).


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 22, 2026
  7. self-assigned this
    on Aug 22, 2026
  8. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2 — FOLDED into #10676 (chain head)
    Session: session_01RfyXxZ2WPjcjhuXpiQQc3y
    Branch: claude/issue-10676-driver-emits-spec-introspection-shape — shared with the chain head; this card has no branch of its own by design, which is the family-dispatch convention, not a half-state
    Worktree: objectstack-issue-10676
    Domain: domain:engine
    File surface: as declared on the chain head — driver-sql/src/sql-driver.ts (introspectSchema) · objectql/src/util.ts · pins · one authorised narrow test touch in service-datasource
    Container & model: M, mode:subagent, model: opus — one dispatch covering both members
    Clause-②: no — conformance to a contract packages/spec already declares; spec itself untouched, accept set unchanged, no public surface widened
    Serial constraints cleared: full check recorded on the chain head, #10676 — see its claim. Short form: sql-driver.ts released by PR #11104 (merged); #11009 stays serial on the same file; #10888 is package-disjoint.

    This card's independent acceptance criterion (fold gate ④ — the batch cannot silently deliver less than both halves): Object.keys() of a live SqlDriver.introspectSchema() result contains dialect and introspectedAt, with introspectedAt present unconditionally because the spec contract declares it required. The chain head's criterion is separate — generateObjectDraft driven off a real introspectSchema() result carries primaryKey. One PR, one Fixes line per member, one commit per member.

    Premise re-verified on merged origin/main @ f59035cb4, as the unlock note required rather than trusting the card: introspectSchema is at :9799 and the file emits no introspectedAt anywhere — so this half is still entirely un-repaired after #10997/#11104 landed. That PR changed only the completeness and ordering of primaryKeys values; it did not touch the return shape.

    ⛔ Not in this family, recorded so it is not absorbed by keyword: #11101 (PG/MySQL key ordering plus the silent catch … => []) and #11000 (fields.<f>.primaryKey authorability — a different key in a different place, and the ruling that released this card explicitly does not cover it).


    Generated by Claude Code

  9. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    os-dev-report

    Folded delivery — this is the chain head #10676's report, repeated verbatim on this member so neither card is blind. "issue" is the chain head by design; this card's own acceptance criterion is answered inside: Object.keys() of a live SqlDriver.introspectSchema() result is ["tables","dialect","introspectedAt"], with dialect: "sqlite" and introspectedAt a real ISO instant, measured live rather than described.

    {
      "issue": 10676,
      "status": "done",
      "branch": "claude/issue-10676-driver-emits-spec-introspection-shape",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11124",
      "premise_still_valid": true,
      "summary": "Folded delivery of #10676 + #10998 as one draft PR, one commit per member. SqlDriver.introspectSchema now emits the packages/spec IntrospectedColumn/IntrospectedSchema shape: primaryKey on every column (and isPrimary is gone, not emitted alongside), plus dialect and the required introspectedAt. IntrospectedColumn/Table/Schema in BOTH packages/drivers/driver-sql/src/sql-driver.ts and packages/objectql/src/util.ts now derive from the spec import instead of re-declaring it, so a key added to the contract fails their tsc until the producer emits it; packages/spec itself is untouched. Two premise refinements, both measured rather than assumed: (1) PR #11073 landed the same ruling's item 8 while this card sat, so generateObjectDraft no longer renders fields.FIELD.primaryKey at all -- the introspected key now surfaces as the '// Remote primary key: ...' comment in the generated source, so #10676's acceptance criterion is asserted there, and the new pin ALSO asserts the definition stays free of the unauthorable key. (2) dialect had to be the driver's dialectName vocabulary (sqlite/postgres/mysql), not the spec's SQLDialectSchema enum ('postgresql'): the only in-tree consumer keys DIALECT_ALIASES on type-compat.ts's SqlDialect, so emitting 'postgresql' would have satisfied Object.keys() while leaving every per-dialect alias exactly as unreachable -- the omission wearing a fix's clothes. Single-site/dialect-independence assumption HELD, re-derived not inherited: one assignment site in introspectSchema downstream of the per-dialect helpers, and the return object is built once for every dialect. No per-dialect claim about primaryKeys CONTENT is made -- only SQLite was executed (no PG/MySQL listener reachable).",
      "tests": "All numbers at head 86ccc458c, each read from the command's own verdict line (redirect-then-capture, never a bare $? behind a pipe). ACCEPTANCE #10676: generateObjectDraft driven off a REAL SqlDriver.introspectSchema() against live in-memory SQLite -> draft.source carries '// Remote primary key: id'; composite key (declared order order_id,line_no vs column order sku,order_id,line_no) -> '// Remote primary key: order_id, line_no'; a table declaring no key -> no comment. ACCEPTANCE #10998: Object.keys() of a live result = [\"tables\",\"dialect\",\"introspectedAt\"], dialect=\"sqlite\", introspectedAt a real ISO instant inside the run window; refreshCatalog persists dialect 'sqlite' (was undefined). BEFORE (same probe, pre-fix driver dist): Object.keys()=[\"tables\"], id column {\"isPrimary\":true,...} with no primaryKey key, and NO remote-key comment in the draft. REVERSE VERIFICATION: the new service-lane pin was run against the PRE-FIX built driver and failed on exactly the two intended assertions ('expected undefined to be \\'// Remote primary key: id\\''), 2 failed / 560 passed; after rebuilding driver-sql it is green. REBUILD DISCIPLINE (these tests import @objectstack/driver-sql as a PACKAGE, so they resolve dist/, and the ledger in scripts/check-test-source-alias.mjs lists that pair): driver-sql was rebuilt after each edit and the change was confirmed ON DISK before reading any verdict -- grep of packages/drivers/driver-sql/dist/index.js: 'isPrimary' 0 occurrences (was 2), 'primaryKey: false' 1, 'introspectedAt' 2. SUITES: driver-sql 1795 passed | 72 skipped (112 files); objectql 4034 passed (228 files); service-datasource 563 passed (26 files) -- the deliberate landmine external-introspection-seam.test.ts reddened exactly once (1 failed / 561 passed) before its producer-spelling case was flipped. typecheck clean for all three packages. pnpm lint (repo-wide 'eslint . --no-inline-config') clean in 127s -- FULL run, no narrowing claimed or needed. GATES (re-derived with 'node scripts/pm/dispatch-gates.mjs' with no paths, against the real change set, after removing my two untracked measurement probes so they could not inflate it): check:driver-conformance, check:durability-log-level, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-ci-filter-parity.mjs, check-engine-split-ratio.mjs (EXIT 0, 97.5%, 'oldest visible commit 2026-05-21 (predates the window)' -- the shallow-clone refusal did NOT bite in this worktree, so no --shallow-since fetch was needed), check-plugin-teardown-shape.mjs, docs-audit/check-affected-docs.mjs, check:nul-bytes (plus my own control-char scan over the changed files: no matches). Convention-triggered by the new test files: check:query-options-erasure, check:type-check-coverage, check:type-check-debt (--re-measure: 33 ledger entries re-measured, none above its recorded number -- objectql's TEST_DEBT 354 is a ledgered entry my fixture edits touch, so this one was owed; it required building the whole workspace closure first, and the first attempt was killed by the container's 10-minute foreground cap at exit 143 and re-run to completion, never backgrounded), check:engine-double-contract, check:where-matcher. Changeset families: check-adr-0087-registration (1 declared-breaking changeset, disposition 'runtime-interface-only' MECHANICALLY VERIFIED against the four named symbols), check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:objectui-changeset. Changeset: .changeset/driver-emits-spec-introspection-shape.md, minor for driver-sql + objectql (breaking output shape, shipped minor per the repo's launch-window convention). MEASURED NEGATIVE RESULT, reported because it bounds the win: on the SQLite arm, supplying the dialect leaves suggestFieldTypeForSqlType's answers byte-identical for all 10 columns of a wide fixture (sqlite's alias map overlaps the base map), so the type-mapping payoff of dialect is on Postgres/MySQL and is NOT claimed here; what does change on SQLite is the persisted catalog's dialect.",
      "cross_lane_test_changes": "packages/services/service-datasource/src/__tests__/external-introspection-seam.test.ts -- ONLY the producer-spelling case: its title ('the driver speaks isPrimary/primaryKeys and never the spec spelling' -> 'the driver speaks the spec spelling: primaryKey/primaryKeys'), its two producer assertions (id.isPrimary===true / id.primaryKey===undefined -> id.primaryKey===true / id.isPrimary===undefined; the table.primaryKeys assertion is unchanged), and the file docblock's tense plus a note that the union read's isPrimary arm now has no in-tree producer. NOT touched: primaryKeyReader and its three-signal union read, its docblock, the two hand-built staged-disagreement cases, and the whole second describe block. One NEW file added in that package (nothing existing modified by it): src/__tests__/external-object-draft-real-introspection.test.ts, the pin the ruling requires.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11122: driver-sql introspectSchema still diverges from the spec contract on two keys the ruling does not name -- IntrospectedTable.indexes is declared REQUIRED and never emitted (the capability exists: introspectIndexes returns {name,columns,unique,primary}; wiring it costs one extra read per table and needs an onFailure policy, so it is filed not guessed), and IntrospectedColumn.defaultValue is declared string while the producer emits null (measured) and fixtures carry booleans; maxLength is emitted as the STRING \"255\" against a number declaration, same site.",
        "filed as #11123 (finding): service-datasource's primaryKeyReader docblock now describes a producer that no longer exists (its table still names SqlDriver as the isPrimary producer) and its #10997 note still says the composite-key truncation is unrepaired upstream (PR #11104 merged); the union read's isPrimary arm now has no in-tree producer left. Deliberately not edited -- that belt is the services lane's, on its own one-release clock."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions