Skip to content

Commit 95437e7

Browse files
os-zhuangclaude
andauthored
fix(driver-sql): introspectSchema() emits the spec introspection contract — primaryKey, dialect, introspectedAt (#10676, #10998) (#11124)
* fix(driver-sql): emit the spec `primaryKey` column spelling, not `isPrimary` (#10676) `SqlDriver.introspectSchema` declared its own `IntrospectedColumn` and spelled primary-key membership `isPrimary`, while everything downstream of `plugin.ts` is typed against `packages/spec/src/contracts/schema-diff-service.ts`, which spells it `primaryKey`. The value crossed between the two contracts untyped, so `ExternalDatasourceService.generateObjectDraft` — which reads `col.primaryKey` — saw `undefined` on every real driver result and every federated object drafted from a remote table silently lost its remote primary key. Maintainer ruling, 2026-08-22 (live session, 「同意所有」 item 9 = 驱动侧对齐 spec 契约): `packages/spec` is the one contract and the driver aligns to it. - `driver-sql` and `objectql/src/util.ts` now DERIVE `IntrospectedColumn` from the spec import instead of re-declaring it, so a key added to the contract fails their `tsc` until the producer emits it. Two divergences are kept explicitly (`defaultValue` stays `unknown` — Knex reports `null`; `isUnique` / `maxLength` are SQL extras the spec does not declare). - The driver emits `primaryKey` and no longer emits `isPrimary`: one spelling, so no consumer can key off the wrong one again. - New pin `external-object-draft-real-introspection.test.ts` drives `generateObjectDraft` off a REAL `introspectSchema()` result, which is what the ruling requires and what the hand-written fixtures could never do. It fails on the pre-fix driver (measured: the `// Remote primary key:` line is absent) and asserts the key does not return as the unauthorable `fields.<f>.primaryKey`. - `external-introspection-seam.test.ts` asserted the producer's OLD spelling as a deliberate landmine; its producer-spelling case is flipped to the new direction. The service's three-signal union read is untouched. Fixes #10676 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y * fix(driver-sql): emit the spec `dialect` and required `introspectedAt` (#10998) `introspectSchema()` returned `{ tables }` and nothing else, while `packages/spec/src/contracts/schema-diff-service.ts` declares `dialect` and a REQUIRED `introspectedAt`. Measured live on in-memory SQLite before this change: `Object.keys()` was `["tables"]`. Two silent consequences — type mapping ran with `dialect: undefined` on the whole federation path, so every per-dialect alias in `suggestFieldTypeForSqlType` was unreachable there, and `refreshCatalog` persisted `dialect: undefined` into the `external_catalog` record Studio's schema browser and the boot gate read back. Same ruling as #10676 (2026-08-22, 「同意所有」 item 9): the driver aligns to the one contract. - `IntrospectedTable` / `IntrospectedSchema` now derive from the spec import in both `driver-sql` and `objectql/src/util.ts`, so `introspectedAt` being required is enforced by `tsc` rather than by remembering. `indexes` is `Omit`ted rather than emitted empty: this driver does not introspect indexes, and `[]` would claim a table has none — filed separately, not guessed. - `dialect` is `this.dialectName` (`sqlite` / `postgres` / `mysql` / `unknown`), NOT the raw Knex client and NOT the spec's `SQLDialectSchema` enum: the only in-tree consumer keys `DIALECT_ALIASES` on the `SqlDialect` vocabulary of `type-compat.ts`, which spells PostgreSQL `postgres`. Emitting `postgresql` would satisfy `Object.keys()` while leaving the aliases just as unreachable. - `introspectedAt` is stamped before the reads begin, so it never claims to cover a moment later than the first table actually read. Measured after: `Object.keys()` is `["tables","dialect","introspectedAt"]`; the persisted catalog records `dialect: 'sqlite'`. On the SQLite arm the suggested field types are byte-identical before and after (its alias map overlaps the base map); the per-dialect payoff is on Postgres/MySQL, which are unreachable from this container and are therefore not claimed. Fixes #10998 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y * chore(changeset): driver-sql emits the spec introspection contract (#10676, #10998) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d806081 commit 95437e7

8 files changed

Lines changed: 515 additions & 68 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/driver-sql": minor
3+
"@objectstack/objectql": minor
4+
---
5+
6+
fix(driver-sql): `introspectSchema()` emits the spec introspection contract — `primaryKey`, `dialect`, `introspectedAt` (#10676, #10998)
7+
8+
**BREAKING** change to the value `SqlDriver.introspectSchema()` returns, shipped
9+
as `minor` under the repo's launch-window convention for breaking changes.
10+
11+
`packages/spec/src/contracts/schema-diff-service.ts` declares one introspection
12+
contract. The driver declared a second one beside it and, separately, so did
13+
`packages/objectql/src/util.ts`. The three agreed on the idea and disagreed on
14+
the vocabulary: the driver spelled a column's primary-key membership
15+
`isPrimary`, the spec spells it `primaryKey`; the spec declares `dialect` and a
16+
REQUIRED `introspectedAt` that the driver's schema type never mentioned and
17+
`introspectSchema()` therefore never emitted. Nothing was type-unsound — each
18+
side compiled against its own declaration and the value crossed between them
19+
with no compiler in the middle.
20+
21+
Measured on a live in-memory SQLite database before this change: the id column
22+
of a `primary key (id)` table came back carrying `isPrimary: true` with no
23+
`primaryKey` key at all, and `Object.keys()` of the schema was `["tables"]`.
24+
Two consequences, both silent:
25+
26+
- `ExternalDatasourceService.generateObjectDraft` reads `col.primaryKey`, so
27+
every federated object drafted from a real remote table lost the remote
28+
primary key — the addressing key for the federated table, dropped by the
29+
codegen meant to produce it (#10676).
30+
- type mapping ran with `dialect: undefined` across the whole federation path,
31+
making every per-dialect alias in `suggestFieldTypeForSqlType` unreachable
32+
there, and `refreshCatalog` persisted `dialect: undefined` into the
33+
`external_catalog` record Studio's schema browser and the boot gate read
34+
back (#10998).
35+
36+
Maintainer ruling, 2026-08-22 (live session, 「同意所有」 item 9 =
37+
驱动侧对齐 spec 契约): `packages/spec` is the one contract and the driver
38+
aligns to it.
39+
40+
What the driver now returns: every column carries the boolean `primaryKey`, the
41+
schema carries `dialect` and `introspectedAt`, and the retired `isPrimary`
42+
member is gone rather than emitted alongside — one spelling, so no consumer can
43+
key off the wrong one again. `dialect` is the driver's canonical dialect name
44+
(`sqlite`, `postgres`, `mysql`, `unknown`), which is the vocabulary the only
45+
in-tree consumer keys its alias tables on; `introspectedAt` is an ISO 8601
46+
instant stamped before the reads begin.
47+
48+
`IntrospectedColumn`, `IntrospectedTable` and `IntrospectedSchema` in both
49+
`@objectstack/driver-sql` and `@objectstack/objectql` are now derived from the
50+
spec contract instead of re-declared, so a key added there fails their `tsc`
51+
until the producer emits it. Two divergences are kept explicitly: `defaultValue`
52+
stays `unknown` at the SQL layer because Knex reports `null`, and `indexes` is
53+
omitted rather than emitted empty because this driver does not introspect
54+
indexes and an empty array would tell a schema differ that a table has none.
55+
56+
TypeScript consumers of the removed member are told by the compiler, precisely
57+
and at every site: `Property 'isPrimary' does not exist on type
58+
'IntrospectedColumn'`.
59+
60+
<!-- adr-0087: not-required (runtime-interface-only packages/drivers/driver-sql/src/sql-driver.ts#IntrospectedColumn, packages/drivers/driver-sql/src/sql-driver.ts#IntrospectedSchema, packages/objectql/src/util.ts#IntrospectedColumn, packages/objectql/src/util.ts#IntrospectedSchema) these are published runtime TypeScript interfaces describing a driver's introspection RESULT — not a metadata surface. There is no Zod schema, no `packages/spec` declaration of the old spelling, and no stored representation of it, so `objectstack migrate meta` has nothing to rewrite; the channel that reaches every affected consumer is the compiler. -->

packages/drivers/driver-sql/src/sql-driver-composite-primary-key-introspection.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
* the first member of a composite key and silently dropped the rest.
1212
*
1313
* Both output signals were wrong together and for the same reason:
14-
* `introspectSchema` derives `col.isPrimary` FROM `primaryKeys`
15-
* (`if (primaryKeys.includes(col.name)) col.isPrimary = true`), so a consumer
14+
* `introspectSchema` derives `col.primaryKey` FROM `primaryKeys`
15+
* (`if (primaryKeys.includes(col.name)) col.primaryKey = true`), so a consumer
1616
* could not recover the missing member by cross-checking the two. Both are
1717
* asserted here.
1818
*
@@ -80,7 +80,7 @@ describe('SqlDriver composite primary-key introspection (SQLite)', () => {
8080
expect(pkByName).toEqual({ order_id: 1, line_no: 2, sku: 0 });
8181
});
8282

83-
it('reports every member of a composite key, and derives isPrimary for all of them', async () => {
83+
it('reports every member of a composite key, and derives primaryKey for all of them', async () => {
8484
await knexInstance.schema.createTable('order_lines', (t: any) => {
8585
t.string('order_id').notNullable();
8686
t.integer('line_no').notNullable();
@@ -95,8 +95,8 @@ describe('SqlDriver composite primary-key introspection (SQLite)', () => {
9595
expect(table.primaryKeys).toEqual(['order_id', 'line_no']);
9696

9797
// Signal 2: the per-column flag, derived FROM signal 1 — repaired with it.
98-
const isPrimaryByName = Object.fromEntries(table.columns.map((c) => [c.name, c.isPrimary === true]));
99-
expect(isPrimaryByName).toEqual({ order_id: true, line_no: true, sku: false });
98+
const primaryKeyByName = Object.fromEntries(table.columns.map((c) => [c.name, c.primaryKey === true]));
99+
expect(primaryKeyByName).toEqual({ order_id: true, line_no: true, sku: false });
100100
});
101101

102102
it('orders primaryKeys by pk ordinal, not by column position', async () => {
@@ -164,7 +164,7 @@ describe('SqlDriver composite primary-key introspection (SQLite)', () => {
164164
expect(schema.tables['widgets'].primaryKeys).toEqual(['id']);
165165
expect(schema.tables['audit_lines'].primaryKeys).toEqual([]);
166166

167-
const auditPrimary = schema.tables['audit_lines'].columns.map((c) => c.isPrimary === true);
167+
const auditPrimary = schema.tables['audit_lines'].columns.map((c) => c.primaryKey === true);
168168
expect(auditPrimary).toEqual([false, false]);
169169
});
170170
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Pin: `SqlDriver.introspectSchema()` emits the `packages/spec` introspection
5+
* contract — the shape every consumer downstream of `plugin.ts` is typed
6+
* against — and not a second vocabulary of its own.
7+
*
8+
* The defect this closes was invisible to types on both sides. The driver
9+
* declared its own `IntrospectedColumn` spelling key membership `isPrimary?`;
10+
* `packages/spec/src/contracts/schema-diff-service.ts` declares `primaryKey`,
11+
* `dialect` and a REQUIRED `introspectedAt`. Each side compiled against its
12+
* own declaration, the value crossed between them untyped, and the consumer
13+
* read keys no driver ever set. Maintainer ruling, 2026-08-22 (live session,
14+
* 「同意所有」 item 9 = 驱动侧对齐 spec 契约): the driver aligns to the spec.
15+
*
16+
* Asserted on the BYTES of a live introspection rather than on a type, because
17+
* a type is exactly what failed to catch this: a hand-written fixture in
18+
* either spelling is blind to the seam. Only better-sqlite3 is executed here —
19+
* every assertion below is on a value built at a single dialect-independent
20+
* site in `introspectSchema`, downstream of the per-dialect helpers, so the
21+
* SHAPE cannot vary by dialect even though the per-dialect CONTENT is not
22+
* measured here.
23+
*/
24+
25+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
26+
import { SqlDriver } from '../src/index.js';
27+
28+
describe('SqlDriver.introspectSchema emits the spec introspection contract', () => {
29+
let driver: SqlDriver;
30+
let knexInstance: any;
31+
32+
beforeEach(async () => {
33+
driver = new SqlDriver({
34+
client: 'better-sqlite3',
35+
connection: { filename: ':memory:' },
36+
useNullAsDefault: true,
37+
});
38+
knexInstance = (driver as unknown as { knex: any }).knex;
39+
await knexInstance.schema.createTable('customers', (t: any) => {
40+
t.string('id').primary();
41+
t.string('name');
42+
t.integer('age');
43+
});
44+
});
45+
46+
afterEach(async () => {
47+
await knexInstance?.destroy();
48+
});
49+
50+
it('spells column key membership `primaryKey`, the spec spelling', async () => {
51+
const schema = await driver.introspectSchema();
52+
const byName = Object.fromEntries(
53+
schema.tables['customers'].columns.map((c) => [c.name, c]),
54+
);
55+
56+
expect(byName.id.primaryKey).toBe(true);
57+
// Negative half: an implementation that stamped the key onto the first
58+
// column, or onto every column, would satisfy the line above alone.
59+
expect(byName.name.primaryKey).toBe(false);
60+
expect(byName.age.primaryKey).toBe(false);
61+
});
62+
63+
it('emits `dialect` and the required `introspectedAt`', async () => {
64+
const before = Date.now();
65+
const schema = await driver.introspectSchema();
66+
67+
// #10998's acceptance criterion, spelled exactly as it was written: the
68+
// producer returned `{ tables }` alone while the contract declares three
69+
// keys, so consumers read two nobody set — type mapping ran with no
70+
// dialect on the whole federation path, and `refreshCatalog` persisted
71+
// `dialect: undefined` into the record Studio and the boot gate read back.
72+
expect(Object.keys(schema)).toEqual(
73+
expect.arrayContaining(['tables', 'dialect', 'introspectedAt']),
74+
);
75+
76+
// The dialect TOKEN, not merely the key's presence. The consumer is
77+
// `suggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect)`,
78+
// whose vocabulary spells these `sqlite` / `postgres` / `mysql`; a token
79+
// outside it (`better-sqlite3`, or the spec enum's `postgresql`) would
80+
// leave every per-dialect alias unreachable with the key still present.
81+
expect(schema.dialect).toBe('sqlite');
82+
83+
// Required in the contract, so it is emitted unconditionally — and it is a
84+
// real ISO 8601 instant, not a placeholder a consumer would have to guard.
85+
expect(typeof schema.introspectedAt).toBe('string');
86+
expect(new Date(schema.introspectedAt).toISOString()).toBe(schema.introspectedAt);
87+
const at = Date.parse(schema.introspectedAt);
88+
expect(at).toBeGreaterThanOrEqual(before - 1000);
89+
expect(at).toBeLessThanOrEqual(Date.now() + 1000);
90+
});
91+
92+
it('no longer emits the retired `isPrimary` spelling', async () => {
93+
const schema = await driver.introspectSchema();
94+
const id = schema.tables['customers'].columns.find((c) => c.name === 'id')!;
95+
96+
// `in`, not a truthiness check: the failure this closes was a consumer
97+
// reading a key that was ABSENT, so absence is what has to be pinned. Two
98+
// spellings emitted side by side would keep the second contract alive in
99+
// the bytes even with both values agreeing today.
100+
expect('isPrimary' in id).toBe(false);
101+
expect(Object.keys(id)).toEqual(
102+
expect.arrayContaining(['name', 'type', 'nullable', 'primaryKey']),
103+
);
104+
});
105+
});

packages/drivers/driver-sql/src/sql-driver.ts

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
*/
99

1010
import type { DriverOptions, FilterCondition, SchemaMode } from '@objectstack/spec/data';
11+
// The ONE introspection contract (ADR-0015 / `ISchemaDiffService`). This
12+
// driver's introspection types are DERIVED from these rather than
13+
// re-declared next to them — see the `Introspection Types` region below.
14+
import type {
15+
IntrospectedColumn as SpecIntrospectedColumn,
16+
IntrospectedSchema as SpecIntrospectedSchema,
17+
IntrospectedTable as SpecIntrospectedTable,
18+
} from '@objectstack/spec/contracts';
1119
import { parseAutonumberFormat, renderAutonumber, resolveAutonumberFormat, readAutonumberCounter, missingFieldValues, isTenancyDisabled, type AutonumberToken } from '@objectstack/spec/data';
1220
// The DECLARED aggregate vocabulary (#5907). Read from the spec so this driver's
1321
// "the protocol has no such function" refusal cannot drift from what
@@ -3645,31 +3653,76 @@ function nullSafeNegationOperand(node: Record<string, unknown>): Record<string,
36453653

36463654
// ── Introspection Types ──────────────────────────────────────────────────────
36473655

3648-
export interface IntrospectedColumn {
3649-
name: string;
3650-
type: string;
3651-
nullable: boolean;
3656+
/**
3657+
* These are DERIVED from `packages/spec/src/contracts/schema-diff-service.ts`,
3658+
* never re-declared beside it.
3659+
*
3660+
* They used to be a second, independent declaration that happened to describe
3661+
* the same idea in a different vocabulary: this file spelled a column's key
3662+
* membership `isPrimary?`, the spec spells it `primaryKey`; the spec also
3663+
* declares `dialect` and a REQUIRED `introspectedAt` that this file's schema
3664+
* type did not mention and `introspectSchema` therefore never emitted. `plugin.ts` hands
3665+
* this driver's result straight to `ExternalDatasourceService`, which is typed
3666+
* against the spec — so the consumer read a key no driver ever set and every
3667+
* federated object drafted from a remote table silently lost its primary key.
3668+
* Nothing was type-unsound; the two contracts simply never met a compiler.
3669+
*
3670+
* Maintainer ruling, 2026-08-22 (live session, 「同意所有」 item 9 =
3671+
* 驱动侧对齐 spec 契约): `packages/spec` is the one contract and the DRIVER
3672+
* aligns to it. Deriving rather than copying is what makes that mechanical —
3673+
* a key added to the spec contract now fails this file's `tsc` until the
3674+
* driver emits it, which is exactly the failure that was missing.
3675+
*
3676+
* Two divergences remain, deliberately, and neither is a second spelling of
3677+
* something the spec declares:
3678+
*
3679+
* - the SQL layer carries EXTRA per-column facts (`isUnique`, `maxLength`)
3680+
* and extra per-table facts (`foreignKeys`, `primaryKeys`) that the spec's
3681+
* diff-facing contract does not declare;
3682+
* - `defaultValue` is `unknown` here rather than the spec's `string`, because
3683+
* that is what Knex's `columnInfo()` actually returns (measured on live
3684+
* SQLite: `null`). Narrowing the declaration without normalising the value
3685+
* would move the lie rather than remove it.
3686+
*/
3687+
export interface IntrospectedColumn extends Omit<SpecIntrospectedColumn, 'defaultValue'> {
3688+
/** Raw driver-reported default. See the note above on why this is not `string`. */
36523689
defaultValue?: unknown;
3653-
isPrimary?: boolean;
3690+
/** SQL-introspection extra: the column carries a UNIQUE constraint. */
36543691
isUnique?: boolean;
3692+
/** SQL-introspection extra: declared maximum length for string types. */
36553693
maxLength?: number;
36563694
}
36573695

3696+
/** No spec counterpart — foreign keys are a SQL-introspection extra. */
36583697
export interface IntrospectedForeignKey {
36593698
columnName: string;
36603699
referencedTable: string;
36613700
referencedColumn: string;
36623701
constraintName?: string;
36633702
}
36643703

3665-
export interface IntrospectedTable {
3666-
name: string;
3704+
/**
3705+
* `indexes` is `Omit`ted from the spec table rather than emitted empty: this
3706+
* driver does not introspect indexes, and `indexes: []` would tell a schema
3707+
* differ that a table HAS none when it merely was not asked — a worse answer
3708+
* than an absent key. Emitting them for real is per-dialect work over arms
3709+
* this container cannot execute, so it is filed rather than guessed.
3710+
*/
3711+
export interface IntrospectedTable extends Omit<SpecIntrospectedTable, 'columns' | 'indexes'> {
36673712
columns: IntrospectedColumn[];
3713+
/** SQL-introspection extra: outbound foreign keys. */
36683714
foreignKeys: IntrospectedForeignKey[];
3715+
/** SQL-introspection extra: the table's primary-key columns, in key order. */
36693716
primaryKeys: string[];
36703717
}
36713718

3672-
export interface IntrospectedSchema {
3719+
/**
3720+
* `dialect` and `introspectedAt` are inherited from the spec contract, where
3721+
* `introspectedAt` is REQUIRED — so `tsc` now refuses an `introspectSchema`
3722+
* that omits them, which is the check that was missing while this type
3723+
* declared `{ tables }` alone.
3724+
*/
3725+
export interface IntrospectedSchema extends Omit<SpecIntrospectedSchema, 'tables'> {
36733726
tables: Record<string, IntrospectedTable>;
36743727
}
36753728

@@ -9798,6 +9851,10 @@ export class SqlDriver implements IDataDriver {
97989851

97999852
async introspectSchema(): Promise<IntrospectedSchema> {
98009853
const tables: Record<string, IntrospectedTable> = {};
9854+
// Stamped BEFORE the reads, not after: a consumer asking "has the remote
9855+
// changed since this snapshot?" must not be told the snapshot covers a
9856+
// moment later than the first table it actually read.
9857+
const introspectedAt = new Date().toISOString();
98019858
let tableNames: string[] = [];
98029859

98039860
if (this.isPostgres) {
@@ -9837,14 +9894,22 @@ export class SqlDriver implements IDataDriver {
98379894
const uniqueConstraints = await this.introspectUniqueConstraints(tableName);
98389895

98399896
for (const col of columns) {
9840-
if (primaryKeys.includes(col.name)) col.isPrimary = true;
9897+
if (primaryKeys.includes(col.name)) col.primaryKey = true;
98419898
if (uniqueConstraints.includes(col.name)) col.isUnique = true;
98429899
}
98439900

98449901
tables[tableName] = { name: tableName, columns, foreignKeys, primaryKeys };
98459902
}
98469903

9847-
return { tables };
9904+
// `dialectName` — not the raw Knex client, and not the spec's
9905+
// `SQLDialectSchema` enum. The only in-tree consumer of this key is
9906+
// `suggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect)`,
9907+
// whose `SqlDialect` vocabulary (`packages/spec/src/data/type-compat.ts`)
9908+
// spells PostgreSQL `postgres`, exactly as `dialectName` does. Emitting
9909+
// the enum's `postgresql` instead would put the key in `Object.keys()`
9910+
// while leaving every per-dialect type alias unreachable — the omission
9911+
// this repairs, wearing a fix's clothes.
9912+
return { tables, dialect: this.dialectName, introspectedAt };
98489913
}
98499914

98509915
// ===================================
@@ -12548,7 +12613,9 @@ export class SqlDriver implements IDataDriver {
1254812613
type,
1254912614
nullable: info.nullable !== false,
1255012615
defaultValue: info.defaultValue,
12551-
isPrimary: false,
12616+
// The spec contract's spelling, and the only one this driver emits.
12617+
// `introspectSchema` flips it from the table's key list below.
12618+
primaryKey: false,
1255212619
isUnique: false,
1255312620
maxLength,
1255412621
});
@@ -12695,7 +12762,7 @@ export class SqlDriver implements IDataDriver {
1269512762
// of the key", `1` for the first key column, `2` for the second, and so
1269612763
// on. Filtering on `pk === 1` therefore kept only the first member of a
1269712764
// composite key and silently dropped the rest, and because
12698-
// `introspectSchema` derives `col.isPrimary` FROM this list, both output
12765+
// `introspectSchema` derives `col.primaryKey` FROM this list, both output
1269912766
// signals were wrong together.
1270012767
//
1270112768
// Ordering by the ordinal (rather than taking `table_info`'s row order,

0 commit comments

Comments
 (0)