@casekit/orm2-migrate 0.0.0-20250331202540 → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +3 -15
- package/build/pull/getColumns.d.ts +34 -0
- package/build/pull/{getTables.js → getColumns.js} +31 -4
- package/build/pull/{getTables.test.js → getColumns.test.js} +26 -4
- package/build/pull/getForeignKeys.d.ts +18 -0
- package/build/pull/getForeignKeys.js +72 -0
- package/build/pull/getForeignKeys.test.js +86 -0
- package/build/pull/getPrimaryKeys.d.ts +14 -0
- package/build/pull/getPrimaryKeys.js +30 -0
- package/build/pull/getPrimaryKeys.test.d.ts +1 -0
- package/build/pull/getPrimaryKeys.test.js +87 -0
- package/build/pull/getUniqueConstraints.d.ts +14 -0
- package/build/pull/getUniqueConstraints.js +27 -0
- package/build/pull/getUniqueConstraints.test.d.ts +1 -0
- package/build/pull/getUniqueConstraints.test.js +99 -0
- package/build/pull/index.d.ts +4 -0
- package/build/pull/index.js +1 -0
- package/build/pull/pullDefault.d.ts +6 -0
- package/build/pull/pullDefault.js +44 -0
- package/build/pull/pullDefault.test.d.ts +1 -0
- package/build/pull/pullDefault.test.js +82 -0
- package/build/pull.d.ts +12 -14
- package/build/pull.js +54 -2
- package/build/push/createTableSql.test.js +11 -0
- package/package.json +18 -18
- package/build/pull/getTables.d.ts +0 -60
- package/build/render/renderDefault.d.ts +0 -2
- package/build/render/renderDefault.js +0 -12
- package/build/render/renderDefault.test.js +0 -23
- package/build/render/renderModel.d.ts +0 -2
- package/build/render/renderModel.js +0 -35
- /package/build/pull/{getTables.test.d.ts → getColumns.test.d.ts} +0 -0
- /package/build/{render/renderDefault.test.d.ts → pull/getForeignKeys.test.d.ts} +0 -0
package/build/index.d.ts
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
export declare const migrate: {
|
|
2
2
|
drop: (db: import("@casekit/orm2").Orm) => Promise<void>;
|
|
3
3
|
push: (db: import("@casekit/orm2").Orm) => Promise<void>;
|
|
4
|
-
pull: (db: import("@casekit/orm2").Orm, schemas: string[]) => Promise<
|
|
5
|
-
schema: string;
|
|
6
|
-
table: string;
|
|
7
|
-
column: string;
|
|
8
|
-
ordinalPosition: number | null;
|
|
9
|
-
type: string;
|
|
10
|
-
default: string | null;
|
|
11
|
-
nullable: boolean;
|
|
12
|
-
udtSchema: string;
|
|
13
|
-
udt: string;
|
|
14
|
-
elementType: string | null;
|
|
15
|
-
elementTypeSchema: string | null;
|
|
16
|
-
cardinality: number;
|
|
17
|
-
size: number | null;
|
|
18
|
-
}[]>;
|
|
4
|
+
pull: (db: import("@casekit/orm2").Orm, schemas: string[]) => Promise<import("#pull.js").Table[]>;
|
|
19
5
|
};
|
|
6
|
+
export type { Table } from "#pull.js";
|
|
7
|
+
export type { Column, ForeignKey, PrimaryKey, UniqueConstraint, } from "#pull/index.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ColumnSchema: z.ZodObject<{
|
|
3
|
+
schema: z.ZodString;
|
|
4
|
+
table: z.ZodString;
|
|
5
|
+
column: z.ZodString;
|
|
6
|
+
ordinalPosition: z.ZodNullable<z.ZodNumber>;
|
|
7
|
+
type: z.ZodString;
|
|
8
|
+
default: z.ZodNullable<z.ZodString>;
|
|
9
|
+
nullable: z.ZodBoolean;
|
|
10
|
+
udtSchema: z.ZodString;
|
|
11
|
+
udt: z.ZodString;
|
|
12
|
+
elementType: z.ZodNullable<z.ZodString>;
|
|
13
|
+
elementTypeSchema: z.ZodNullable<z.ZodString>;
|
|
14
|
+
cardinality: z.ZodNumber;
|
|
15
|
+
size: z.ZodNullable<z.ZodNumber>;
|
|
16
|
+
isSerial: z.ZodBoolean;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type Column = z.infer<typeof ColumnSchema>;
|
|
19
|
+
export declare const getColumns: (schemas: string[]) => import("@casekit/orm2").SQLStatement<{
|
|
20
|
+
schema: string;
|
|
21
|
+
table: string;
|
|
22
|
+
column: string;
|
|
23
|
+
ordinalPosition: number | null;
|
|
24
|
+
type: string;
|
|
25
|
+
default: string | null;
|
|
26
|
+
nullable: boolean;
|
|
27
|
+
udtSchema: string;
|
|
28
|
+
udt: string;
|
|
29
|
+
elementType: string | null;
|
|
30
|
+
elementTypeSchema: string | null;
|
|
31
|
+
cardinality: number;
|
|
32
|
+
size: number | null;
|
|
33
|
+
isSerial: boolean;
|
|
34
|
+
}>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sql } from "@casekit/orm2";
|
|
3
|
-
export const
|
|
3
|
+
export const ColumnSchema = z.object({
|
|
4
4
|
schema: z.string(),
|
|
5
5
|
table: z.string(),
|
|
6
6
|
column: z.string(),
|
|
@@ -14,14 +14,20 @@ export const TableColumnSchema = z.object({
|
|
|
14
14
|
elementTypeSchema: z.string().nullable(),
|
|
15
15
|
cardinality: z.number(),
|
|
16
16
|
size: z.number().nullable(),
|
|
17
|
+
isSerial: z.boolean(),
|
|
17
18
|
});
|
|
18
|
-
export const
|
|
19
|
+
export const getColumns = (schemas) => sql(ColumnSchema) `
|
|
19
20
|
SELECT
|
|
20
21
|
c.table_schema AS "schema",
|
|
21
22
|
c.table_name AS "table",
|
|
22
23
|
c.column_name AS "column",
|
|
23
24
|
c.ordinal_position AS "ordinalPosition",
|
|
24
|
-
|
|
25
|
+
CASE
|
|
26
|
+
WHEN c.udt_name = 'int2vector' THEN 'int2vector'
|
|
27
|
+
WHEN c.udt_name = 'oidvector' THEN 'oidvector'
|
|
28
|
+
WHEN c.data_type = 'ARRAY' AND c.udt_name LIKE '\_int2vector' THEN 'int2vector[]'
|
|
29
|
+
ELSE c.data_type
|
|
30
|
+
END AS "type",
|
|
25
31
|
c.column_default AS "default",
|
|
26
32
|
c.is_nullable = 'YES' AS "nullable",
|
|
27
33
|
c.udt_schema AS "udtSchema",
|
|
@@ -29,7 +35,8 @@ export const getTables = (schemas) => sql(TableColumnSchema) `
|
|
|
29
35
|
e.data_type AS "elementType",
|
|
30
36
|
e.udt_schema AS "elementTypeSchema",
|
|
31
37
|
pa.attndims AS cardinality,
|
|
32
|
-
c.character_maximum_length AS "size"
|
|
38
|
+
c.character_maximum_length AS "size",
|
|
39
|
+
COALESCE(seq_owned.is_serial, false) AS "isSerial"
|
|
33
40
|
FROM
|
|
34
41
|
information_schema.tables t
|
|
35
42
|
JOIN information_schema.columns c ON t.table_name = c.table_name
|
|
@@ -46,6 +53,26 @@ export const getTables = (schemas) => sql(TableColumnSchema) `
|
|
|
46
53
|
AND e.object_type = 'TABLE'
|
|
47
54
|
AND e.collection_type_identifier = c.dtd_identifier
|
|
48
55
|
AND c.data_type = 'ARRAY'
|
|
56
|
+
LEFT JOIN (
|
|
57
|
+
SELECT
|
|
58
|
+
pn.nspname AS schema_name,
|
|
59
|
+
pc.relname AS table_name,
|
|
60
|
+
pa.attname AS column_name,
|
|
61
|
+
true AS is_serial
|
|
62
|
+
FROM
|
|
63
|
+
pg_depend pd
|
|
64
|
+
JOIN pg_class pc ON pd.refobjid = pc.oid AND pc.relkind = 'r'
|
|
65
|
+
JOIN pg_attribute pa ON pd.refobjid = pa.attrelid AND pd.refobjsubid = pa.attnum
|
|
66
|
+
JOIN pg_namespace pn ON pc.relnamespace = pn.oid
|
|
67
|
+
JOIN pg_class seq ON pd.objid = seq.oid AND seq.relkind = 'S'
|
|
68
|
+
WHERE
|
|
69
|
+
pd.deptype = 'a'
|
|
70
|
+
AND pd.classid = 'pg_class'::regclass
|
|
71
|
+
AND pd.refclassid = 'pg_class'::regclass
|
|
72
|
+
AND pn.nspname IN (${schemas})
|
|
73
|
+
) seq_owned ON seq_owned.schema_name = c.table_schema
|
|
74
|
+
AND seq_owned.table_name = c.table_name
|
|
75
|
+
AND seq_owned.column_name = c.column_name
|
|
49
76
|
WHERE
|
|
50
77
|
t.table_schema IN (${schemas})
|
|
51
78
|
AND t.table_type = 'BASE TABLE'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest";
|
|
2
2
|
import { orm, sql } from "@casekit/orm2";
|
|
3
|
-
import {
|
|
4
|
-
describe("
|
|
3
|
+
import { getColumns } from "./getColumns.js";
|
|
4
|
+
describe("getColumns", () => {
|
|
5
5
|
const db = orm({
|
|
6
6
|
schema: "migrate-get-tables-test",
|
|
7
7
|
models: {},
|
|
@@ -20,7 +20,7 @@ describe("getTables", () => {
|
|
|
20
20
|
await db.query `DROP SCHEMA IF EXISTS "migrate-get-tables-test" CASCADE;`;
|
|
21
21
|
});
|
|
22
22
|
test("with an empty database it returns no tables or columns", async () => {
|
|
23
|
-
const statement =
|
|
23
|
+
const statement = getColumns(["migrate-get-tables-test"]);
|
|
24
24
|
const result = await db.query(statement);
|
|
25
25
|
expect(result).toEqual([]);
|
|
26
26
|
});
|
|
@@ -59,16 +59,19 @@ describe("getTables", () => {
|
|
|
59
59
|
type: "smallint",
|
|
60
60
|
default: expect.stringContaining("nextval"),
|
|
61
61
|
nullable: false,
|
|
62
|
+
isSerial: true,
|
|
62
63
|
}),
|
|
63
64
|
expect.objectContaining({
|
|
64
65
|
type: "integer",
|
|
65
66
|
default: expect.stringContaining("nextval"),
|
|
66
67
|
nullable: false,
|
|
68
|
+
isSerial: true,
|
|
67
69
|
}),
|
|
68
70
|
expect.objectContaining({
|
|
69
71
|
type: "bigint",
|
|
70
72
|
default: expect.stringContaining("nextval"),
|
|
71
73
|
nullable: false,
|
|
74
|
+
isSerial: true,
|
|
72
75
|
}),
|
|
73
76
|
],
|
|
74
77
|
],
|
|
@@ -288,10 +291,29 @@ describe("getTables", () => {
|
|
|
288
291
|
expect.objectContaining({ type: "macaddr" }),
|
|
289
292
|
],
|
|
290
293
|
],
|
|
294
|
+
[
|
|
295
|
+
"handles sql now() default values correctly",
|
|
296
|
+
sql `
|
|
297
|
+
CREATE TABLE "migrate-get-tables-test"."timestamp_defaults_test" (
|
|
298
|
+
"created_at" TIMESTAMP DEFAULT now(),
|
|
299
|
+
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now()
|
|
300
|
+
);
|
|
301
|
+
`,
|
|
302
|
+
[
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
type: "timestamp without time zone",
|
|
305
|
+
default: "now()",
|
|
306
|
+
}),
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
type: "timestamp with time zone",
|
|
309
|
+
default: "now()",
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
],
|
|
291
313
|
])("%s", async ([_, query, expected]) => {
|
|
292
314
|
await db.transact(async (db) => {
|
|
293
315
|
await db.query(query);
|
|
294
|
-
const result = await db.query(
|
|
316
|
+
const result = await db.query(getColumns(["migrate-get-tables-test"]));
|
|
295
317
|
expect(result).toEqual(expected);
|
|
296
318
|
}, { rollback: true });
|
|
297
319
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ForeignKeySchema: z.ZodObject<{
|
|
3
|
+
schema: z.ZodString;
|
|
4
|
+
constraintName: z.ZodString;
|
|
5
|
+
tableFrom: z.ZodString;
|
|
6
|
+
columnsFrom: z.ZodArray<z.ZodString>;
|
|
7
|
+
tableTo: z.ZodString;
|
|
8
|
+
columnsTo: z.ZodArray<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export type ForeignKey = z.infer<typeof ForeignKeySchema>;
|
|
11
|
+
export declare const getForeignKeys: (schemas: string[]) => import("@casekit/orm2").SQLStatement<{
|
|
12
|
+
schema: string;
|
|
13
|
+
constraintName: string;
|
|
14
|
+
tableFrom: string;
|
|
15
|
+
columnsFrom: string[];
|
|
16
|
+
tableTo: string;
|
|
17
|
+
columnsTo: string[];
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sql } from "@casekit/orm2";
|
|
3
|
+
export const ForeignKeySchema = z.object({
|
|
4
|
+
schema: z.string(),
|
|
5
|
+
constraintName: z.string(),
|
|
6
|
+
tableFrom: z.string(),
|
|
7
|
+
columnsFrom: z.array(z.string()),
|
|
8
|
+
tableTo: z.string(),
|
|
9
|
+
columnsTo: z.array(z.string()),
|
|
10
|
+
});
|
|
11
|
+
export const getForeignKeys = (schemas) => sql(ForeignKeySchema) `
|
|
12
|
+
SELECT
|
|
13
|
+
nspname AS "schema",
|
|
14
|
+
conname AS "constraintName",
|
|
15
|
+
table_from AS "tableFrom",
|
|
16
|
+
array_agg(columns_from::text ORDER BY ordinality) AS "columnsFrom",
|
|
17
|
+
table_to AS "tableTo",
|
|
18
|
+
array_agg(columns_to::text ORDER BY ordinality) AS "columnsTo"
|
|
19
|
+
FROM (
|
|
20
|
+
SELECT
|
|
21
|
+
conname,
|
|
22
|
+
c.relname AS table_from,
|
|
23
|
+
a.attname AS columns_from,
|
|
24
|
+
cf.relname AS table_to,
|
|
25
|
+
af.attname AS columns_to,
|
|
26
|
+
n.nspname,
|
|
27
|
+
ss2.ordinality
|
|
28
|
+
FROM
|
|
29
|
+
pg_attribute AS af,
|
|
30
|
+
pg_attribute AS a,
|
|
31
|
+
pg_class c,
|
|
32
|
+
pg_class cf,
|
|
33
|
+
pg_namespace n,
|
|
34
|
+
(
|
|
35
|
+
SELECT
|
|
36
|
+
conname,
|
|
37
|
+
conrelid,
|
|
38
|
+
confrelid,
|
|
39
|
+
conkey[i] AS conkey,
|
|
40
|
+
confkey[i] AS confkey,
|
|
41
|
+
i AS ordinality
|
|
42
|
+
FROM (
|
|
43
|
+
SELECT
|
|
44
|
+
conname,
|
|
45
|
+
conrelid,
|
|
46
|
+
confrelid,
|
|
47
|
+
conkey,
|
|
48
|
+
confkey,
|
|
49
|
+
generate_series(1, array_upper(conkey, 1)) AS i
|
|
50
|
+
FROM
|
|
51
|
+
pg_constraint
|
|
52
|
+
WHERE
|
|
53
|
+
contype = 'f') AS ss) AS ss2
|
|
54
|
+
WHERE
|
|
55
|
+
af.attnum = confkey
|
|
56
|
+
AND af.attrelid = confrelid
|
|
57
|
+
AND a.attnum = conkey
|
|
58
|
+
AND a.attrelid = conrelid
|
|
59
|
+
AND a.attrelid = c.oid
|
|
60
|
+
AND af.attrelid = cf.oid
|
|
61
|
+
AND c.relnamespace = n.oid
|
|
62
|
+
AND n.nspname IN (${schemas})) AS ss3
|
|
63
|
+
GROUP BY
|
|
64
|
+
nspname,
|
|
65
|
+
conname,
|
|
66
|
+
table_to,
|
|
67
|
+
table_from
|
|
68
|
+
ORDER BY
|
|
69
|
+
nspname,
|
|
70
|
+
table_from,
|
|
71
|
+
conname
|
|
72
|
+
`;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { getForeignKeys } from "./getForeignKeys.js";
|
|
4
|
+
describe("getForeignKeys", () => {
|
|
5
|
+
const db = orm({
|
|
6
|
+
schema: "migrate-get-foreign-keys-test",
|
|
7
|
+
models: {},
|
|
8
|
+
});
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await db.connect();
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await db.close();
|
|
14
|
+
});
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-foreign-keys-test" CASCADE;`;
|
|
17
|
+
await db.query `CREATE SCHEMA IF NOT EXISTS "migrate-get-foreign-keys-test";`;
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-foreign-keys-test" CASCADE;`;
|
|
21
|
+
});
|
|
22
|
+
test("with an empty database it returns no foreign keys", async () => {
|
|
23
|
+
const statement = getForeignKeys(["migrate-get-foreign-keys-test"]);
|
|
24
|
+
const result = await db.query(statement);
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
test("returns foreign key relationships", async () => {
|
|
28
|
+
await db.transact(async (db) => {
|
|
29
|
+
await db.query(sql `
|
|
30
|
+
CREATE TABLE "migrate-get-foreign-keys-test"."users" (
|
|
31
|
+
"id" SERIAL PRIMARY KEY,
|
|
32
|
+
"name" TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
await db.query(sql `
|
|
36
|
+
CREATE TABLE "migrate-get-foreign-keys-test"."posts" (
|
|
37
|
+
"id" SERIAL PRIMARY KEY,
|
|
38
|
+
"title" TEXT NOT NULL,
|
|
39
|
+
"user_id" INTEGER NOT NULL,
|
|
40
|
+
CONSTRAINT "fk_posts_user_id" FOREIGN KEY ("user_id") REFERENCES "migrate-get-foreign-keys-test"."users" ("id")
|
|
41
|
+
);
|
|
42
|
+
`);
|
|
43
|
+
const result = await db.query(getForeignKeys(["migrate-get-foreign-keys-test"]));
|
|
44
|
+
expect(result).toEqual([
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
schema: "migrate-get-foreign-keys-test",
|
|
47
|
+
constraintName: "fk_posts_user_id",
|
|
48
|
+
tableFrom: "posts",
|
|
49
|
+
columnsFrom: ["user_id"],
|
|
50
|
+
tableTo: "users",
|
|
51
|
+
columnsTo: ["id"],
|
|
52
|
+
}),
|
|
53
|
+
]);
|
|
54
|
+
}, { rollback: true });
|
|
55
|
+
});
|
|
56
|
+
test("handles composite foreign keys", async () => {
|
|
57
|
+
await db.transact(async (db) => {
|
|
58
|
+
await db.query(sql `
|
|
59
|
+
CREATE TABLE "migrate-get-foreign-keys-test"."companies" (
|
|
60
|
+
"id" INTEGER NOT NULL,
|
|
61
|
+
"code" TEXT NOT NULL,
|
|
62
|
+
PRIMARY KEY ("id", "code")
|
|
63
|
+
);
|
|
64
|
+
`);
|
|
65
|
+
await db.query(sql `
|
|
66
|
+
CREATE TABLE "migrate-get-foreign-keys-test"."employees" (
|
|
67
|
+
"id" SERIAL PRIMARY KEY,
|
|
68
|
+
"company_id" INTEGER NOT NULL,
|
|
69
|
+
"company_code" TEXT NOT NULL,
|
|
70
|
+
CONSTRAINT "fk_employees_company" FOREIGN KEY ("company_id", "company_code") REFERENCES "migrate-get-foreign-keys-test"."companies" ("id", "code")
|
|
71
|
+
);
|
|
72
|
+
`);
|
|
73
|
+
const result = await db.query(getForeignKeys(["migrate-get-foreign-keys-test"]));
|
|
74
|
+
expect(result).toEqual([
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
schema: "migrate-get-foreign-keys-test",
|
|
77
|
+
constraintName: "fk_employees_company",
|
|
78
|
+
tableFrom: "employees",
|
|
79
|
+
columnsFrom: ["company_id", "company_code"],
|
|
80
|
+
tableTo: "companies",
|
|
81
|
+
columnsTo: ["id", "code"],
|
|
82
|
+
}),
|
|
83
|
+
]);
|
|
84
|
+
}, { rollback: true });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const PrimaryKeySchema: z.ZodObject<{
|
|
3
|
+
schema: z.ZodString;
|
|
4
|
+
table: z.ZodString;
|
|
5
|
+
constraintName: z.ZodString;
|
|
6
|
+
columns: z.ZodArray<z.ZodString>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type PrimaryKey = z.infer<typeof PrimaryKeySchema>;
|
|
9
|
+
export declare const getPrimaryKeys: (schemas: string[]) => import("@casekit/orm2").SQLStatement<{
|
|
10
|
+
schema: string;
|
|
11
|
+
table: string;
|
|
12
|
+
constraintName: string;
|
|
13
|
+
columns: string[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sql } from "@casekit/orm2";
|
|
3
|
+
export const PrimaryKeySchema = z.object({
|
|
4
|
+
schema: z.string(),
|
|
5
|
+
table: z.string(),
|
|
6
|
+
constraintName: z.string(),
|
|
7
|
+
columns: z.array(z.string()),
|
|
8
|
+
});
|
|
9
|
+
export const getPrimaryKeys = (schemas) => sql(PrimaryKeySchema) `
|
|
10
|
+
SELECT
|
|
11
|
+
c.constraint_schema AS "schema",
|
|
12
|
+
c.table_name AS "table",
|
|
13
|
+
c.constraint_name AS "constraintName",
|
|
14
|
+
array_agg(cc.column_name::text ORDER BY cc.ordinal_position) AS "columns"
|
|
15
|
+
FROM
|
|
16
|
+
information_schema.table_constraints c
|
|
17
|
+
JOIN information_schema.key_column_usage cc ON c.constraint_name = cc.constraint_name
|
|
18
|
+
AND c.constraint_schema = cc.constraint_schema
|
|
19
|
+
AND c.table_name = cc.table_name
|
|
20
|
+
WHERE
|
|
21
|
+
c.constraint_type = 'PRIMARY KEY'
|
|
22
|
+
AND c.constraint_schema IN (${schemas})
|
|
23
|
+
GROUP BY
|
|
24
|
+
c.constraint_schema,
|
|
25
|
+
c.table_name,
|
|
26
|
+
c.constraint_name
|
|
27
|
+
ORDER BY
|
|
28
|
+
c.constraint_schema,
|
|
29
|
+
c.table_name
|
|
30
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { getPrimaryKeys } from "./getPrimaryKeys.js";
|
|
4
|
+
describe("getPrimaryKeys", () => {
|
|
5
|
+
const db = orm({
|
|
6
|
+
schema: "migrate-get-primary-keys-test",
|
|
7
|
+
models: {},
|
|
8
|
+
});
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await db.connect();
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await db.close();
|
|
14
|
+
});
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-primary-keys-test" CASCADE;`;
|
|
17
|
+
await db.query `CREATE SCHEMA IF NOT EXISTS "migrate-get-primary-keys-test";`;
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-primary-keys-test" CASCADE;`;
|
|
21
|
+
});
|
|
22
|
+
test("with an empty database it returns no primary keys", async () => {
|
|
23
|
+
const statement = getPrimaryKeys(["migrate-get-primary-keys-test"]);
|
|
24
|
+
const result = await db.query(statement);
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
test("returns single column primary key", async () => {
|
|
28
|
+
await db.transact(async (db) => {
|
|
29
|
+
await db.query(sql `
|
|
30
|
+
CREATE TABLE "migrate-get-primary-keys-test"."users" (
|
|
31
|
+
"id" SERIAL PRIMARY KEY,
|
|
32
|
+
"name" TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
const result = await db.query(getPrimaryKeys(["migrate-get-primary-keys-test"]));
|
|
36
|
+
expect(result).toEqual([
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
schema: "migrate-get-primary-keys-test",
|
|
39
|
+
table: "users",
|
|
40
|
+
constraintName: "users_pkey",
|
|
41
|
+
columns: ["id"],
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
}, { rollback: true });
|
|
45
|
+
});
|
|
46
|
+
test("returns composite primary key with correct order", async () => {
|
|
47
|
+
await db.transact(async (db) => {
|
|
48
|
+
await db.query(sql `
|
|
49
|
+
CREATE TABLE "migrate-get-primary-keys-test"."user_roles" (
|
|
50
|
+
"user_id" INTEGER NOT NULL,
|
|
51
|
+
"role_id" INTEGER NOT NULL,
|
|
52
|
+
"granted_at" TIMESTAMP DEFAULT NOW(),
|
|
53
|
+
PRIMARY KEY ("user_id", "role_id")
|
|
54
|
+
);
|
|
55
|
+
`);
|
|
56
|
+
const result = await db.query(getPrimaryKeys(["migrate-get-primary-keys-test"]));
|
|
57
|
+
expect(result).toEqual([
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
schema: "migrate-get-primary-keys-test",
|
|
60
|
+
table: "user_roles",
|
|
61
|
+
constraintName: "user_roles_pkey",
|
|
62
|
+
columns: ["user_id", "role_id"],
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
}, { rollback: true });
|
|
66
|
+
});
|
|
67
|
+
test("returns named primary key constraint", async () => {
|
|
68
|
+
await db.transact(async (db) => {
|
|
69
|
+
await db.query(sql `
|
|
70
|
+
CREATE TABLE "migrate-get-primary-keys-test"."products" (
|
|
71
|
+
"id" UUID NOT NULL,
|
|
72
|
+
"name" TEXT NOT NULL,
|
|
73
|
+
CONSTRAINT "pk_products_id" PRIMARY KEY ("id")
|
|
74
|
+
);
|
|
75
|
+
`);
|
|
76
|
+
const result = await db.query(getPrimaryKeys(["migrate-get-primary-keys-test"]));
|
|
77
|
+
expect(result).toEqual([
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
schema: "migrate-get-primary-keys-test",
|
|
80
|
+
table: "products",
|
|
81
|
+
constraintName: "pk_products_id",
|
|
82
|
+
columns: ["id"],
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
}, { rollback: true });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const UniqueConstraintSchema: z.ZodObject<{
|
|
3
|
+
schema: z.ZodString;
|
|
4
|
+
table: z.ZodString;
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
definition: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type UniqueConstraint = z.infer<typeof UniqueConstraintSchema>;
|
|
9
|
+
export declare const getUniqueConstraints: (schemas: string[]) => import("@casekit/orm2").SQLStatement<{
|
|
10
|
+
schema: string;
|
|
11
|
+
table: string;
|
|
12
|
+
name: string;
|
|
13
|
+
definition: string;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sql } from "@casekit/orm2";
|
|
3
|
+
export const UniqueConstraintSchema = z.object({
|
|
4
|
+
schema: z.string(),
|
|
5
|
+
table: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
definition: z.string(),
|
|
8
|
+
});
|
|
9
|
+
export const getUniqueConstraints = (schemas) => sql(UniqueConstraintSchema) `
|
|
10
|
+
SELECT
|
|
11
|
+
i.schemaname AS "schema",
|
|
12
|
+
i.tablename AS "table",
|
|
13
|
+
i.indexname AS "name",
|
|
14
|
+
i.indexdef AS "definition"
|
|
15
|
+
FROM
|
|
16
|
+
pg_indexes i
|
|
17
|
+
LEFT JOIN pg_constraint c ON i.indexname = c.conname
|
|
18
|
+
AND c.contype IN ('p', 'x')
|
|
19
|
+
WHERE
|
|
20
|
+
i.schemaname IN (${schemas})
|
|
21
|
+
AND c.conname IS NULL
|
|
22
|
+
AND i.indexdef LIKE 'CREATE UNIQUE INDEX%'
|
|
23
|
+
ORDER BY
|
|
24
|
+
i.schemaname,
|
|
25
|
+
i.tablename,
|
|
26
|
+
i.indexname
|
|
27
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { getUniqueConstraints } from "./getUniqueConstraints.js";
|
|
4
|
+
describe("getUniqueConstraints", () => {
|
|
5
|
+
const db = orm({
|
|
6
|
+
schema: "migrate-get-unique-constraints-test",
|
|
7
|
+
models: {},
|
|
8
|
+
});
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await db.connect();
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await db.close();
|
|
14
|
+
});
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-unique-constraints-test" CASCADE;`;
|
|
17
|
+
await db.query `CREATE SCHEMA IF NOT EXISTS "migrate-get-unique-constraints-test";`;
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-unique-constraints-test" CASCADE;`;
|
|
21
|
+
});
|
|
22
|
+
test("with an empty database it returns no unique constraints", async () => {
|
|
23
|
+
const statement = getUniqueConstraints([
|
|
24
|
+
"migrate-get-unique-constraints-test",
|
|
25
|
+
]);
|
|
26
|
+
const result = await db.query(statement);
|
|
27
|
+
expect(result).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
test("returns unique index constraints", async () => {
|
|
30
|
+
await db.transact(async (db) => {
|
|
31
|
+
await db.query(sql `
|
|
32
|
+
CREATE TABLE "migrate-get-unique-constraints-test"."users" (
|
|
33
|
+
"id" SERIAL PRIMARY KEY,
|
|
34
|
+
"email" TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
await db.query(sql `
|
|
38
|
+
CREATE UNIQUE INDEX "idx_users_email" ON "migrate-get-unique-constraints-test"."users" ("email");
|
|
39
|
+
`);
|
|
40
|
+
const result = await db.query(getUniqueConstraints([
|
|
41
|
+
"migrate-get-unique-constraints-test",
|
|
42
|
+
]));
|
|
43
|
+
expect(result).toEqual([
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
schema: "migrate-get-unique-constraints-test",
|
|
46
|
+
table: "users",
|
|
47
|
+
name: "idx_users_email",
|
|
48
|
+
definition: expect.stringContaining("CREATE UNIQUE INDEX idx_users_email"),
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
}, { rollback: true });
|
|
52
|
+
});
|
|
53
|
+
test("returns composite unique index constraints", async () => {
|
|
54
|
+
await db.transact(async (db) => {
|
|
55
|
+
await db.query(sql `
|
|
56
|
+
CREATE TABLE "migrate-get-unique-constraints-test"."products" (
|
|
57
|
+
"id" SERIAL PRIMARY KEY,
|
|
58
|
+
"name" TEXT NOT NULL,
|
|
59
|
+
"category" TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
`);
|
|
62
|
+
await db.query(sql `
|
|
63
|
+
CREATE UNIQUE INDEX "idx_products_name_category" ON "migrate-get-unique-constraints-test"."products" ("name", "category");
|
|
64
|
+
`);
|
|
65
|
+
const result = await db.query(getUniqueConstraints([
|
|
66
|
+
"migrate-get-unique-constraints-test",
|
|
67
|
+
]));
|
|
68
|
+
expect(result).toEqual([
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
schema: "migrate-get-unique-constraints-test",
|
|
71
|
+
table: "products",
|
|
72
|
+
name: "idx_products_name_category",
|
|
73
|
+
definition: expect.stringContaining("CREATE UNIQUE INDEX idx_products_name_category"),
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
}, { rollback: true });
|
|
77
|
+
});
|
|
78
|
+
test("excludes primary key constraints", async () => {
|
|
79
|
+
await db.transact(async (db) => {
|
|
80
|
+
await db.query(sql `
|
|
81
|
+
CREATE TABLE "migrate-get-unique-constraints-test"."users" (
|
|
82
|
+
"id" SERIAL PRIMARY KEY,
|
|
83
|
+
"email" TEXT UNIQUE
|
|
84
|
+
);
|
|
85
|
+
`);
|
|
86
|
+
const result = await db.query(getUniqueConstraints([
|
|
87
|
+
"migrate-get-unique-constraints-test",
|
|
88
|
+
]));
|
|
89
|
+
// Should only return the unique constraint on email, not the primary key
|
|
90
|
+
expect(result).toHaveLength(1);
|
|
91
|
+
expect(result[0]).toEqual(expect.objectContaining({
|
|
92
|
+
schema: "migrate-get-unique-constraints-test",
|
|
93
|
+
table: "users",
|
|
94
|
+
definition: expect.stringContaining("email"),
|
|
95
|
+
}));
|
|
96
|
+
expect(result[0].definition).not.toContain("id");
|
|
97
|
+
}, { rollback: true });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes PostgreSQL default values to their canonical forms
|
|
3
|
+
* This is the inverse of renderDefault - it takes raw PostgreSQL defaults
|
|
4
|
+
* and converts them back to standard representations.
|
|
5
|
+
*/
|
|
6
|
+
export declare const pullDefault: (defaultValue: string | null) => string | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes PostgreSQL default values to their canonical forms
|
|
3
|
+
* This is the inverse of renderDefault - it takes raw PostgreSQL defaults
|
|
4
|
+
* and converts them back to standard representations.
|
|
5
|
+
*/
|
|
6
|
+
export const pullDefault = (defaultValue) => {
|
|
7
|
+
if (defaultValue === null) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
// Only convert when the default is literally "now()" - not timestamp literals
|
|
11
|
+
if (defaultValue === "now()") {
|
|
12
|
+
return "now()";
|
|
13
|
+
}
|
|
14
|
+
// Handle other common SQL functions that should remain as function calls
|
|
15
|
+
if (/^(gen_random_uuid|uuid_generate_v\d+|current_timestamp|current_date|current_time)\(\)$/.test(defaultValue)) {
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
// Handle nextval sequences (for serial types) - keep as-is
|
|
19
|
+
if (/^nextval\('.*'::regclass\)$/.test(defaultValue)) {
|
|
20
|
+
return defaultValue;
|
|
21
|
+
}
|
|
22
|
+
// Handle simple literals without type casting
|
|
23
|
+
if (/^'(.*)'::(text|varchar|char|bpchar)$/.test(defaultValue)) {
|
|
24
|
+
return defaultValue.replace(/^'(.*)'::(text|varchar|char|bpchar)$/, "'$1'");
|
|
25
|
+
}
|
|
26
|
+
// Handle numeric literals with type casting
|
|
27
|
+
if (/^'(.*)'::(numeric|decimal|real|double precision|float\d*|int\d*|smallint|bigint|integer)$/.test(defaultValue)) {
|
|
28
|
+
return defaultValue.replace(/^'(.*)'::(numeric|decimal|real|double precision|float\d*|int\d*|smallint|bigint|integer)$/, "$1");
|
|
29
|
+
}
|
|
30
|
+
// Handle boolean literals
|
|
31
|
+
if (defaultValue === "true" || defaultValue === "false") {
|
|
32
|
+
return defaultValue;
|
|
33
|
+
}
|
|
34
|
+
// Handle NULL with type casting
|
|
35
|
+
if (/^NULL::.+$/.test(defaultValue)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// Handle array literals - keep as-is since they're complex
|
|
39
|
+
if (/^(ARRAY\[.*\]|\{.*\})/.test(defaultValue)) {
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
// For everything else, return as-is
|
|
43
|
+
return defaultValue;
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { pullDefault } from "./pullDefault.js";
|
|
3
|
+
describe("pullDefault", () => {
|
|
4
|
+
test("should return null for null values", () => {
|
|
5
|
+
expect(pullDefault(null)).toBe(null);
|
|
6
|
+
});
|
|
7
|
+
test("should preserve timestamp literals as-is", () => {
|
|
8
|
+
expect(pullDefault("'2025-06-28 15:02:55.251517'::timestamp without time zone")).toBe("'2025-06-28 15:02:55.251517'::timestamp without time zone");
|
|
9
|
+
expect(pullDefault("'2025-06-28 15:02:55.251517+00'::timestamptz")).toBe("'2025-06-28 15:02:55.251517+00'::timestamptz");
|
|
10
|
+
expect(pullDefault("'2023-01-01 12:00:00'::timestamp")).toBe("'2023-01-01 12:00:00'::timestamp");
|
|
11
|
+
expect(pullDefault("'2025-06-28 15:02:55'::timestamp with time zone")).toBe("'2025-06-28 15:02:55'::timestamp with time zone");
|
|
12
|
+
});
|
|
13
|
+
test("should preserve SQL function calls", () => {
|
|
14
|
+
expect(pullDefault("now()")).toBe("now()");
|
|
15
|
+
expect(pullDefault("gen_random_uuid()")).toBe("gen_random_uuid()");
|
|
16
|
+
expect(pullDefault("uuid_generate_v4()")).toBe("uuid_generate_v4()");
|
|
17
|
+
expect(pullDefault("current_timestamp()")).toBe("current_timestamp()");
|
|
18
|
+
expect(pullDefault("current_date()")).toBe("current_date()");
|
|
19
|
+
expect(pullDefault("current_time()")).toBe("current_time()");
|
|
20
|
+
});
|
|
21
|
+
test("should preserve nextval sequences for serial types", () => {
|
|
22
|
+
expect(pullDefault("nextval('user_id_seq'::regclass)")).toBe("nextval('user_id_seq'::regclass)");
|
|
23
|
+
expect(pullDefault("nextval('public.post_id_seq'::regclass)")).toBe("nextval('public.post_id_seq'::regclass)");
|
|
24
|
+
});
|
|
25
|
+
test("should handle text literals with type casting", () => {
|
|
26
|
+
expect(pullDefault("'hello'::text")).toBe("'hello'");
|
|
27
|
+
expect(pullDefault("'world'::varchar")).toBe("'world'");
|
|
28
|
+
expect(pullDefault("'test'::char")).toBe("'test'");
|
|
29
|
+
expect(pullDefault("'example'::bpchar")).toBe("'example'");
|
|
30
|
+
});
|
|
31
|
+
test("should handle numeric literals with type casting", () => {
|
|
32
|
+
expect(pullDefault("'123'::numeric")).toBe("123");
|
|
33
|
+
expect(pullDefault("'456'::integer")).toBe("456");
|
|
34
|
+
expect(pullDefault("'789'::bigint")).toBe("789");
|
|
35
|
+
expect(pullDefault("'12.34'::decimal")).toBe("12.34");
|
|
36
|
+
expect(pullDefault("'3.14'::real")).toBe("3.14");
|
|
37
|
+
expect(pullDefault("'2.718'::double precision")).toBe("2.718");
|
|
38
|
+
expect(pullDefault("'42'::smallint")).toBe("42");
|
|
39
|
+
expect(pullDefault("'99.99'::float4")).toBe("99.99");
|
|
40
|
+
expect(pullDefault("'88.88'::int4")).toBe("88.88");
|
|
41
|
+
});
|
|
42
|
+
test("should handle boolean literals", () => {
|
|
43
|
+
expect(pullDefault("true")).toBe("true");
|
|
44
|
+
expect(pullDefault("false")).toBe("false");
|
|
45
|
+
});
|
|
46
|
+
test("should handle NULL with type casting", () => {
|
|
47
|
+
expect(pullDefault("NULL::text")).toBe(null);
|
|
48
|
+
expect(pullDefault("NULL::integer")).toBe(null);
|
|
49
|
+
expect(pullDefault("NULL::timestamp")).toBe(null);
|
|
50
|
+
});
|
|
51
|
+
test("should preserve array literals", () => {
|
|
52
|
+
expect(pullDefault("ARRAY[1, 2, 3]")).toBe("ARRAY[1, 2, 3]");
|
|
53
|
+
expect(pullDefault("ARRAY['foo', 'bar']")).toBe("ARRAY['foo', 'bar']");
|
|
54
|
+
expect(pullDefault("ARRAY[ARRAY[ARRAY['a']]]")).toBe("ARRAY[ARRAY[ARRAY['a']]]");
|
|
55
|
+
expect(pullDefault("{1, 2, 3}")).toBe("{1, 2, 3}");
|
|
56
|
+
expect(pullDefault("{{foo, bar}, {baz, qux}}")).toBe("{{foo, bar}, {baz, qux}}");
|
|
57
|
+
});
|
|
58
|
+
test("should handle complex expressions as-is", () => {
|
|
59
|
+
expect(pullDefault('\'{"key": "value"}\'::jsonb')).toBe('\'{"key": "value"}\'::jsonb');
|
|
60
|
+
expect(pullDefault("'[]'::json")).toBe("'[]'::json");
|
|
61
|
+
expect(pullDefault("'happy'::mood")).toBe("'happy'::mood");
|
|
62
|
+
expect(pullDefault("'admin'::user_role")).toBe("'admin'::user_role");
|
|
63
|
+
expect(pullDefault("'(0,0)'::point")).toBe("'(0,0)'::point");
|
|
64
|
+
expect(pullDefault("'1 day'::interval")).toBe("'1 day'::interval");
|
|
65
|
+
});
|
|
66
|
+
test("should handle money defaults", () => {
|
|
67
|
+
expect(pullDefault("42.42")).toBe("42.42");
|
|
68
|
+
expect(pullDefault("$100.00")).toBe("$100.00");
|
|
69
|
+
});
|
|
70
|
+
test("should handle edge cases", () => {
|
|
71
|
+
expect(pullDefault("")).toBe("");
|
|
72
|
+
expect(pullDefault("0")).toBe("0");
|
|
73
|
+
expect(pullDefault("-1")).toBe("-1");
|
|
74
|
+
expect(pullDefault("'0'")).toBe("'0'");
|
|
75
|
+
});
|
|
76
|
+
test("should handle complex SQL expressions", () => {
|
|
77
|
+
// These should be preserved as-is since they're complex
|
|
78
|
+
expect(pullDefault("CASE WHEN foo = 'bar' THEN 1 ELSE 0 END")).toBe("CASE WHEN foo = 'bar' THEN 1 ELSE 0 END");
|
|
79
|
+
expect(pullDefault("COALESCE(column1, column2, 'default')")).toBe("COALESCE(column1, column2, 'default')");
|
|
80
|
+
expect(pullDefault("LENGTH('hello')")).toBe("LENGTH('hello')");
|
|
81
|
+
});
|
|
82
|
+
});
|
package/build/pull.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Orm } from "@casekit/orm2";
|
|
2
|
-
|
|
2
|
+
import { type Column } from "./pull/getColumns.js";
|
|
3
|
+
import { type ForeignKey } from "./pull/getForeignKeys.js";
|
|
4
|
+
import { type PrimaryKey } from "./pull/getPrimaryKeys.js";
|
|
5
|
+
import { type UniqueConstraint } from "./pull/getUniqueConstraints.js";
|
|
6
|
+
export type Table = {
|
|
3
7
|
schema: string;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
udt: string;
|
|
12
|
-
elementType: string | null;
|
|
13
|
-
elementTypeSchema: string | null;
|
|
14
|
-
cardinality: number;
|
|
15
|
-
size: number | null;
|
|
16
|
-
}[]>;
|
|
8
|
+
name: string;
|
|
9
|
+
columns: Column[];
|
|
10
|
+
foreignKeys: ForeignKey[];
|
|
11
|
+
primaryKey: PrimaryKey | null;
|
|
12
|
+
uniqueConstraints: UniqueConstraint[];
|
|
13
|
+
};
|
|
14
|
+
export declare const pull: (db: Orm, schemas: string[]) => Promise<Table[]>;
|
package/build/pull.js
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { orderBy } from "es-toolkit";
|
|
2
|
+
import { getColumns } from "./pull/getColumns.js";
|
|
3
|
+
import { getForeignKeys } from "./pull/getForeignKeys.js";
|
|
4
|
+
import { getPrimaryKeys } from "./pull/getPrimaryKeys.js";
|
|
5
|
+
import { getUniqueConstraints, } from "./pull/getUniqueConstraints.js";
|
|
6
|
+
import { pullDefault } from "./pull/pullDefault.js";
|
|
2
7
|
export const pull = async (db, schemas) => {
|
|
3
|
-
|
|
8
|
+
const [columns, foreignKeys, primaryKeys, uniqueConstraints] = await Promise.all([
|
|
9
|
+
db.query(getColumns(schemas)),
|
|
10
|
+
db.query(getForeignKeys(schemas)),
|
|
11
|
+
db.query(getPrimaryKeys(schemas)),
|
|
12
|
+
db.query(getUniqueConstraints(schemas)),
|
|
13
|
+
]);
|
|
14
|
+
const tablesMap = new Map();
|
|
15
|
+
for (const column of orderBy(columns, ["ordinalPosition"], ["asc"])) {
|
|
16
|
+
const key = `${column.schema}.${column.table}`;
|
|
17
|
+
if (!tablesMap.has(key)) {
|
|
18
|
+
tablesMap.set(key, {
|
|
19
|
+
schema: column.schema,
|
|
20
|
+
name: column.table,
|
|
21
|
+
columns: [],
|
|
22
|
+
foreignKeys: [],
|
|
23
|
+
primaryKey: null,
|
|
24
|
+
uniqueConstraints: [],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// Normalize the default value using pullDefault
|
|
28
|
+
const normalizedColumn = {
|
|
29
|
+
...column,
|
|
30
|
+
default: pullDefault(column.default),
|
|
31
|
+
};
|
|
32
|
+
tablesMap.get(key).columns.push(normalizedColumn);
|
|
33
|
+
}
|
|
34
|
+
for (const fk of foreignKeys) {
|
|
35
|
+
const key = `${fk.schema}.${fk.tableFrom}`;
|
|
36
|
+
const table = tablesMap.get(key);
|
|
37
|
+
if (table) {
|
|
38
|
+
table.foreignKeys.push(fk);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const pk of primaryKeys) {
|
|
42
|
+
const key = `${pk.schema}.${pk.table}`;
|
|
43
|
+
const table = tablesMap.get(key);
|
|
44
|
+
if (table) {
|
|
45
|
+
table.primaryKey = pk;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const uc of uniqueConstraints) {
|
|
49
|
+
const key = `${uc.schema}.${uc.table}`;
|
|
50
|
+
const table = tablesMap.get(key);
|
|
51
|
+
if (table) {
|
|
52
|
+
table.uniqueConstraints.push(uc);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return orderBy(Array.from(tablesMap.values()), ["schema", "name"], ["asc", "asc"]);
|
|
4
56
|
};
|
|
@@ -79,6 +79,17 @@ describe("createTableSql", () => {
|
|
|
79
79
|
);
|
|
80
80
|
`,
|
|
81
81
|
],
|
|
82
|
+
[
|
|
83
|
+
"timestamp without time zone",
|
|
84
|
+
{ default: sql `now()` },
|
|
85
|
+
unindent `
|
|
86
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
87
|
+
"id" serial NOT NULL,
|
|
88
|
+
"value" timestamp without time zone NOT NULL DEFAULT now(),
|
|
89
|
+
PRIMARY KEY ("id")
|
|
90
|
+
);
|
|
91
|
+
`,
|
|
92
|
+
],
|
|
82
93
|
])("%s %s", async ([type, options, expected]) => {
|
|
83
94
|
const config = normalizeConfig({
|
|
84
95
|
models: {
|
package/package.json
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casekit/orm2-migrate",
|
|
3
3
|
"description": "",
|
|
4
|
-
"version": "0.0.0
|
|
4
|
+
"version": "0.0.0",
|
|
5
5
|
"author": "",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"camelcase": "^8.0.0",
|
|
8
|
-
"
|
|
9
|
-
"@casekit/orm2
|
|
10
|
-
"@casekit/orm2-config": "0.0.0
|
|
11
|
-
"@casekit/sql": "0.0.0
|
|
12
|
-
"@casekit/toolbox": "0.0.0
|
|
8
|
+
"es-toolkit": "^1.39.3",
|
|
9
|
+
"@casekit/orm2": "0.0.0",
|
|
10
|
+
"@casekit/orm2-config": "0.0.0",
|
|
11
|
+
"@casekit/sql": "0.0.0",
|
|
12
|
+
"@casekit/toolbox": "0.0.0",
|
|
13
|
+
"@casekit/orm2-schema": "0.0.0"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
16
|
"@casekit/unindent": "^1.0.5",
|
|
16
17
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
|
17
|
-
"@types/node": "^
|
|
18
|
-
"@types/pg": "^8.
|
|
18
|
+
"@types/node": "^24.0.3",
|
|
19
|
+
"@types/pg": "^8.15.4",
|
|
19
20
|
"@types/uuid": "^10.0.0",
|
|
20
|
-
"@vitest/coverage-v8": "^3.
|
|
21
|
-
"dotenv": "^16.
|
|
21
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
22
|
+
"dotenv": "^16.5.0",
|
|
22
23
|
"prettier": "^3.5.3",
|
|
23
|
-
"prettier-plugin-svelte": "^3.
|
|
24
|
-
"typescript": "^5.8.
|
|
24
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
25
|
+
"typescript": "^5.8.3",
|
|
25
26
|
"uuid": "^11.1.0",
|
|
26
27
|
"vite-tsconfig-paths": "^5.1.4",
|
|
27
|
-
"vitest": "^3.
|
|
28
|
-
"@casekit/orm2-fixtures": "0.0.0
|
|
29
|
-
"@casekit/
|
|
30
|
-
"@casekit/
|
|
28
|
+
"vitest": "^3.2.4",
|
|
29
|
+
"@casekit/orm2-fixtures": "0.0.0",
|
|
30
|
+
"@casekit/tsconfig": "0.0.0",
|
|
31
|
+
"@casekit/prettier-config": "0.0.0"
|
|
31
32
|
},
|
|
32
33
|
"exports": {
|
|
33
34
|
".": "./build/index.js"
|
|
@@ -40,10 +41,9 @@
|
|
|
40
41
|
},
|
|
41
42
|
"keywords": [],
|
|
42
43
|
"license": "ISC",
|
|
43
|
-
"main": "index.js",
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"pg": "^8.13.1",
|
|
46
|
-
"zod": "^
|
|
46
|
+
"zod": "^4.0.17"
|
|
47
47
|
},
|
|
48
48
|
"prettier": "@casekit/prettier-config",
|
|
49
49
|
"type": "module",
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export declare const TableColumnSchema: z.ZodObject<{
|
|
3
|
-
schema: z.ZodString;
|
|
4
|
-
table: z.ZodString;
|
|
5
|
-
column: z.ZodString;
|
|
6
|
-
ordinalPosition: z.ZodNullable<z.ZodNumber>;
|
|
7
|
-
type: z.ZodString;
|
|
8
|
-
default: z.ZodNullable<z.ZodString>;
|
|
9
|
-
nullable: z.ZodBoolean;
|
|
10
|
-
udtSchema: z.ZodString;
|
|
11
|
-
udt: z.ZodString;
|
|
12
|
-
elementType: z.ZodNullable<z.ZodString>;
|
|
13
|
-
elementTypeSchema: z.ZodNullable<z.ZodString>;
|
|
14
|
-
cardinality: z.ZodNumber;
|
|
15
|
-
size: z.ZodNullable<z.ZodNumber>;
|
|
16
|
-
}, "strip", z.ZodTypeAny, {
|
|
17
|
-
schema: string;
|
|
18
|
-
table: string;
|
|
19
|
-
column: string;
|
|
20
|
-
ordinalPosition: number | null;
|
|
21
|
-
type: string;
|
|
22
|
-
default: string | null;
|
|
23
|
-
nullable: boolean;
|
|
24
|
-
udtSchema: string;
|
|
25
|
-
udt: string;
|
|
26
|
-
elementType: string | null;
|
|
27
|
-
elementTypeSchema: string | null;
|
|
28
|
-
cardinality: number;
|
|
29
|
-
size: number | null;
|
|
30
|
-
}, {
|
|
31
|
-
schema: string;
|
|
32
|
-
table: string;
|
|
33
|
-
column: string;
|
|
34
|
-
ordinalPosition: number | null;
|
|
35
|
-
type: string;
|
|
36
|
-
default: string | null;
|
|
37
|
-
nullable: boolean;
|
|
38
|
-
udtSchema: string;
|
|
39
|
-
udt: string;
|
|
40
|
-
elementType: string | null;
|
|
41
|
-
elementTypeSchema: string | null;
|
|
42
|
-
cardinality: number;
|
|
43
|
-
size: number | null;
|
|
44
|
-
}>;
|
|
45
|
-
export type TableColumn = z.infer<typeof TableColumnSchema>;
|
|
46
|
-
export declare const getTables: (schemas: string[]) => import("@casekit/orm2").SQLStatement<{
|
|
47
|
-
schema: string;
|
|
48
|
-
table: string;
|
|
49
|
-
column: string;
|
|
50
|
-
ordinalPosition: number | null;
|
|
51
|
-
type: string;
|
|
52
|
-
default: string | null;
|
|
53
|
-
nullable: boolean;
|
|
54
|
-
udtSchema: string;
|
|
55
|
-
udt: string;
|
|
56
|
-
elementType: string | null;
|
|
57
|
-
elementTypeSchema: string | null;
|
|
58
|
-
cardinality: number;
|
|
59
|
-
size: number | null;
|
|
60
|
-
}>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const isNumeric = (value) => {
|
|
2
|
-
return /^-?\d+(\.\d+)?$/.test(value);
|
|
3
|
-
};
|
|
4
|
-
export const renderDefault = (d) => {
|
|
5
|
-
return /^-?\d+$/.exec(d)
|
|
6
|
-
? d
|
|
7
|
-
: /::text$/.exec(d)
|
|
8
|
-
? d.replace(/::text$/, "")
|
|
9
|
-
: ["true", "false"].includes(d)
|
|
10
|
-
? d
|
|
11
|
-
: `sql\`${d}\``;
|
|
12
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { renderDefault } from "./renderDefault.js";
|
|
3
|
-
describe("renderDefault", () => {
|
|
4
|
-
test("should return numeric values as-is", () => {
|
|
5
|
-
expect(renderDefault("42")).toBe("42");
|
|
6
|
-
expect(renderDefault("0")).toBe("0");
|
|
7
|
-
expect(renderDefault("-123")).toBe("-123");
|
|
8
|
-
});
|
|
9
|
-
test("should remove ::text suffix from text values", () => {
|
|
10
|
-
expect(renderDefault("hello::text")).toBe("hello");
|
|
11
|
-
expect(renderDefault("some string::text")).toBe("some string");
|
|
12
|
-
});
|
|
13
|
-
test("should return boolean literals as-is", () => {
|
|
14
|
-
expect(renderDefault("true")).toBe("true");
|
|
15
|
-
expect(renderDefault("false")).toBe("false");
|
|
16
|
-
});
|
|
17
|
-
test("should wrap other values in sql tag", () => {
|
|
18
|
-
expect(renderDefault("CURRENT_TIMESTAMP")).toBe("sql`CURRENT_TIMESTAMP`");
|
|
19
|
-
expect(renderDefault("uuid_generate_v4()")).toBe("sql`uuid_generate_v4()`");
|
|
20
|
-
expect(renderDefault("now()")).toBe("sql`now()`");
|
|
21
|
-
expect(renderDefault("hello")).toBe("sql`hello`");
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import camelcase from "camelcase";
|
|
2
|
-
import { unindent } from "@casekit/unindent";
|
|
3
|
-
// const renderType = (column: TableColumn) => {
|
|
4
|
-
// if (column.type === "ARRAY") {
|
|
5
|
-
// return (
|
|
6
|
-
// column.elementType +
|
|
7
|
-
// times(column.)
|
|
8
|
-
// .map(() => "[]")
|
|
9
|
-
// .join("")
|
|
10
|
-
// );
|
|
11
|
-
// } else {
|
|
12
|
-
// return column.type;
|
|
13
|
-
// }
|
|
14
|
-
// };
|
|
15
|
-
export const generateModel = (columns) => {
|
|
16
|
-
return unindent `
|
|
17
|
-
import { ModelDefinition } from "@casekit/orm2";
|
|
18
|
-
|
|
19
|
-
export const ${camelcase(columns[0].table)} = {
|
|
20
|
-
table: "${columns[0].table}",
|
|
21
|
-
fields: {
|
|
22
|
-
${columns
|
|
23
|
-
.map((column) => {
|
|
24
|
-
return `${camelcase(column.column)}: {
|
|
25
|
-
name: "${column.column}",
|
|
26
|
-
type: "${column.type}",
|
|
27
|
-
${column.nullable ? "nullable: true," : ""}
|
|
28
|
-
${column.default ? `default: "${column.default}",` : ""}
|
|
29
|
-
}`;
|
|
30
|
-
})
|
|
31
|
-
.join(",\n")}
|
|
32
|
-
},
|
|
33
|
-
} as const satisfies ModelDefinition;
|
|
34
|
-
`;
|
|
35
|
-
};
|
|
File without changes
|
|
File without changes
|