Skip to content

Commit 87eb3c2

Browse files
committed
test(migrate-ts): pin that a stored length((col)::text) on char(n) is drift
Covers the non-regex bpchar case on real Postgres and offline.
1 parent 0061305 commit 87eb3c2

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

‎server/typescript/packages/migrate-ts/test/check-evolution/diff-offline.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ describe("diffTableChecks — regex check on a text-family column", () => {
7777
const r = await diff({ expected: withKey(undefined, [generated]), actual: withKey("bpchar", [uncast]), dialect: "postgres" });
7878
expect(r.changes.filter((c) => c.kind.endsWith("-check"))).toEqual([]);
7979
});
80+
test("bpchar column: a stored length((col)::text) is drift against the uncast length()", async () => {
81+
const expected = CHK("spaces_key_length_chk", `length("key") >= 2`);
82+
const cast = CHK("spaces_key_length_chk", `((length((key)::text) >= 2))`);
83+
const uncast = CHK("spaces_key_length_chk", `(length(key) >= 2)`);
84+
const kinds = async (udt: string, actual: CheckDescriptor) =>
85+
(await diff({ expected: withKey(undefined, [expected]), actual: withKey(udt, [actual]), dialect: "postgres" }))
86+
.changes.filter((c) => c.kind.endsWith("-check")).map((c) => c.kind).sort();
87+
expect(await kinds("bpchar", cast)).toEqual(["add-check", "drop-check"]);
88+
expect(await kinds("bpchar", uncast)).toEqual([]);
89+
expect(await kinds("varchar", cast)).toEqual([]);
90+
});
8091
test("citext column: cast vs uncast is drift", async () => {
8192
expect((await checkChanges("citext")).map((c) => c.kind).sort()).toEqual(["add-check", "drop-check"]);
8293
});

‎server/typescript/packages/migrate-ts/test/integration/pg-regex-check-cast.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ const meta = JSON.stringify({
4040
},
4141
});
4242

43+
const lengthMeta = JSON.stringify({
44+
"metadata.root": {
45+
package: "acme",
46+
children: [
47+
{
48+
"object.entity": {
49+
name: "Space",
50+
children: [
51+
{ "source.rdb": { "@table": "spaces" } },
52+
{ "field.long": { name: "id" } },
53+
{ "field.string": { name: "key", "@column": "key", children: [{ "validator.length": { name: "len", "@min": 2 } }] } },
54+
{ "identity.primary": { name: "pk", "@fields": ["id"], "@generation": "increment" } },
55+
],
56+
},
57+
},
58+
],
59+
},
60+
});
61+
4362
describe("regex CHECK vs Postgres's stored rewrite (live PG)", () => {
4463
if (!PG_URL) {
4564
test.skip("skipped — MIGRATE_TS_PG_URL not set", () => {});
@@ -105,6 +124,38 @@ describe("regex CHECK vs Postgres's stored rewrite (live PG)", () => {
105124
expect(r.kinds).toEqual(["add-check", "drop-check"]);
106125
});
107126

127+
// The same holds off the regex operator: `length(bpchar)` is its own function, so a stored
128+
// `length((key)::text)` is a different constraint from the uncast `length("key")`, while a
129+
// varchar column's cast is Postgres's own rewrite.
130+
const lengthChanges = async (colType: string, authoredCheck: string) => {
131+
await sql.raw("DROP TABLE IF EXISTS spaces CASCADE").execute(kysely);
132+
await sql
133+
.raw(`CREATE TABLE spaces (id bigserial PRIMARY KEY, "key" ${colType} NOT NULL,
134+
CONSTRAINT spaces_key_length_chk CHECK (${authoredCheck}))`)
135+
.execute(kysely);
136+
const actual = await introspectPostgres(kysely);
137+
const stored = actual.tables.find((t) => t.name === "spaces")?.checks.find((c) => c.name === "spaces_key_length_chk");
138+
const loaded = await new MetaDataLoader().load([new InMemoryStringSource(lengthMeta)]);
139+
expect(loaded.errors).toEqual([]);
140+
const d = await diff({ expected: buildExpectedSchema(loaded.root, { dialect: "postgres" }), actual, dialect: "postgres" });
141+
return { stored: stored?.expression, kinds: d.changes.filter((c) => c.kind.endsWith("-check")).map((c) => c.kind).sort() };
142+
};
143+
test("char(n) column: Postgres stores length() uncast and it is clean", async () => {
144+
const r = await lengthChanges("char(8)", `length("key") >= 2`);
145+
expect(r.stored).not.toContain("::text");
146+
expect(r.kinds).toEqual([]);
147+
});
148+
test("char(n) column: a stored length((col)::text) is drift against the uncast length()", async () => {
149+
const r = await lengthChanges("char(8)", `length(("key")::text) >= 2`);
150+
expect(r.stored).toContain("(key)::text");
151+
expect(r.kinds).toEqual(["add-check", "drop-check"]);
152+
});
153+
test("varchar column: Postgres's length((col)::text) rewrite is no drift", async () => {
154+
const r = await lengthChanges("varchar(8)", `length("key") >= 2`);
155+
expect(r.stored).toContain("(key)::text");
156+
expect(r.kinds).toEqual([]);
157+
});
158+
108159
test("varchar column: a changed pattern still reads as drift", async () => {
109160
const r = await checkChanges("varchar(64)", `"key" ~ '^[a-z]+$'`);
110161
expect(r.kinds).toEqual(["add-check", "drop-check"]);

0 commit comments

Comments
 (0)