@casekit/orm2-migrate 0.0.0-20250322230249
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 +19 -0
- package/build/index.js +8 -0
- package/build/pull/getTables.d.ts +60 -0
- package/build/pull/getTables.js +56 -0
- package/build/pull/getTables.test.d.ts +1 -0
- package/build/pull/getTables.test.js +298 -0
- package/build/pull.d.ts +16 -0
- package/build/pull.js +4 -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 +37 -0
- package/build/push/createTableSql.test.d.ts +1 -0
- package/build/push/createTableSql.test.js +99 -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 +36 -0
- package/build/render/renderDefault.d.ts +2 -0
- package/build/render/renderDefault.js +12 -0
- package/build/render/renderDefault.test.d.ts +1 -0
- package/build/render/renderDefault.test.js +23 -0
- package/build/render/renderModel.d.ts +2 -0
- package/build/render/renderModel.js +35 -0
- package/package.json +59 -0
package/build/drop.d.ts
ADDED
package/build/drop.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sql } from "@casekit/orm2";
|
|
2
|
+
export const drop = async (db) => {
|
|
3
|
+
const schemas = new Set(Object.values(db.config.models).map((model) => model.schema));
|
|
4
|
+
await db.transact(async (db) => {
|
|
5
|
+
for (const schema of schemas.values()) {
|
|
6
|
+
console.log(` - Dropping schema ${schema}`);
|
|
7
|
+
await db.query `
|
|
8
|
+
DROP SCHEMA IF EXISTS ${sql.ident(schema)} CASCADE;
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
};
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const migrate: {
|
|
2
|
+
drop: (db: import("@casekit/orm2").Orm) => Promise<void>;
|
|
3
|
+
push: (db: import("@casekit/orm2").Orm) => Promise<void>;
|
|
4
|
+
pull: (db: import("@casekit/orm2").Orm, schemas: string[]) => Promise<{
|
|
5
|
+
table: string;
|
|
6
|
+
schema: string;
|
|
7
|
+
column: string;
|
|
8
|
+
type: string;
|
|
9
|
+
nullable: boolean;
|
|
10
|
+
default: string | null;
|
|
11
|
+
ordinalPosition: number | null;
|
|
12
|
+
udtSchema: string;
|
|
13
|
+
udt: string;
|
|
14
|
+
elementType: string | null;
|
|
15
|
+
elementTypeSchema: string | null;
|
|
16
|
+
cardinality: number;
|
|
17
|
+
size: number | null;
|
|
18
|
+
}[]>;
|
|
19
|
+
};
|
package/build/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
table: string;
|
|
18
|
+
schema: string;
|
|
19
|
+
column: string;
|
|
20
|
+
type: string;
|
|
21
|
+
nullable: boolean;
|
|
22
|
+
default: string | null;
|
|
23
|
+
ordinalPosition: number | null;
|
|
24
|
+
udtSchema: string;
|
|
25
|
+
udt: string;
|
|
26
|
+
elementType: string | null;
|
|
27
|
+
elementTypeSchema: string | null;
|
|
28
|
+
cardinality: number;
|
|
29
|
+
size: number | null;
|
|
30
|
+
}, {
|
|
31
|
+
table: string;
|
|
32
|
+
schema: string;
|
|
33
|
+
column: string;
|
|
34
|
+
type: string;
|
|
35
|
+
nullable: boolean;
|
|
36
|
+
default: string | null;
|
|
37
|
+
ordinalPosition: number | null;
|
|
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
|
+
table: string;
|
|
48
|
+
schema: string;
|
|
49
|
+
column: string;
|
|
50
|
+
type: string;
|
|
51
|
+
nullable: boolean;
|
|
52
|
+
default: string | null;
|
|
53
|
+
ordinalPosition: number | null;
|
|
54
|
+
udtSchema: string;
|
|
55
|
+
udt: string;
|
|
56
|
+
elementType: string | null;
|
|
57
|
+
elementTypeSchema: string | null;
|
|
58
|
+
cardinality: number;
|
|
59
|
+
size: number | null;
|
|
60
|
+
}>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sql } from "@casekit/orm2";
|
|
3
|
+
export const TableColumnSchema = z.object({
|
|
4
|
+
schema: z.string(),
|
|
5
|
+
table: z.string(),
|
|
6
|
+
column: z.string(),
|
|
7
|
+
ordinalPosition: z.number().nullable(),
|
|
8
|
+
type: z.string(),
|
|
9
|
+
default: z.string().nullable(),
|
|
10
|
+
nullable: z.boolean(),
|
|
11
|
+
udtSchema: z.string(),
|
|
12
|
+
udt: z.string(),
|
|
13
|
+
elementType: z.string().nullable(),
|
|
14
|
+
elementTypeSchema: z.string().nullable(),
|
|
15
|
+
cardinality: z.number(),
|
|
16
|
+
size: z.number().nullable(),
|
|
17
|
+
});
|
|
18
|
+
export const getTables = (schemas) => sql(TableColumnSchema) `
|
|
19
|
+
SELECT
|
|
20
|
+
c.table_schema AS "schema",
|
|
21
|
+
c.table_name AS "table",
|
|
22
|
+
c.column_name AS "column",
|
|
23
|
+
c.ordinal_position AS "ordinalPosition",
|
|
24
|
+
c.data_type AS "type",
|
|
25
|
+
c.column_default AS "default",
|
|
26
|
+
c.is_nullable = 'YES' AS "nullable",
|
|
27
|
+
c.udt_schema AS "udtSchema",
|
|
28
|
+
c.udt_name AS "udt",
|
|
29
|
+
e.data_type AS "elementType",
|
|
30
|
+
e.udt_schema AS "elementTypeSchema",
|
|
31
|
+
pa.attndims AS cardinality,
|
|
32
|
+
c.character_maximum_length AS "size"
|
|
33
|
+
FROM
|
|
34
|
+
information_schema.tables t
|
|
35
|
+
JOIN information_schema.columns c ON t.table_name = c.table_name
|
|
36
|
+
AND t.table_schema = c.table_schema
|
|
37
|
+
AND t.table_catalog = c.table_catalog
|
|
38
|
+
JOIN pg_catalog.pg_class pc ON pc.relname = c.table_name
|
|
39
|
+
JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace
|
|
40
|
+
AND pn.nspname = c.table_schema
|
|
41
|
+
JOIN pg_catalog.pg_attribute pa ON pa.attrelid = pc.oid
|
|
42
|
+
AND pa.attname = c.column_name
|
|
43
|
+
LEFT JOIN information_schema.element_types e ON c.table_catalog = e.object_catalog
|
|
44
|
+
AND c.table_schema = e.object_schema
|
|
45
|
+
AND c.table_name = e.object_name
|
|
46
|
+
AND e.object_type = 'TABLE'
|
|
47
|
+
AND e.collection_type_identifier = c.dtd_identifier
|
|
48
|
+
AND c.data_type = 'ARRAY'
|
|
49
|
+
WHERE
|
|
50
|
+
t.table_schema IN (${schemas})
|
|
51
|
+
AND t.table_type = 'BASE TABLE'
|
|
52
|
+
AND pa.attnum > 0
|
|
53
|
+
ORDER BY
|
|
54
|
+
t.table_name,
|
|
55
|
+
c.ordinal_position
|
|
56
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { getTables } from "./getTables.js";
|
|
4
|
+
describe("getTables", () => {
|
|
5
|
+
const db = orm({
|
|
6
|
+
schema: "migrate-get-tables-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-tables-test" CASCADE;`;
|
|
17
|
+
await db.query `CREATE SCHEMA IF NOT EXISTS "migrate-get-tables-test";`;
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await db.query `DROP SCHEMA IF EXISTS "migrate-get-tables-test" CASCADE;`;
|
|
21
|
+
});
|
|
22
|
+
test("with an empty database it returns no tables or columns", async () => {
|
|
23
|
+
const statement = getTables(["migrate-get-tables-test"]);
|
|
24
|
+
const result = await db.query(statement);
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
test.for([
|
|
28
|
+
[
|
|
29
|
+
"handles uuid columns",
|
|
30
|
+
sql `
|
|
31
|
+
CREATE TABLE "migrate-get-tables-test"."uuid_test" (
|
|
32
|
+
"uuid_col" UUID DEFAULT gen_random_uuid(),
|
|
33
|
+
"uuid_col_notnull" UUID NOT NULL
|
|
34
|
+
);
|
|
35
|
+
`,
|
|
36
|
+
[
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
type: "uuid",
|
|
39
|
+
default: "gen_random_uuid()",
|
|
40
|
+
nullable: true,
|
|
41
|
+
}),
|
|
42
|
+
expect.objectContaining({
|
|
43
|
+
type: "uuid",
|
|
44
|
+
nullable: false,
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"handles serial columns",
|
|
50
|
+
sql `
|
|
51
|
+
CREATE TABLE "migrate-get-tables-test"."serial_test" (
|
|
52
|
+
"smallserial_col" SMALLSERIAL,
|
|
53
|
+
"serial_col" SERIAL,
|
|
54
|
+
"bigserial_col" BIGSERIAL
|
|
55
|
+
);
|
|
56
|
+
`,
|
|
57
|
+
[
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
type: "smallint",
|
|
60
|
+
default: expect.stringContaining("nextval"),
|
|
61
|
+
nullable: false,
|
|
62
|
+
}),
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
type: "integer",
|
|
65
|
+
default: expect.stringContaining("nextval"),
|
|
66
|
+
nullable: false,
|
|
67
|
+
}),
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
type: "bigint",
|
|
70
|
+
default: expect.stringContaining("nextval"),
|
|
71
|
+
nullable: false,
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"handles boolean columns",
|
|
77
|
+
sql `
|
|
78
|
+
CREATE TABLE "migrate-get-tables-test"."boolean_test" (
|
|
79
|
+
"bool_col" BOOLEAN DEFAULT true
|
|
80
|
+
);
|
|
81
|
+
`,
|
|
82
|
+
[
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
type: "boolean",
|
|
85
|
+
default: "true",
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"handles numeric columns",
|
|
91
|
+
sql `
|
|
92
|
+
CREATE TABLE "migrate-get-tables-test"."numeric_test" (
|
|
93
|
+
"numeric_col" NUMERIC(10,2)
|
|
94
|
+
);
|
|
95
|
+
`,
|
|
96
|
+
[
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
type: "numeric",
|
|
99
|
+
udt: "numeric",
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"handles various numeric and decimal types",
|
|
105
|
+
sql `
|
|
106
|
+
CREATE TABLE "migrate-get-tables-test"."number_test" (
|
|
107
|
+
"smallint_col" SMALLINT,
|
|
108
|
+
"integer_col" INTEGER,
|
|
109
|
+
"bigint_col" BIGINT,
|
|
110
|
+
"decimal_col" DECIMAL(10,2),
|
|
111
|
+
"real_col" REAL,
|
|
112
|
+
"double_col" DOUBLE PRECISION
|
|
113
|
+
);
|
|
114
|
+
`,
|
|
115
|
+
[
|
|
116
|
+
expect.objectContaining({ type: "smallint", udt: "int2" }),
|
|
117
|
+
expect.objectContaining({ type: "integer", udt: "int4" }),
|
|
118
|
+
expect.objectContaining({ type: "bigint", udt: "int8" }),
|
|
119
|
+
expect.objectContaining({ type: "numeric", udt: "numeric" }),
|
|
120
|
+
expect.objectContaining({ type: "real", udt: "float4" }),
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
type: "double precision",
|
|
123
|
+
udt: "float8",
|
|
124
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
"handles character types",
|
|
129
|
+
sql `
|
|
130
|
+
CREATE TABLE "migrate-get-tables-test"."char_test" (
|
|
131
|
+
"char_col" CHAR(10),
|
|
132
|
+
"varchar_col" VARCHAR(50),
|
|
133
|
+
"text_col" TEXT,
|
|
134
|
+
"name_col" NAME
|
|
135
|
+
);
|
|
136
|
+
`,
|
|
137
|
+
[
|
|
138
|
+
expect.objectContaining({ type: "character", size: 10 }),
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
type: "character varying",
|
|
141
|
+
size: 50,
|
|
142
|
+
}),
|
|
143
|
+
expect.objectContaining({ type: "text" }),
|
|
144
|
+
expect.objectContaining({ type: "name" }),
|
|
145
|
+
],
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"handles date and time types",
|
|
149
|
+
sql `
|
|
150
|
+
CREATE TABLE "migrate-get-tables-test"."datetime_test" (
|
|
151
|
+
"date_col" DATE,
|
|
152
|
+
"time_col" TIME,
|
|
153
|
+
"timetz_col" TIME WITH TIME ZONE,
|
|
154
|
+
"timestamp_col" TIMESTAMP,
|
|
155
|
+
"timestamptz_col" TIMESTAMP WITH TIME ZONE,
|
|
156
|
+
"interval_col" INTERVAL
|
|
157
|
+
);
|
|
158
|
+
`,
|
|
159
|
+
[
|
|
160
|
+
expect.objectContaining({ type: "date" }),
|
|
161
|
+
expect.objectContaining({ type: "time without time zone" }),
|
|
162
|
+
expect.objectContaining({ type: "time with time zone" }),
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
type: "timestamp without time zone",
|
|
165
|
+
}),
|
|
166
|
+
expect.objectContaining({ type: "timestamp with time zone" }),
|
|
167
|
+
expect.objectContaining({ type: "interval" }),
|
|
168
|
+
],
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"handles array columns",
|
|
172
|
+
sql `
|
|
173
|
+
CREATE TABLE "migrate-get-tables-test"."array_test" (
|
|
174
|
+
"array_col" INTEGER[]
|
|
175
|
+
);
|
|
176
|
+
`,
|
|
177
|
+
[
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
type: "ARRAY",
|
|
180
|
+
elementType: "integer",
|
|
181
|
+
cardinality: 1,
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
"handles multi-dimensional array columns",
|
|
187
|
+
sql `
|
|
188
|
+
CREATE TABLE "migrate-get-tables-test"."array_test" (
|
|
189
|
+
"multi_dim_array_col" TEXT[][][] NOT NULL DEFAULT ARRAY[ARRAY[ARRAY['a']]]
|
|
190
|
+
);
|
|
191
|
+
`,
|
|
192
|
+
[
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
type: "ARRAY",
|
|
195
|
+
elementType: "text",
|
|
196
|
+
cardinality: 3,
|
|
197
|
+
nullable: false,
|
|
198
|
+
default: "ARRAY[ARRAY[ARRAY['a'::text]]]",
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"handles json columns",
|
|
204
|
+
sql `
|
|
205
|
+
CREATE TABLE "migrate-get-tables-test"."json_test" (
|
|
206
|
+
"json_col" JSON
|
|
207
|
+
);
|
|
208
|
+
`,
|
|
209
|
+
[
|
|
210
|
+
expect.objectContaining({
|
|
211
|
+
type: "json",
|
|
212
|
+
}),
|
|
213
|
+
],
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"handles enum columns",
|
|
217
|
+
sql `
|
|
218
|
+
CREATE TYPE "migrate-get-tables-test"."mood" AS ENUM ('happy', 'sad');
|
|
219
|
+
CREATE TABLE "migrate-get-tables-test"."enum_test" (
|
|
220
|
+
"enum_col" "migrate-get-tables-test"."mood"
|
|
221
|
+
);
|
|
222
|
+
`,
|
|
223
|
+
[
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
type: "USER-DEFINED",
|
|
226
|
+
udtSchema: "migrate-get-tables-test",
|
|
227
|
+
udt: "mood",
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
],
|
|
231
|
+
[
|
|
232
|
+
"handles composite type columns",
|
|
233
|
+
sql `
|
|
234
|
+
CREATE TYPE "migrate-get-tables-test"."complex" AS (x double precision, y double precision);
|
|
235
|
+
CREATE TABLE "migrate-get-tables-test"."composite_test" (
|
|
236
|
+
"complex_col" "migrate-get-tables-test"."complex"
|
|
237
|
+
);
|
|
238
|
+
`,
|
|
239
|
+
[
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
type: "USER-DEFINED",
|
|
242
|
+
udtSchema: "migrate-get-tables-test",
|
|
243
|
+
udt: "complex",
|
|
244
|
+
}),
|
|
245
|
+
],
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"handles currency columns",
|
|
249
|
+
sql `
|
|
250
|
+
CREATE TABLE "migrate-get-tables-test"."currency_test" (
|
|
251
|
+
"money_col" MONEY DEFAULT 42.42
|
|
252
|
+
);
|
|
253
|
+
`,
|
|
254
|
+
[
|
|
255
|
+
expect.objectContaining({
|
|
256
|
+
type: "money",
|
|
257
|
+
default: "42.42",
|
|
258
|
+
}),
|
|
259
|
+
],
|
|
260
|
+
],
|
|
261
|
+
[
|
|
262
|
+
"handles geometric columns",
|
|
263
|
+
sql `
|
|
264
|
+
CREATE TABLE "migrate-get-tables-test"."geometry_test" (
|
|
265
|
+
"point_col" POINT,
|
|
266
|
+
"line_col" LINE,
|
|
267
|
+
"polygon_col" POLYGON
|
|
268
|
+
);
|
|
269
|
+
`,
|
|
270
|
+
[
|
|
271
|
+
expect.objectContaining({ type: "point" }),
|
|
272
|
+
expect.objectContaining({ type: "line" }),
|
|
273
|
+
expect.objectContaining({ type: "polygon" }),
|
|
274
|
+
],
|
|
275
|
+
],
|
|
276
|
+
[
|
|
277
|
+
"handles network address columns",
|
|
278
|
+
sql `
|
|
279
|
+
CREATE TABLE "migrate-get-tables-test"."network_test" (
|
|
280
|
+
"inet_col" INET,
|
|
281
|
+
"cidr_col" CIDR,
|
|
282
|
+
"macaddr_col" MACADDR
|
|
283
|
+
);
|
|
284
|
+
`,
|
|
285
|
+
[
|
|
286
|
+
expect.objectContaining({ type: "inet" }),
|
|
287
|
+
expect.objectContaining({ type: "cidr" }),
|
|
288
|
+
expect.objectContaining({ type: "macaddr" }),
|
|
289
|
+
],
|
|
290
|
+
],
|
|
291
|
+
])("%s", async ([_, query, expected]) => {
|
|
292
|
+
await db.transact(async (db) => {
|
|
293
|
+
await db.query(query);
|
|
294
|
+
const result = await db.query(getTables(["migrate-get-tables-test"]));
|
|
295
|
+
expect(result).toEqual(expected);
|
|
296
|
+
}, { rollback: true });
|
|
297
|
+
});
|
|
298
|
+
});
|
package/build/pull.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Orm } from "@casekit/orm2";
|
|
2
|
+
export declare const pull: (db: Orm, schemas: string[]) => Promise<{
|
|
3
|
+
table: string;
|
|
4
|
+
schema: string;
|
|
5
|
+
column: string;
|
|
6
|
+
type: string;
|
|
7
|
+
nullable: boolean;
|
|
8
|
+
default: string | null;
|
|
9
|
+
ordinalPosition: number | null;
|
|
10
|
+
udtSchema: string;
|
|
11
|
+
udt: string;
|
|
12
|
+
elementType: string | null;
|
|
13
|
+
elementTypeSchema: string | null;
|
|
14
|
+
cardinality: number;
|
|
15
|
+
size: number | null;
|
|
16
|
+
}[]>;
|
package/build/pull.js
ADDED
|
@@ -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/orm2";
|
|
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("uuid-ossp");
|
|
14
|
+
expect(statement.text).toEqual('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";');
|
|
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/orm2";
|
|
2
|
+
import { NormalizedForeignKeyDefinition, NormalizedModelDefinition } from "@casekit/orm2-config";
|
|
3
|
+
export declare const createForeignKeyConstraintSql: (model: NormalizedModelDefinition, fk: NormalizedForeignKeyDefinition) => SQLStatement<import("pg").QueryResultRow>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SQLStatement, sql } from "@casekit/orm2";
|
|
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 {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { orm } from "@casekit/orm2";
|
|
3
|
+
import { unindent } from "@casekit/unindent";
|
|
4
|
+
import { createForeignKeyConstraintSql } from "./createForeignKeyConstraintSql.js";
|
|
5
|
+
describe("createForeignKeyConstraintSql", () => {
|
|
6
|
+
const db = orm({
|
|
7
|
+
schema: "orm",
|
|
8
|
+
models: {
|
|
9
|
+
foo: {
|
|
10
|
+
table: "foo",
|
|
11
|
+
fields: {
|
|
12
|
+
id: { type: "serial", primaryKey: true },
|
|
13
|
+
barId: { column: "bar_id", type: "integer" },
|
|
14
|
+
otherFooId: { column: "other_foo_id", type: "integer" },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
bar: {
|
|
18
|
+
table: "bar",
|
|
19
|
+
fields: {
|
|
20
|
+
id: { type: "serial", primaryKey: true },
|
|
21
|
+
fooId: { column: "foo_id", type: "integer" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
baz: {
|
|
25
|
+
table: "baz",
|
|
26
|
+
fields: {
|
|
27
|
+
id: { type: "serial", primaryKey: true },
|
|
28
|
+
fooId: { column: "foo_id", type: "integer" },
|
|
29
|
+
barId: { column: "bar_id", type: "integer" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const foo = db.config.models["foo"];
|
|
35
|
+
const baz = db.config.models["baz"];
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
await db.connect();
|
|
38
|
+
});
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
await db.close();
|
|
41
|
+
});
|
|
42
|
+
test.for([
|
|
43
|
+
[
|
|
44
|
+
"basic foreign key",
|
|
45
|
+
foo,
|
|
46
|
+
{
|
|
47
|
+
name: "foo_other_foo_id_fk",
|
|
48
|
+
fields: ["otherFooId"],
|
|
49
|
+
columns: ["other_foo_id"],
|
|
50
|
+
references: {
|
|
51
|
+
model: "foo",
|
|
52
|
+
schema: "orm",
|
|
53
|
+
table: "foo",
|
|
54
|
+
fields: ["id"],
|
|
55
|
+
columns: ["id"],
|
|
56
|
+
},
|
|
57
|
+
onUpdate: null,
|
|
58
|
+
onDelete: null,
|
|
59
|
+
},
|
|
60
|
+
unindent `
|
|
61
|
+
ALTER TABLE "orm"."foo"
|
|
62
|
+
ADD CONSTRAINT "foo_other_foo_id_fk" FOREIGN KEY ("other_foo_id") REFERENCES "orm"."foo" ("id")
|
|
63
|
+
`,
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"on update cascade",
|
|
67
|
+
foo,
|
|
68
|
+
{
|
|
69
|
+
name: "foo_other_foo_id_fk",
|
|
70
|
+
fields: ["otherFooId"],
|
|
71
|
+
columns: ["other_foo_id"],
|
|
72
|
+
references: {
|
|
73
|
+
model: "foo",
|
|
74
|
+
schema: "orm",
|
|
75
|
+
table: "foo",
|
|
76
|
+
fields: ["id"],
|
|
77
|
+
columns: ["id"],
|
|
78
|
+
},
|
|
79
|
+
onUpdate: "CASCADE",
|
|
80
|
+
onDelete: null,
|
|
81
|
+
},
|
|
82
|
+
unindent `
|
|
83
|
+
ALTER TABLE "orm"."foo"
|
|
84
|
+
ADD CONSTRAINT "foo_other_foo_id_fk" FOREIGN KEY ("other_foo_id") REFERENCES "orm"."foo" ("id") ON UPDATE CASCADE
|
|
85
|
+
`,
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"on delete set null",
|
|
89
|
+
foo,
|
|
90
|
+
{
|
|
91
|
+
name: "foo_other_foo_id_fk",
|
|
92
|
+
fields: ["otherFooId"],
|
|
93
|
+
columns: ["other_foo_id"],
|
|
94
|
+
references: {
|
|
95
|
+
model: "foo",
|
|
96
|
+
schema: "orm",
|
|
97
|
+
table: "foo",
|
|
98
|
+
fields: ["id"],
|
|
99
|
+
columns: ["id"],
|
|
100
|
+
},
|
|
101
|
+
onUpdate: null,
|
|
102
|
+
onDelete: "SET NULL",
|
|
103
|
+
},
|
|
104
|
+
unindent `
|
|
105
|
+
ALTER TABLE "orm"."foo"
|
|
106
|
+
ADD CONSTRAINT "foo_other_foo_id_fk" FOREIGN KEY ("other_foo_id") REFERENCES "orm"."foo" ("id") ON DELETE SET NULL
|
|
107
|
+
`,
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"multi-column",
|
|
111
|
+
baz,
|
|
112
|
+
{
|
|
113
|
+
name: "baz_foo_id_bar_id_fk",
|
|
114
|
+
fields: ["fooId", "barId"],
|
|
115
|
+
columns: ["foo_id", "bar_id"],
|
|
116
|
+
references: {
|
|
117
|
+
model: "foo",
|
|
118
|
+
schema: "orm",
|
|
119
|
+
table: "foo",
|
|
120
|
+
fields: ["otherFooId", "barId"],
|
|
121
|
+
columns: ["other_foo_id", "bar_id"],
|
|
122
|
+
},
|
|
123
|
+
onUpdate: "RESTRICT",
|
|
124
|
+
onDelete: "SET NULL",
|
|
125
|
+
},
|
|
126
|
+
unindent `
|
|
127
|
+
ALTER TABLE "orm"."baz"
|
|
128
|
+
ADD CONSTRAINT "baz_foo_id_bar_id_fk" FOREIGN KEY ("foo_id", "bar_id") REFERENCES "orm"."foo" ("other_foo_id", "bar_id") ON DELETE SET NULL ON UPDATE RESTRICT
|
|
129
|
+
`,
|
|
130
|
+
],
|
|
131
|
+
])("%s", async ([_, model, fk, expected]) => {
|
|
132
|
+
const statement = createForeignKeyConstraintSql(model, fk);
|
|
133
|
+
expect(statement.pretty).toEqual(expected);
|
|
134
|
+
await db.transact(async (db) => {
|
|
135
|
+
await db.query `CREATE TABLE orm.foo (id SERIAL PRIMARY KEY, bar_id INTEGER, other_foo_id INTEGER)`;
|
|
136
|
+
await db.query `CREATE TABLE orm.bar (id SERIAL PRIMARY KEY, foo_id INTEGER)`;
|
|
137
|
+
await db.query `CREATE TABLE orm.baz (id SERIAL PRIMARY KEY, foo_id INTEGER, bar_id INTEGER)`;
|
|
138
|
+
await db.query `CREATE UNIQUE INDEX foo_other_foo_id_bar_id_key ON orm.foo (other_foo_id, bar_id)`;
|
|
139
|
+
await expect(db.query(statement)).resolves.not.toThrow();
|
|
140
|
+
}, { rollback: true });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createSchemaSql: (schema: string) => import("@casekit/sql").SQLStatement<import("pg").QueryResultRow>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { orm } from "@casekit/orm2";
|
|
3
|
+
import { createSchemaSql } from "./createSchemaSql.js";
|
|
4
|
+
describe("createSchemaSql", () => {
|
|
5
|
+
const db = orm({ models: {} });
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
await db.connect();
|
|
8
|
+
});
|
|
9
|
+
afterAll(async () => {
|
|
10
|
+
await db.close();
|
|
11
|
+
});
|
|
12
|
+
test.for([
|
|
13
|
+
["basic schema name", "test_schema"],
|
|
14
|
+
["schema name with spaces", "test schema"],
|
|
15
|
+
["malicious", "test-schema;drop table"],
|
|
16
|
+
])("%s", async ([, schema]) => {
|
|
17
|
+
const result = createSchemaSql(schema);
|
|
18
|
+
expect(result.text).toBe(`CREATE SCHEMA IF NOT EXISTS "${schema}";`);
|
|
19
|
+
await db.transact(async (db) => {
|
|
20
|
+
await expect(db.query(result)).resolves.not.toThrow();
|
|
21
|
+
}, { rollback: true });
|
|
22
|
+
});
|
|
23
|
+
test("should throw on empty schema name", () => {
|
|
24
|
+
expect(() => createSchemaSql("")).toThrowError("Cannot create schema with empty name");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SQLStatement, sql } from "@casekit/orm2";
|
|
2
|
+
import { arrayToSqlArray } from "#push/arrayToSqlArray.js";
|
|
3
|
+
export const createTableSql = (model) => {
|
|
4
|
+
const fields = Object.values(model.fields);
|
|
5
|
+
const primaryKey = model.primaryKey;
|
|
6
|
+
const statement = sql `CREATE TABLE IF NOT EXISTS ${sql.ident(model.schema)}.${sql.ident(model.table)} (\n`;
|
|
7
|
+
for (const field of fields) {
|
|
8
|
+
statement.append ` ${sql.ident(field.column)} `;
|
|
9
|
+
// dangerously appending raw SQL here, but we need to,
|
|
10
|
+
// and this only comes from user-defined schemas, so should be ok
|
|
11
|
+
statement.push(new SQLStatement(field.type));
|
|
12
|
+
if (!field.nullable)
|
|
13
|
+
statement.append ` NOT NULL`;
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- let's do this check to be safe
|
|
15
|
+
if (field.default !== null || field.default === undefined) {
|
|
16
|
+
statement.append ` DEFAULT `;
|
|
17
|
+
if (field.default instanceof SQLStatement) {
|
|
18
|
+
statement.push(field.default);
|
|
19
|
+
}
|
|
20
|
+
else if (typeof field.default === "string") {
|
|
21
|
+
statement.push(sql.literal(field.default));
|
|
22
|
+
}
|
|
23
|
+
else if (Array.isArray(field.default)) {
|
|
24
|
+
statement.push(sql.literal(arrayToSqlArray(field.default)));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
statement.push(sql.literal(JSON.stringify(field.default)));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
statement.append `,\n`;
|
|
31
|
+
}
|
|
32
|
+
if (primaryKey.length > 0) {
|
|
33
|
+
statement.append ` PRIMARY KEY (${sql.join(primaryKey.map((pk) => sql.ident(pk.column)), ", ")})`;
|
|
34
|
+
}
|
|
35
|
+
statement.append `\n);`;
|
|
36
|
+
return statement;
|
|
37
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { normalizeConfig } from "@casekit/orm2-config";
|
|
4
|
+
import { config } from "@casekit/orm2-fixtures";
|
|
5
|
+
import { unindent } from "@casekit/unindent";
|
|
6
|
+
import { createTableSql } from "./createTableSql.js";
|
|
7
|
+
describe("createTableSql", () => {
|
|
8
|
+
const db = orm(config);
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await db.connect();
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await db.close();
|
|
14
|
+
});
|
|
15
|
+
test.for([
|
|
16
|
+
[
|
|
17
|
+
"integer",
|
|
18
|
+
{},
|
|
19
|
+
unindent `
|
|
20
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
21
|
+
"id" serial NOT NULL,
|
|
22
|
+
"value" integer NOT NULL,
|
|
23
|
+
PRIMARY KEY ("id")
|
|
24
|
+
);
|
|
25
|
+
`,
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"text",
|
|
29
|
+
{ nullable: true },
|
|
30
|
+
unindent `
|
|
31
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
32
|
+
"id" serial NOT NULL,
|
|
33
|
+
"value" text,
|
|
34
|
+
PRIMARY KEY ("id")
|
|
35
|
+
);
|
|
36
|
+
`,
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"integer",
|
|
40
|
+
{ unique: true, default: 1312 },
|
|
41
|
+
unindent `
|
|
42
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
43
|
+
"id" serial NOT NULL,
|
|
44
|
+
"value" integer NOT NULL DEFAULT '1312',
|
|
45
|
+
PRIMARY KEY ("id")
|
|
46
|
+
);
|
|
47
|
+
`,
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"text",
|
|
51
|
+
{ unique: true, default: "foo" },
|
|
52
|
+
unindent `
|
|
53
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
54
|
+
"id" serial NOT NULL,
|
|
55
|
+
"value" text NOT NULL DEFAULT 'foo',
|
|
56
|
+
PRIMARY KEY ("id")
|
|
57
|
+
);
|
|
58
|
+
`,
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"uuid",
|
|
62
|
+
{ default: sql `uuid_generate_v4()` },
|
|
63
|
+
unindent `
|
|
64
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
65
|
+
"id" serial NOT NULL,
|
|
66
|
+
"value" uuid NOT NULL DEFAULT uuid_generate_v4 (),
|
|
67
|
+
PRIMARY KEY ("id")
|
|
68
|
+
);
|
|
69
|
+
`,
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"text[][][]",
|
|
73
|
+
{ default: [[[1, 2, 3]]] },
|
|
74
|
+
unindent `
|
|
75
|
+
CREATE TABLE IF NOT EXISTS "public"."foo" (
|
|
76
|
+
"id" serial NOT NULL,
|
|
77
|
+
"value" text[] [] [] NOT NULL DEFAULT '{ { { 1, 2, 3 } } }',
|
|
78
|
+
PRIMARY KEY ("id")
|
|
79
|
+
);
|
|
80
|
+
`,
|
|
81
|
+
],
|
|
82
|
+
])("%s %s", async ([type, options, expected]) => {
|
|
83
|
+
const config = normalizeConfig({
|
|
84
|
+
models: {
|
|
85
|
+
foo: {
|
|
86
|
+
fields: {
|
|
87
|
+
id: { type: "serial", primaryKey: true },
|
|
88
|
+
value: { type: type, ...options },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const statement = createTableSql(config.models["foo"]);
|
|
94
|
+
expect(statement.pretty).toEqual(expected);
|
|
95
|
+
await db.transact(async (db) => {
|
|
96
|
+
await expect(db.query(statement)).resolves.not.toThrow();
|
|
97
|
+
}, { rollback: true });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { NormalizedModelDefinition, NormalizedUniqueConstraintDefinition } from "@casekit/orm2-config";
|
|
2
|
+
export declare const createUniqueConstraintSql: (model: NormalizedModelDefinition, constraint: NormalizedUniqueConstraintDefinition) => import("@casekit/orm2").SQLStatement<import("pg").QueryResultRow>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sql } from "@casekit/orm2";
|
|
2
|
+
export const createUniqueConstraintSql = (model, constraint) => {
|
|
3
|
+
const statement = sql `
|
|
4
|
+
CREATE UNIQUE INDEX ${sql.ident(constraint.name)}
|
|
5
|
+
ON ${sql.ident(model.schema)}.${sql.ident(model.table)}
|
|
6
|
+
(${sql.join(constraint.columns.map(sql.ident), ", ")})
|
|
7
|
+
`;
|
|
8
|
+
if (constraint.nullsNotDistinct) {
|
|
9
|
+
statement.append `
|
|
10
|
+
NULLS NOT DISTINCT`;
|
|
11
|
+
}
|
|
12
|
+
if (constraint.where) {
|
|
13
|
+
statement.append `
|
|
14
|
+
WHERE ${constraint.where}`;
|
|
15
|
+
}
|
|
16
|
+
return statement;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
2
|
+
import { orm, sql } from "@casekit/orm2";
|
|
3
|
+
import { unindent } from "@casekit/unindent";
|
|
4
|
+
import { createUniqueConstraintSql } from "./createUniqueConstraintSql.js";
|
|
5
|
+
describe("createUniqueConstraintSql", () => {
|
|
6
|
+
const db = orm({
|
|
7
|
+
schema: "orm",
|
|
8
|
+
models: {
|
|
9
|
+
foo: {
|
|
10
|
+
table: "foo",
|
|
11
|
+
fields: {
|
|
12
|
+
id: { type: "serial", primaryKey: true },
|
|
13
|
+
fooValue: { column: "foo_value", type: "text" },
|
|
14
|
+
barValue: { column: "bar_value", type: "integer" },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const foo = db.config.models["foo"];
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
await db.connect();
|
|
22
|
+
});
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await db.close();
|
|
25
|
+
});
|
|
26
|
+
test.for([
|
|
27
|
+
[
|
|
28
|
+
"basic unique constraint",
|
|
29
|
+
{
|
|
30
|
+
name: "foo_foo_value_key",
|
|
31
|
+
fields: ["fooValue"],
|
|
32
|
+
columns: ["foo_value"],
|
|
33
|
+
nullsNotDistinct: false,
|
|
34
|
+
where: null,
|
|
35
|
+
},
|
|
36
|
+
unindent `
|
|
37
|
+
CREATE UNIQUE INDEX "foo_foo_value_key" ON "orm"."foo" ("foo_value")
|
|
38
|
+
`,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"nulls not distinct",
|
|
42
|
+
{
|
|
43
|
+
name: "foo_foo_value_key",
|
|
44
|
+
fields: ["fooValue"],
|
|
45
|
+
columns: ["foo_value"],
|
|
46
|
+
nullsNotDistinct: true,
|
|
47
|
+
where: null,
|
|
48
|
+
},
|
|
49
|
+
unindent `
|
|
50
|
+
CREATE UNIQUE INDEX "foo_foo_value_key" ON "orm"."foo" ("foo_value") NULLS NOT DISTINCT
|
|
51
|
+
`,
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"where clause",
|
|
55
|
+
{
|
|
56
|
+
name: "foo_foo_value_key",
|
|
57
|
+
fields: ["fooValue"],
|
|
58
|
+
columns: ["foo_value"],
|
|
59
|
+
nullsNotDistinct: false,
|
|
60
|
+
where: sql `bar_value > 0`,
|
|
61
|
+
},
|
|
62
|
+
unindent `
|
|
63
|
+
CREATE UNIQUE INDEX "foo_foo_value_key" ON "orm"."foo" ("foo_value")
|
|
64
|
+
WHERE
|
|
65
|
+
bar_value > 0
|
|
66
|
+
`,
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"multi-column",
|
|
70
|
+
{
|
|
71
|
+
name: "foo_foo_value_bar_value_key",
|
|
72
|
+
fields: ["fooValue", "barValue"],
|
|
73
|
+
columns: ["foo_value", "bar_value"],
|
|
74
|
+
nullsNotDistinct: false,
|
|
75
|
+
where: null,
|
|
76
|
+
},
|
|
77
|
+
unindent `
|
|
78
|
+
CREATE UNIQUE INDEX "foo_foo_value_bar_value_key" ON "orm"."foo" ("foo_value", "bar_value")
|
|
79
|
+
`,
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
"multi-column with nulls not distinct and where",
|
|
83
|
+
{
|
|
84
|
+
name: "foo_foo_value_bar_value_key",
|
|
85
|
+
fields: ["fooValue", "barValue"],
|
|
86
|
+
columns: ["foo_value", "bar_value"],
|
|
87
|
+
nullsNotDistinct: true,
|
|
88
|
+
where: sql `bar_value > 0 AND foo_value IS NOT NULL`,
|
|
89
|
+
},
|
|
90
|
+
unindent `
|
|
91
|
+
CREATE UNIQUE INDEX "foo_foo_value_bar_value_key" ON "orm"."foo" ("foo_value", "bar_value") NULLS NOT DISTINCT
|
|
92
|
+
WHERE
|
|
93
|
+
bar_value > 0
|
|
94
|
+
AND foo_value IS NOT NULL
|
|
95
|
+
`,
|
|
96
|
+
],
|
|
97
|
+
])("%s", async ([_, constraint, expected]) => {
|
|
98
|
+
const statement = createUniqueConstraintSql(foo, constraint);
|
|
99
|
+
expect(statement.pretty).toEqual(expected);
|
|
100
|
+
await db.transact(async (db) => {
|
|
101
|
+
await db.query `CREATE TABLE orm.foo (id SERIAL PRIMARY KEY, foo_value TEXT, bar_value INTEGER)`;
|
|
102
|
+
await expect(db.query(statement)).resolves.not.toThrow();
|
|
103
|
+
}, { rollback: true });
|
|
104
|
+
});
|
|
105
|
+
});
|
package/build/push.d.ts
ADDED
package/build/push.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { toSentence } from "@casekit/toolbox";
|
|
2
|
+
import { createExtensionsSql } from "#push/createExtensionSql.js";
|
|
3
|
+
import { createForeignKeyConstraintSql } from "#push/createForeignKeyConstraintSql.js";
|
|
4
|
+
import { createSchemaSql } from "#push/createSchemaSql.js";
|
|
5
|
+
import { createTableSql } from "#push/createTableSql.js";
|
|
6
|
+
import { createUniqueConstraintSql } from "#push/createUniqueConstraintSql.js";
|
|
7
|
+
export const push = async (db) => {
|
|
8
|
+
const schemas = new Set(Object.values(db.config.models).map((model) => model.schema));
|
|
9
|
+
console.log(`Pushing schemas ${toSentence(schemas)} to database`);
|
|
10
|
+
await db.transact(async (db) => {
|
|
11
|
+
for (const extension of db.config.extensions) {
|
|
12
|
+
console.log(` - Creating extension "${extension}"`);
|
|
13
|
+
await db.query(createExtensionsSql(extension));
|
|
14
|
+
}
|
|
15
|
+
for (const schema of schemas.values()) {
|
|
16
|
+
console.log(` - Creating schema "${schema}"`);
|
|
17
|
+
await db.query(createSchemaSql(schema));
|
|
18
|
+
}
|
|
19
|
+
for (const model of Object.values(db.config.models)) {
|
|
20
|
+
console.log(` - Creating table "${model.schema}"."${model.table}"`);
|
|
21
|
+
await db.query(createTableSql(model));
|
|
22
|
+
}
|
|
23
|
+
for (const model of Object.values(db.config.models)) {
|
|
24
|
+
for (const fk of model.foreignKeys) {
|
|
25
|
+
console.log(` - Creating foreign key constraint "${fk.name}" ON "${model.table}"`);
|
|
26
|
+
await db.query(createForeignKeyConstraintSql(model, fk));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const model of Object.values(db.config.models)) {
|
|
30
|
+
for (const constraint of model.uniqueConstraints) {
|
|
31
|
+
console.log(` - Creating unique constraint "${constraint.name}" ON "${model.schema}"."${model.table}"`);
|
|
32
|
+
await db.query(createUniqueConstraintSql(model, constraint));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@casekit/orm2-migrate",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "0.0.0-20250322230249",
|
|
5
|
+
"author": "",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"camelcase": "^8.0.0",
|
|
8
|
+
"@casekit/orm2": "0.0.0-20250322230249",
|
|
9
|
+
"@casekit/orm2-schema": "0.0.0-20250322230249",
|
|
10
|
+
"@casekit/toolbox": "0.0.0-20250322230249",
|
|
11
|
+
"@casekit/orm2-config": "0.0.0-20250322230249",
|
|
12
|
+
"@casekit/sql": "0.0.0-20250322230249"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@casekit/unindent": "^1.0.5",
|
|
16
|
+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
|
17
|
+
"@types/node": "^22.13.11",
|
|
18
|
+
"@types/pg": "^8.11.11",
|
|
19
|
+
"@types/uuid": "^10.0.0",
|
|
20
|
+
"@vitest/coverage-v8": "^3.0.9",
|
|
21
|
+
"dotenv": "^16.4.7",
|
|
22
|
+
"prettier": "^3.5.3",
|
|
23
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
24
|
+
"typescript": "^5.8.2",
|
|
25
|
+
"uuid": "^11.1.0",
|
|
26
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
27
|
+
"vitest": "^3.0.9",
|
|
28
|
+
"@casekit/orm2-fixtures": "0.0.0-20250322230249",
|
|
29
|
+
"@casekit/tsconfig": "0.0.0-20250322230249",
|
|
30
|
+
"@casekit/prettier-config": "0.0.0-20250322230249"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./build/index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"/build"
|
|
37
|
+
],
|
|
38
|
+
"imports": {
|
|
39
|
+
"#*": "./build/*"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [],
|
|
42
|
+
"license": "ISC",
|
|
43
|
+
"main": "index.js",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"pg": "^8.13.1",
|
|
46
|
+
"zod": "^3.24.2"
|
|
47
|
+
},
|
|
48
|
+
"prettier": "@casekit/prettier-config",
|
|
49
|
+
"type": "module",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "rm -rf ./build && tsc",
|
|
52
|
+
"format:check": "prettier --check .",
|
|
53
|
+
"format": "prettier --write .",
|
|
54
|
+
"lint": "eslint src --max-warnings 0",
|
|
55
|
+
"test": "vitest --run --typecheck --coverage",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
}
|
|
59
|
+
}
|