diff --git a/frontend/packages/schema/src/parser/tbls/index.test.ts b/frontend/packages/schema/src/parser/tbls/index.test.ts index 50e52228b5..ab35afdee4 100644 --- a/frontend/packages/schema/src/parser/tbls/index.test.ts +++ b/frontend/packages/schema/src/parser/tbls/index.test.ts @@ -618,6 +618,54 @@ describe(processor, () => { expect(value.tables['posts']?.constraints).toEqual(expected) }) + it('INTERLEAVE constraint', async () => { + const { value } = await processor( + JSON.stringify({ + name: 'testdb', + tables: [ + { + name: 'singers', + type: 'TABLE', + columns: [{ name: 'singer_id', type: 'int', nullable: false }], + }, + { + name: 'albums', + type: 'TABLE', + columns: [ + { name: 'singer_id', type: 'int', nullable: false }, + { name: 'album_id', type: 'int', nullable: false }, + ], + constraints: [ + { + type: 'INTERLEAVE', + name: 'albums_interleave', + def: 'INTERLEAVE IN PARENT singers ON DELETE CASCADE', + table: 'albums', + referenced_table: 'singers', + columns: ['singer_id', 'album_id'], + referenced_columns: ['singer_id'], + }, + ], + }, + ], + }), + ) + + const expected = { + albums_interleave: { + type: 'FOREIGN KEY', + name: 'albums_interleave', + columnNames: ['singer_id', 'album_id'], + targetTableName: 'singers', + targetColumnNames: ['singer_id'], + updateConstraint: 'NO_ACTION', + deleteConstraint: 'CASCADE', + }, + } + + expect(value.tables['albums']?.constraints).toEqual(expected) + }) + it('UNIQUE constraint', async () => { const { value } = await processor( JSON.stringify({ diff --git a/frontend/packages/schema/src/parser/tbls/parser.ts b/frontend/packages/schema/src/parser/tbls/parser.ts index ea9b03f53d..54200fa086 100644 --- a/frontend/packages/schema/src/parser/tbls/parser.ts +++ b/frontend/packages/schema/src/parser/tbls/parser.ts @@ -211,6 +211,59 @@ function processCheckConstraint(constraint: { return null } +/** + * Process an INTERLEAVE constraint as a FOREIGN KEY fallback. + * + * tbls can represent Cloud Spanner parent-child relationships as + * INTERLEAVE constraints, but Liam's internal schema model does not + * support INTERLEAVE as a first-class constraint type. + * + * To keep the fix scoped to the tbls parser and reuse the existing + * relationship generation pipeline, INTERLEAVE is normalized into a + * FOREIGN KEY-shaped constraint here. + * + * If we fully support Spanner INTERLEAVE in Liam, we should reconsider this approach + * and potentially add INTERLEAVE as a first-class constraint type in the schema model. + * + * @see https://github.com/liam-hq/liam/issues/2411#issuecomment-3082740297 + */ +function processInterleaveConstraint(constraint: { + type: string + name: string + columns?: string[] + def: string + referenced_table?: string + referenced_columns?: string[] +}): [string, Constraints[string]] | null { + if ( + constraint.type === 'INTERLEAVE' && + constraint.columns && + constraint.columns.length > 0 && + constraint.referenced_columns && + constraint.referenced_columns.length > 0 && + constraint.referenced_table + ) { + const { updateConstraint, deleteConstraint } = extractForeignKeyActions( + constraint.def, + ) + + return [ + constraint.name, + { + type: 'FOREIGN KEY', + name: constraint.name, + columnNames: constraint.columns, + targetTableName: constraint.referenced_table, + targetColumnNames: constraint.referenced_columns, + updateConstraint, + deleteConstraint, + }, + ] + } + + return null +} + /** * Process constraints for a table */ @@ -244,6 +297,8 @@ function processConstraints( result = processUniqueConstraint(constraint) } else if (constraint.type === 'CHECK') { result = processCheckConstraint(constraint) + } else if (constraint.type === 'INTERLEAVE') { + result = processInterleaveConstraint(constraint) } // Add constraint to the collection if valid