Skip to content

Commit 95f4968

Browse files
SevInfclaude
andcommitted
fix(scalar-arrays): parse multi-word element-type casts in array_position CHECK recognizer
The Shape 3 array_position element-non-null recognizer used a cast fragment `(?:::[^\s)]+)?` that stopped at the first whitespace, so multi-word Postgres type names (e.g. `timestamp with time zone` for DateTime[]) failed to match and the column dropped out of drift management, breaking schema verification after migrate. Anchor the cast with a non-greedy `(?:::.+?)?` up to the closing `) IS NULL`; the cast is discarded and the predicate re-canonicalized, so the wider match is safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
1 parent 3683911 commit 95f4968

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,10 +1506,13 @@ export function parseCheckConstraintDef(constraintdef: string): ParsedCheckConst
15061506

15071507
// Shape 3: array_position(col, NULL) IS NULL — the scalar-array element-non-null
15081508
// guard. Postgres stores it with an element-type cast on NULL and drops quotes
1509-
// on simple identifiers, so accept an optional `::type` cast and both column
1510-
// forms, then re-canonicalize through the shared projection helper.
1509+
// on simple identifiers, so accept an optional `::type` cast (including
1510+
// multi-word type names like `timestamp with time zone`) and both column
1511+
// forms, then re-canonicalize through the shared projection helper. The cast
1512+
// is discarded, so a non-greedy `.+?` anchored by the closing `) IS NULL`
1513+
// captures the whole type name without over-running the predicate.
15111514
const arrayPositionMatch = inner.match(
1512-
/^array_position\s*\(\s*(?:"((?:[^"]|"")*)"|(\w+))\s*,\s*NULL(?:::[^\s)]+)?\s*\)\s+IS\s+NULL$/i,
1515+
/^array_position\s*\(\s*(?:"((?:[^"]|"")*)"|(\w+))\s*,\s*NULL(?:::.+?)?\s*\)\s+IS\s+NULL$/i,
15131516
);
15141517
if (arrayPositionMatch) {
15151518
const column =

packages/3-targets/6-adapters/postgres/test/control-adapter.check-constraints.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ describe('parseCheckConstraintDef', () => {
120120
});
121121
});
122122

123+
it('re-canonicalizes with a multi-word element-type cast (timestamptz[])', () => {
124+
const result = parseCheckConstraintDef(
125+
'CHECK ((array_position(dates, NULL::timestamp with time zone) IS NULL))',
126+
);
127+
expect(result).toEqual({
128+
kind: 'expression',
129+
expression: 'array_position("dates", NULL) IS NULL',
130+
});
131+
});
132+
133+
it('re-canonicalizes with a multi-word element-type cast (double precision[])', () => {
134+
const result = parseCheckConstraintDef(
135+
'CHECK ((array_position(measurements, NULL::double precision) IS NULL))',
136+
);
137+
expect(result).toEqual({
138+
kind: 'expression',
139+
expression: 'array_position("measurements", NULL) IS NULL',
140+
});
141+
});
142+
123143
it('preserves quoting for a mixed-case element-non-null column', () => {
124144
const result = parseCheckConstraintDef(
125145
'CHECK ((array_position("myTags", NULL::text) IS NULL))',

packages/3-targets/6-adapters/postgres/test/migrations/native-array-columns.integration.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ function buildArrayContract(): Contract<SqlStorage> {
3939
tags: { nativeType: 'text', codecId: 'pg/text@1', nullable: false, many: true },
4040
labels: { nativeType: 'text', codecId: 'pg/text@1', nullable: true, many: true },
4141
scores: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false, many: true },
42+
dates: {
43+
nativeType: 'timestamptz',
44+
codecId: 'pg/timestamptz@1',
45+
nullable: true,
46+
many: true,
47+
},
4248
tagsWithDefault: {
4349
nativeType: 'text',
4450
codecId: 'pg/text@1',
@@ -320,6 +326,7 @@ describe.sequential('native array columns DDL', () => {
320326
const checkNames = checkRows.rows.map((r) => r.constraint_name).sort();
321327
expect(checkNames).toEqual(
322328
[
329+
'ArrayTest_dates_elem_not_null',
323330
'ArrayTest_labels_elem_not_null',
324331
'ArrayTest_scores_elem_not_null',
325332
'ArrayTest_tags_elem_not_null',

0 commit comments

Comments
 (0)