Skip to content

Commit b6963f8

Browse files
committed
fix(planner): resolve four planner/verifier bugs blocking reconciliation
Squashed from fix/planner-issues branch (22 commits) for clean rebase onto main. - TML-2077: ALTER TYPE postcheck uses format_type() instead of column existence - TML-2087: indexOrConstraint populated on extra-object issues - TML-2088: FK detection in strict mode when contract has no FKs - TML-2091: extra_default detection and DROP DEFAULT operation - default_missing operation builder (additive SET DEFAULT) - default_mismatch operation builder (widening, known-failing: TML-2089) - buildExpectedFormatType with FORMAT_TYPE_DISPLAY, mixed-case UDT quoting (P1-2, P1-4) - constraintExistsCheck scoped to table with to_regclass() (P2-1) - autoincrement guard in buildDefaultOperation (TML-2107) - it.fails annotations for TML-2089, TML-2135 - Integration + unit tests
1 parent d773f53 commit b6963f8

11 files changed

Lines changed: 2306 additions & 27 deletions

File tree

packages/1-framework/1-core/migration/control-plane/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,8 @@ export interface SchemaIssue {
408408
| 'index_mismatch'
409409
| 'dependency_missing'
410410
| 'default_missing'
411-
| 'default_mismatch';
411+
| 'default_mismatch'
412+
| 'extra_default';
412413
readonly table?: string;
413414
readonly column?: string;
414415
readonly indexOrConstraint?: string;

packages/1-framework/1-core/shared/config/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ export interface SchemaIssue {
208208
| 'index_mismatch'
209209
| 'dependency_missing'
210210
| 'default_missing'
211-
| 'default_mismatch';
211+
| 'default_mismatch'
212+
| 'extra_default';
212213
readonly table?: string;
213214
readonly column?: string;
214215
readonly indexOrConstraint?: string;

packages/2-sql/3-tooling/family/src/core/schema-verify/verify-helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export function verifyForeignKeys(
236236
issues.push({
237237
kind: 'extra_foreign_key',
238238
table: tableName,
239+
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
239240
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
240241
});
241242
nodes.push({
@@ -338,6 +339,7 @@ export function verifyUniqueConstraints(
338339
issues.push({
339340
kind: 'extra_unique_constraint',
340341
table: tableName,
342+
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
341343
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
342344
});
343345
nodes.push({
@@ -445,6 +447,7 @@ export function verifyIndexes(
445447
issues.push({
446448
kind: 'extra_index',
447449
table: tableName,
450+
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
448451
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
449452
});
450453
nodes.push({

packages/2-sql/3-tooling/family/src/core/schema-verify/verify-sql-schema.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ function verifyTableChildren(options: {
348348
tableName,
349349
tablePath,
350350
issues,
351+
strict,
351352
typeMetadataRegistry,
352353
codecHooks,
353354
...ifDefined('normalizeDefault', normalizeDefault),
@@ -418,9 +419,11 @@ function verifyTableChildren(options: {
418419
});
419420
}
420421

421-
// Verify FK constraints only for FKs with constraint: true
422+
// Verify FK constraints only for FKs with constraint: true.
423+
// Always call when strict mode is on so extra-FK detection runs even if
424+
// the contract has no FKs for this table.
422425
const constraintFks = contractTable.foreignKeys.filter((fk) => fk.constraint === true);
423-
if (constraintFks.length > 0) {
426+
if (constraintFks.length > 0 || strict) {
424427
const fkStatuses = verifyForeignKeys(
425428
constraintFks,
426429
schemaTable.foreignKeys,
@@ -475,6 +478,7 @@ function collectContractColumnNodes(options: {
475478
tableName: string;
476479
tablePath: string;
477480
issues: SchemaIssue[];
481+
strict: boolean;
478482
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
479483
codecHooks: Map<string, CodecControlHooks>;
480484
normalizeDefault?: DefaultNormalizer;
@@ -486,6 +490,7 @@ function collectContractColumnNodes(options: {
486490
tableName,
487491
tablePath,
488492
issues,
493+
strict,
489494
typeMetadataRegistry,
490495
codecHooks,
491496
normalizeDefault,
@@ -526,6 +531,7 @@ function collectContractColumnNodes(options: {
526531
schemaColumn,
527532
columnPath,
528533
issues,
534+
strict,
529535
typeMetadataRegistry,
530536
codecHooks,
531537
...ifDefined('normalizeDefault', normalizeDefault),
@@ -576,6 +582,7 @@ function verifyColumn(options: {
576582
schemaColumn: SqlSchemaIR['tables'][string]['columns'][string];
577583
columnPath: string;
578584
issues: SchemaIssue[];
585+
strict: boolean;
579586
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
580587
codecHooks: Map<string, CodecControlHooks>;
581588
normalizeDefault?: DefaultNormalizer;
@@ -588,6 +595,7 @@ function verifyColumn(options: {
588595
schemaColumn,
589596
columnPath,
590597
issues,
598+
strict,
591599
codecHooks,
592600
normalizeDefault,
593601
normalizeNativeType,
@@ -728,6 +736,26 @@ function verifyColumn(options: {
728736
});
729737
columnStatus = 'fail';
730738
}
739+
} else if (strict && schemaColumn.default) {
740+
issues.push({
741+
kind: 'extra_default',
742+
table: tableName,
743+
column: columnName,
744+
actual: schemaColumn.default,
745+
message: `Column "${tableName}"."${columnName}" has default ${schemaColumn.default} in database but contract specifies no default`,
746+
});
747+
columnChildren.push({
748+
status: 'fail',
749+
kind: 'default',
750+
name: 'default',
751+
contractPath: `${columnPath}.default`,
752+
code: 'extra_default',
753+
message: `Extra default: ${schemaColumn.default}`,
754+
expected: undefined,
755+
actual: schemaColumn.default,
756+
children: [],
757+
});
758+
columnStatus = 'fail';
731759
}
732760

733761
// Single-pass aggregation for better performance

packages/2-sql/3-tooling/family/test/schema-verify.strict.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,56 @@ describe('verifySqlSchema - result structure', () => {
115115
totalNodes: expect.any(Number),
116116
});
117117
});
118+
119+
it('detects extra foreign keys when contract has no FKs for the table', () => {
120+
const contract = createTestContract({
121+
parent: createContractTable({
122+
id: { nativeType: 'int4', nullable: false },
123+
}),
124+
child: createContractTable({
125+
id: { nativeType: 'int4', nullable: false },
126+
parent_id: { nativeType: 'int4', nullable: false },
127+
}),
128+
});
129+
130+
const schema = createTestSchemaIR({
131+
parent: createSchemaTable('parent', {
132+
id: { nativeType: 'int4', nullable: false },
133+
}),
134+
child: createSchemaTable(
135+
'child',
136+
{
137+
id: { nativeType: 'int4', nullable: false },
138+
parent_id: { nativeType: 'int4', nullable: false },
139+
},
140+
{
141+
foreignKeys: [
142+
{
143+
columns: ['parent_id'],
144+
referencedTable: 'parent',
145+
referencedColumns: ['id'],
146+
name: 'child_parent_id_fkey',
147+
},
148+
],
149+
},
150+
),
151+
});
152+
153+
const result = verifySqlSchema({
154+
contract,
155+
schema,
156+
strict: true,
157+
typeMetadataRegistry: emptyTypeMetadataRegistry,
158+
frameworkComponents: [],
159+
});
160+
161+
expect(result.ok).toBe(false);
162+
expect(result.schema.issues).toContainEqual(
163+
expect.objectContaining({
164+
kind: 'extra_foreign_key',
165+
table: 'child',
166+
indexOrConstraint: 'child_parent_id_fkey',
167+
}),
168+
);
169+
});
118170
});

0 commit comments

Comments
 (0)