Skip to content

Commit 135cb8d

Browse files
committed
no-mistakes(document): No documentation changes needed
1 parent ce24a71 commit 135cb8d

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* A regex CHECK on a text-family column must not read as drift on Postgres.
3+
*
4+
* Postgres stores `"key" ~ '^[a-z0-9][a-z0-9-]*$'` as `(((key)::text ~ '…'::text))`. The
5+
* oracle is the live engine: create the table from the DDL migrate emits for the authored
6+
* check, read the constraint back through the real introspector, and diff the authored
7+
* metadata against it. On a citext column the cast changes the operator (case-sensitive vs
8+
* citext's case-insensitive `~`), so the same pair must still read as drift.
9+
*
10+
* Skips gracefully when MIGRATE_TS_PG_URL is not set.
11+
*/
12+
13+
import { test, expect, beforeAll, afterAll, describe } from "bun:test";
14+
import { Pool } from "pg";
15+
import { Kysely, PostgresDialect, sql } from "kysely";
16+
import { MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata";
17+
import { buildExpectedSchema } from "../../src/expected-schema.js";
18+
import { introspectPostgres } from "../../src/introspect/postgres.js";
19+
import { diff } from "../../src/diff/index.js";
20+
21+
const PG_URL = process.env["MIGRATE_TS_PG_URL"];
22+
const PATTERN = "^[a-z0-9][a-z0-9-]*$";
23+
24+
const meta = JSON.stringify({
25+
"metadata.root": {
26+
package: "acme",
27+
children: [
28+
{
29+
"object.entity": {
30+
name: "Space",
31+
children: [
32+
{ "source.rdb": { "@table": "spaces" } },
33+
{ "field.long": { name: "id" } },
34+
{ "field.string": { name: "key", "@column": "key", children: [{ "validator.regex": { name: "re", "@pattern": PATTERN } }] } },
35+
{ "identity.primary": { name: "pk", "@fields": ["id"], "@generation": "increment" } },
36+
],
37+
},
38+
},
39+
],
40+
},
41+
});
42+
43+
describe("regex CHECK vs Postgres's stored rewrite (live PG)", () => {
44+
if (!PG_URL) {
45+
test.skip("skipped — MIGRATE_TS_PG_URL not set", () => {});
46+
return;
47+
}
48+
let pool: Pool;
49+
let kysely: Kysely<Record<string, unknown>>;
50+
51+
beforeAll(async () => {
52+
pool = new Pool({ connectionString: PG_URL });
53+
kysely = new Kysely<Record<string, unknown>>({ dialect: new PostgresDialect({ pool }) });
54+
await sql.raw("CREATE EXTENSION IF NOT EXISTS citext").execute(kysely);
55+
});
56+
afterAll(async () => {
57+
await sql.raw("DROP TABLE IF EXISTS spaces CASCADE").execute(kysely);
58+
await pool.end();
59+
});
60+
61+
const checkChanges = async (colType: string, authoredCheck = `"key" ~ '${PATTERN}'`) => {
62+
await sql.raw("DROP TABLE IF EXISTS spaces CASCADE").execute(kysely);
63+
// The constraint is created from the AUTHORED spelling; PG stores its own rendering.
64+
await sql
65+
.raw(`CREATE TABLE spaces (id bigserial PRIMARY KEY, "key" ${colType} NOT NULL,
66+
CONSTRAINT spaces_key_regex_chk CHECK (${authoredCheck}))`)
67+
.execute(kysely);
68+
const actual = await introspectPostgres(kysely);
69+
const stored = actual.tables.find((t) => t.name === "spaces")?.checks.find((c) => c.name === "spaces_key_regex_chk");
70+
const loaded = await new MetaDataLoader().load([new InMemoryStringSource(meta)]);
71+
expect(loaded.errors).toEqual([]);
72+
const d = await diff({ expected: buildExpectedSchema(loaded.root, { dialect: "postgres" }), actual, dialect: "postgres" });
73+
return { stored: stored?.expression, kinds: d.changes.filter((c) => c.kind.endsWith("-check")).map((c) => c.kind).sort() };
74+
};
75+
76+
test.each(["varchar(64)", "text", "char(8)"])("%s column: PG's rewrite is no drift", async (colType) => {
77+
const r = await checkChanges(colType);
78+
// The gate is not vacuous: PG really did rewrite the authored text.
79+
expect(r.stored).toContain("::text");
80+
expect(r.kinds).toEqual([]);
81+
});
82+
83+
test("citext column, uncast check: PG keeps citext's own operator and it is clean", async () => {
84+
const r = await checkChanges("citext");
85+
expect(r.stored).toContain("::citext");
86+
expect(r.kinds).toEqual([]);
87+
});
88+
89+
test("citext column: a check stored with an explicit ::text cast is a different (case-sensitive) check, so drift", async () => {
90+
const r = await checkChanges("citext", `("key")::text ~ '${PATTERN}'`);
91+
expect(r.kinds).toEqual(["add-check", "drop-check"]);
92+
});
93+
94+
test("varchar column: a changed pattern still reads as drift", async () => {
95+
const r = await checkChanges("varchar(64)", `"key" ~ '^[a-z]+$'`);
96+
expect(r.kinds).toEqual(["add-check", "drop-check"]);
97+
});
98+
});

0 commit comments

Comments
 (0)