@casekit/orm-migrate 0.0.1-release-candidate
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/drop.d.ts +2 -0
- package/build/drop.js +12 -0
- package/build/index.d.ts +7 -0
- package/build/index.js +8 -0
- package/build/pull/getColumns.d.ts +34 -0
- package/build/pull/getColumns.js +83 -0
- package/build/pull/getColumns.test.d.ts +1 -0
- package/build/pull/getColumns.test.js +320 -0
- package/build/pull/getForeignKeys.d.ts +18 -0
- package/build/pull/getForeignKeys.js +72 -0
- package/build/pull/getForeignKeys.test.d.ts +1 -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 +14 -0
- package/build/pull.js +56 -0
- package/build/push/arrayToSqlArray.d.ts +1 -0
- package/build/push/arrayToSqlArray.js +6 -0
- package/build/push/arrayToSqlArray.test.d.ts +1 -0
- package/build/push/arrayToSqlArray.test.js +29 -0
- package/build/push/createExtensionSql.d.ts +2 -0
- package/build/push/createExtensionSql.js +4 -0
- package/build/push/createExtensionSql.test.d.ts +1 -0
- package/build/push/createExtensionSql.test.js +20 -0
- package/build/push/createForeignKeyConstraintSql.d.ts +3 -0
- package/build/push/createForeignKeyConstraintSql.js +17 -0
- package/build/push/createForeignKeyConstraintSql.test.d.ts +1 -0
- package/build/push/createForeignKeyConstraintSql.test.js +142 -0
- package/build/push/createSchemaSql.d.ts +1 -0
- package/build/push/createSchemaSql.js +7 -0
- package/build/push/createSchemaSql.test.d.ts +1 -0
- package/build/push/createSchemaSql.test.js +26 -0
- package/build/push/createTableSql.d.ts +3 -0
- package/build/push/createTableSql.js +38 -0
- package/build/push/createTableSql.test.d.ts +1 -0
- package/build/push/createTableSql.test.js +110 -0
- package/build/push/createUniqueConstraintSql.d.ts +2 -0
- package/build/push/createUniqueConstraintSql.js +17 -0
- package/build/push/createUniqueConstraintSql.test.d.ts +1 -0
- package/build/push/createUniqueConstraintSql.test.js +105 -0
- package/build/push.d.ts +2 -0
- package/build/push.js +38 -0
- package/package.json +59 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sql } from "@casekit/orm";
|
|
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/orm";
|
|
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/orm").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/orm";
|
|
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/orm";
|
|
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
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Orm } from "@casekit/orm";
|
|
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 = {
|
|
7
|
+
schema: string;
|
|
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
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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";
|
|
7
|
+
export const pull = async (db, schemas) => {
|
|
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"]);
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const arrayToSqlArray: (values: unknown[]) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { arrayToSqlArray } from "./arrayToSqlArray.js";
|
|
3
|
+
describe("arrayToSqlArray", () => {
|
|
4
|
+
test("converts a flat array to SQL array format", () => {
|
|
5
|
+
const input = [1, 2, 3];
|
|
6
|
+
const output = arrayToSqlArray(input);
|
|
7
|
+
expect(output).toBe("{ 1, 2, 3 }");
|
|
8
|
+
});
|
|
9
|
+
test("converts a nested array to SQL array format", () => {
|
|
10
|
+
const input = [1, [2, 3], 4];
|
|
11
|
+
const output = arrayToSqlArray(input);
|
|
12
|
+
expect(output).toBe("{ 1, { 2, 3 }, 4 }");
|
|
13
|
+
});
|
|
14
|
+
test("converts an array with strings to SQL array format", () => {
|
|
15
|
+
const input = ["a", "b", "c"];
|
|
16
|
+
const output = arrayToSqlArray(input);
|
|
17
|
+
expect(output).toBe('{ "a", "b", "c" }');
|
|
18
|
+
});
|
|
19
|
+
test("converts a mixed array to SQL array format", () => {
|
|
20
|
+
const input = [1, "b", [2, "c"]];
|
|
21
|
+
const output = arrayToSqlArray(input);
|
|
22
|
+
expect(output).toBe('{ 1, "b", { 2, "c" } }');
|
|
23
|
+
});
|
|
24
|
+
test("converts an empty array to SQL array format", () => {
|
|
25
|
+
const input = [];
|
|
26
|
+
const output = arrayToSqlArray(input);
|
|
27
|
+
expect(output).toBe("{ }");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { orm } from "@casekit/orm";
|
|
3
|
+
import { createExtensionsSql } from "./createExtensionSql.js";
|
|
4
|
+
describe("createExtensionsSql", () => {
|
|
5
|
+
const db = orm({ models: {} });
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
await db.connect();
|
|
8
|
+
});
|
|
9
|
+
afterAll(async () => {
|
|
10
|
+
await db.close();
|
|
11
|
+
});
|
|
12
|
+
test("creates SQL statement for creating an extension", async () => {
|
|
13
|
+
const statement = createExtensionsSql("foo", "uuid-ossp");
|
|
14
|
+
expect(statement.text).toEqual('CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA "foo";');
|
|
15
|
+
expect(statement.values).toEqual([]);
|
|
16
|
+
await db.transact(async (db) => {
|
|
17
|
+
await expect(db.query(statement)).resolves.not.toThrow();
|
|
18
|
+
}, { rollback: true });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SQLStatement } from "@casekit/orm";
|
|
2
|
+
import { NormalizedForeignKeyDefinition, NormalizedModelDefinition } from "@casekit/orm-config";
|
|
3
|
+
export declare const createForeignKeyConstraintSql: (model: NormalizedModelDefinition, fk: NormalizedForeignKeyDefinition) => SQLStatement<import("pg").QueryResultRow>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SQLStatement, sql } from "@casekit/orm";
|
|
2
|
+
export const createForeignKeyConstraintSql = (model, fk) => {
|
|
3
|
+
const statement = sql `
|
|
4
|
+
ALTER TABLE ${sql.ident(model.schema)}.${sql.ident(model.table)}
|
|
5
|
+
ADD CONSTRAINT ${sql.ident(fk.name)}
|
|
6
|
+
FOREIGN KEY (${sql.join(fk.columns.map(sql.ident), ", ")})
|
|
7
|
+
REFERENCES ${sql.ident(fk.references.schema)}.${sql.ident(fk.references.table)} (${sql.join(fk.references.columns.map(sql.ident), ", ")})`;
|
|
8
|
+
if (fk.onDelete) {
|
|
9
|
+
statement.append `
|
|
10
|
+
ON DELETE ${new SQLStatement(fk.onDelete)}`;
|
|
11
|
+
}
|
|
12
|
+
if (fk.onUpdate) {
|
|
13
|
+
statement.append `
|
|
14
|
+
ON UPDATE ${new SQLStatement(fk.onUpdate)}`;
|
|
15
|
+
}
|
|
16
|
+
return statement;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|