Skip to content

Commit ebd71fd

Browse files
dmealingclaude
andcommitted
feat(migrate-ts): derive regex CHECK from validator.regex @pattern (postgres)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e1590b9 commit ebd71fd

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

server/typescript/packages/migrate-ts/src/expected-schema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,16 @@ function validatorCheck(
312312
if (v.min === undefined) return null;
313313
return { name: `${tableName}_${col}_length_chk`, expression: `length(${col}) >= ${v.min}` };
314314
}
315+
case VALIDATOR_SUBTYPE_REGEX: {
316+
// Postgres-only: SQLite has no native regex operator.
317+
if (dialect === "sqlite" || dialect === "d1") return null;
318+
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
319+
if (typeof pattern !== "string" || pattern.length === 0) return null;
320+
return {
321+
name: `${tableName}_${col}_regex_chk`,
322+
expression: `${col} ~ '${pattern.replace(/'/g, "''")}'`,
323+
};
324+
}
315325
default:
316326
return null;
317327
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// test/validator-check/regex.test.ts
2+
import { describe, test, expect } from "bun:test";
3+
import { MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata";
4+
import type { MetaData } from "@metaobjectsdev/metadata";
5+
import { buildExpectedSchema } from "../../src/expected-schema.js";
6+
import type { Dialect } from "../../src/types.js";
7+
8+
async function load(json: string): Promise<MetaData> {
9+
return (await new MetaDataLoader().load([new InMemoryStringSource(json)])).root;
10+
}
11+
const ENTITY = (fieldChildren: string) => JSON.stringify({
12+
"metadata.root": { children: [{
13+
"object.entity": { name: "User", children: [
14+
{ "field.long": { name: "id" } },
15+
...JSON.parse(fieldChildren),
16+
{ "source.rdb": { name: "src", "@table": "users" } },
17+
{ "identity.primary": { name: "pk", "@fields": ["id"], "@generation": "increment" } },
18+
] },
19+
}] },
20+
});
21+
function checks(root: MetaData, dialect: Dialect) {
22+
return buildExpectedSchema(root, { dialect }).tables[0]!.checks;
23+
}
24+
25+
describe("validator.regex → CHECK", () => {
26+
test("postgres: @pattern → ~ check", async () => {
27+
const root = await load(ENTITY(`[{"field.string":{"name":"slug","children":[
28+
{"validator.regex":{"name":"re","@pattern":"^[a-z]+$"}}]}}]`));
29+
const c = checks(root, "postgres").find((x) => x.name === "users_slug_regex_chk");
30+
expect(c?.expression).toBe("slug ~ '^[a-z]+$'");
31+
});
32+
test("single-quote in pattern is escaped", async () => {
33+
const root = await load(ENTITY(`[{"field.string":{"name":"q","children":[
34+
{"validator.regex":{"name":"re","@pattern":"o'brien"}}]}}]`));
35+
const c = checks(root, "postgres").find((x) => x.name === "users_q_regex_chk");
36+
expect(c?.expression).toBe("q ~ 'o''brien'");
37+
});
38+
test("sqlite: regex emits NO check (no native regex)", async () => {
39+
const root = await load(ENTITY(`[{"field.string":{"name":"slug","children":[
40+
{"validator.regex":{"name":"re","@pattern":"^[a-z]+$"}}]}}]`));
41+
expect(checks(root, "sqlite").some((x) => x.name.includes("_regex_chk"))).toBe(false);
42+
});
43+
});

0 commit comments

Comments
 (0)