Skip to content

Commit 51ab749

Browse files
authored
feat: create table with constraints (#19828)
1 parent 4db76dd commit 51ab749

File tree

7 files changed

+170
-60
lines changed

7 files changed

+170
-60
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Kysely, sql } from 'kysely';
2+
3+
export async function up(db: Kysely<any>): Promise<void> {
4+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_originalfilename_trigram","sql":"CREATE INDEX \\"idx_originalfilename_trigram\\" ON \\"assets\\" USING gin (f_unaccent(\\"originalFileName\\") gin_trgm_ops);"}'::jsonb WHERE "name" = 'index_idx_originalfilename_trigram';`.execute(db);
5+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_local_date_time_month","sql":"CREATE INDEX \\"idx_local_date_time_month\\" ON \\"assets\\" ((date_trunc(''MONTH''::text, (\\"localDateTime\\" AT TIME ZONE ''UTC''::text)) AT TIME ZONE ''UTC''::text));"}'::jsonb WHERE "name" = 'index_idx_local_date_time_month';`.execute(db);
6+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_local_date_time","sql":"CREATE INDEX \\"idx_local_date_time\\" ON \\"assets\\" (((\\"localDateTime\\" at time zone ''UTC'')::date));"}'::jsonb WHERE "name" = 'index_idx_local_date_time';`.execute(db);
7+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"UQ_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_library_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"libraryId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NOT NULL);"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_library_checksum';`.execute(db);
8+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"UQ_assets_owner_checksum","sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NULL);"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_checksum';`.execute(db);
9+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"IDX_activity_like","sql":"CREATE UNIQUE INDEX \\"IDX_activity_like\\" ON \\"activity\\" (\\"assetId\\", \\"userId\\", \\"albumId\\") WHERE (\\"isLiked\\" = true);"}'::jsonb WHERE "name" = 'index_IDX_activity_like';`.execute(db);
10+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"face_index","sql":"CREATE INDEX \\"face_index\\" ON \\"face_search\\" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16);"}'::jsonb WHERE "name" = 'index_face_index';`.execute(db);
11+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"IDX_geodata_gist_earthcoord","sql":"CREATE INDEX \\"IDX_geodata_gist_earthcoord\\" ON \\"geodata_places\\" (ll_to_earth_public(latitude, longitude));"}'::jsonb WHERE "name" = 'index_IDX_geodata_gist_earthcoord';`.execute(db);
12+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_geodata_places_name","sql":"CREATE INDEX \\"idx_geodata_places_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"name\\") gin_trgm_ops);"}'::jsonb WHERE "name" = 'index_idx_geodata_places_name';`.execute(db);
13+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_geodata_places_admin2_name","sql":"CREATE INDEX \\"idx_geodata_places_admin2_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"admin2Name\\") gin_trgm_ops);"}'::jsonb WHERE "name" = 'index_idx_geodata_places_admin2_name';`.execute(db);
14+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_geodata_places_admin1_name","sql":"CREATE INDEX \\"idx_geodata_places_admin1_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"admin1Name\\") gin_trgm_ops);"}'::jsonb WHERE "name" = 'index_idx_geodata_places_admin1_name';`.execute(db);
15+
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"idx_geodata_places_alternate_names","sql":"CREATE INDEX \\"idx_geodata_places_alternate_names\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"alternateNames\\") gin_trgm_ops);"}'::jsonb WHERE "name" = 'index_idx_geodata_places_alternate_names';`.execute(db);
16+
}
17+
18+
export async function down(db: Kysely<any>): Promise<void> {
19+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_originalfilename_trigram\\" ON \\"assets\\" USING gin (f_unaccent(\\"originalFileName\\") gin_trgm_ops)","name":"idx_originalfilename_trigram","type":"index"}'::jsonb WHERE "name" = 'index_idx_originalfilename_trigram';`.execute(db);
20+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_local_date_time_month\\" ON \\"assets\\" ((date_trunc(''MONTH''::text, (\\"localDateTime\\" AT TIME ZONE ''UTC''::text)) AT TIME ZONE ''UTC''::text))","name":"idx_local_date_time_month","type":"index"}'::jsonb WHERE "name" = 'index_idx_local_date_time_month';`.execute(db);
21+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_local_date_time\\" ON \\"assets\\" (((\\"localDateTime\\" at time zone ''UTC'')::date))","name":"idx_local_date_time","type":"index"}'::jsonb WHERE "name" = 'index_idx_local_date_time';`.execute(db);
22+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_library_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"libraryId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NOT NULL)","name":"UQ_assets_owner_library_checksum","type":"index"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_library_checksum';`.execute(db);
23+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NULL)","name":"UQ_assets_owner_checksum","type":"index"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_checksum';`.execute(db);
24+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE UNIQUE INDEX \\"IDX_activity_like\\" ON \\"activity\\" (\\"assetId\\", \\"userId\\", \\"albumId\\") WHERE (\\"isLiked\\" = true)","name":"IDX_activity_like","type":"index"}'::jsonb WHERE "name" = 'index_IDX_activity_like';`.execute(db);
25+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"face_index\\" ON \\"face_search\\" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)","name":"face_index","type":"index"}'::jsonb WHERE "name" = 'index_face_index';`.execute(db);
26+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"IDX_geodata_gist_earthcoord\\" ON \\"geodata_places\\" (ll_to_earth_public(latitude, longitude))","name":"IDX_geodata_gist_earthcoord","type":"index"}'::jsonb WHERE "name" = 'index_IDX_geodata_gist_earthcoord';`.execute(db);
27+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_geodata_places_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"name\\") gin_trgm_ops)","name":"idx_geodata_places_name","type":"index"}'::jsonb WHERE "name" = 'index_idx_geodata_places_name';`.execute(db);
28+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_geodata_places_admin2_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"admin2Name\\") gin_trgm_ops)","name":"idx_geodata_places_admin2_name","type":"index"}'::jsonb WHERE "name" = 'index_idx_geodata_places_admin2_name';`.execute(db);
29+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_geodata_places_admin1_name\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"admin1Name\\") gin_trgm_ops)","name":"idx_geodata_places_admin1_name","type":"index"}'::jsonb WHERE "name" = 'index_idx_geodata_places_admin1_name';`.execute(db);
30+
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE INDEX \\"idx_geodata_places_alternate_names\\" ON \\"geodata_places\\" USING gin (f_unaccent(\\"alternateNames\\") gin_trgm_ops)","name":"idx_geodata_places_alternate_names","type":"index"}'::jsonb WHERE "name" = 'index_idx_geodata_places_alternate_names';`.execute(db);
31+
}

server/src/sql-tools/comparers/table.comparer.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,27 @@ import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer';
55
import { compare } from 'src/sql-tools/helpers';
66
import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types';
77

8-
const newTable = (name: string) => ({
9-
name,
10-
columns: [],
11-
indexes: [],
12-
constraints: [],
13-
triggers: [],
14-
synchronize: true,
15-
});
16-
178
export const compareTables: Comparer<DatabaseTable> = {
189
onMissing: (source) => [
1910
{
2011
type: 'TableCreate',
2112
table: source,
2213
reason: Reason.MissingInTarget,
2314
},
24-
// TODO merge constraints into table create record when possible
25-
...compareTable(source, newTable(source.name), { columns: false }),
2615
],
2716
onExtra: (target) => [
28-
...compareTable(newTable(target.name), target, { columns: false }),
2917
{
3018
type: 'TableDrop',
3119
tableName: target.name,
3220
reason: Reason.MissingInSource,
3321
},
3422
],
35-
onCompare: (source, target) => compareTable(source, target, { columns: true }),
23+
onCompare: (source, target) => compareTable(source, target),
3624
};
3725

38-
const compareTable = (source: DatabaseTable, target: DatabaseTable, options: { columns?: boolean }): SchemaDiff[] => {
26+
const compareTable = (source: DatabaseTable, target: DatabaseTable): SchemaDiff[] => {
3927
return [
40-
...(options.columns ? compare(source.columns, target.columns, {}, compareColumns) : []),
28+
...compare(source.columns, target.columns, {}, compareColumns),
4129
...compare(source.indexes, target.indexes, {}, compareIndexes),
4230
...compare(source.constraints, target.constraints, {}, compareConstraints),
4331
...compare(source.triggers, target.triggers, {}, compareTriggers),

server/src/sql-tools/transformers/constraint.transformer.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,43 @@ export const transformConstraints: SqlTransformer = (ctx, item) => {
2020
const withAction = (constraint: { onDelete?: ActionType; onUpdate?: ActionType }) =>
2121
` ON UPDATE ${constraint.onUpdate ?? ActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? ActionType.NO_ACTION}`;
2222

23-
export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => {
24-
const base = `ALTER TABLE "${constraint.tableName}" ADD CONSTRAINT "${constraint.name}"`;
23+
export const asConstraintBody = (constraint: DatabaseConstraint): string => {
24+
const base = `CONSTRAINT "${constraint.name}"`;
25+
2526
switch (constraint.type) {
2627
case ConstraintType.PRIMARY_KEY: {
2728
const columnNames = asColumnList(constraint.columnNames);
28-
return `${base} PRIMARY KEY (${columnNames});`;
29+
return `${base} PRIMARY KEY (${columnNames})`;
2930
}
3031

3132
case ConstraintType.FOREIGN_KEY: {
3233
const columnNames = asColumnList(constraint.columnNames);
3334
const referenceColumnNames = asColumnList(constraint.referenceColumnNames);
3435
return (
3536
`${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` +
36-
withAction(constraint) +
37-
';'
37+
withAction(constraint)
3838
);
3939
}
4040

4141
case ConstraintType.UNIQUE: {
4242
const columnNames = asColumnList(constraint.columnNames);
43-
return `${base} UNIQUE (${columnNames});`;
43+
return `${base} UNIQUE (${columnNames})`;
4444
}
4545

4646
case ConstraintType.CHECK: {
47-
return `${base} CHECK (${constraint.expression});`;
47+
return `${base} CHECK (${constraint.expression})`;
4848
}
4949

5050
default: {
51-
return [];
51+
throw new Error(`Unknown constraint type: ${(constraint as any).type}`);
5252
}
5353
}
5454
};
5555

56+
export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => {
57+
return `ALTER TABLE "${constraint.tableName}" ADD ${asConstraintBody(constraint)};`;
58+
};
59+
5660
export const asConstraintDrop = (tableName: string, constraintName: string): string => {
5761
return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`;
5862
};

server/src/sql-tools/transformers/index.transformer.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe(transformIndexes.name, () => {
1919
},
2020
reason: 'unknown',
2121
}),
22-
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("column1")');
22+
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("column1");');
2323
});
2424

2525
it('should create an unique index', () => {
@@ -35,7 +35,7 @@ describe(transformIndexes.name, () => {
3535
},
3636
reason: 'unknown',
3737
}),
38-
).toEqual('CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1")');
38+
).toEqual('CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1");');
3939
});
4040

4141
it('should create an index with a custom expression', () => {
@@ -51,7 +51,7 @@ describe(transformIndexes.name, () => {
5151
},
5252
reason: 'unknown',
5353
}),
54-
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL)');
54+
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL);');
5555
});
5656

5757
it('should create an index with a where clause', () => {
@@ -68,7 +68,7 @@ describe(transformIndexes.name, () => {
6868
},
6969
reason: 'unknown',
7070
}),
71-
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL)');
71+
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL);');
7272
});
7373

7474
it('should create an index with a custom expression', () => {
@@ -85,7 +85,7 @@ describe(transformIndexes.name, () => {
8585
},
8686
reason: 'unknown',
8787
}),
88-
).toEqual('CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL)');
88+
).toEqual('CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL);');
8989
});
9090
});
9191

server/src/sql-tools/transformers/index.transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const asIndexCreate = (index: DatabaseIndex): string => {
4848
sql += ` WHERE ${index.where}`;
4949
}
5050

51-
return sql;
51+
return sql + ';';
5252
};
5353

5454
export const asIndexDrop = (indexName: string): string => {

server/src/sql-tools/transformers/table.transformer.spec.ts

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,69 @@
11
import { BaseContext } from 'src/sql-tools/contexts/base-context';
22
import { transformTables } from 'src/sql-tools/transformers/table.transformer';
3+
import { ConstraintType, DatabaseTable } from 'src/sql-tools/types';
34
import { describe, expect, it } from 'vitest';
45

56
const ctx = new BaseContext({});
67

8+
const table1: DatabaseTable = {
9+
name: 'table1',
10+
columns: [
11+
{
12+
name: 'column1',
13+
tableName: 'table1',
14+
primary: true,
15+
type: 'character varying',
16+
nullable: true,
17+
isArray: false,
18+
synchronize: true,
19+
},
20+
{
21+
name: 'column2',
22+
tableName: 'table1',
23+
type: 'character varying',
24+
nullable: true,
25+
isArray: false,
26+
synchronize: true,
27+
},
28+
],
29+
indexes: [
30+
{
31+
name: 'index1',
32+
tableName: 'table1',
33+
columnNames: ['column2'],
34+
unique: false,
35+
synchronize: true,
36+
},
37+
],
38+
constraints: [
39+
{
40+
name: 'constraint1',
41+
tableName: 'table1',
42+
columnNames: ['column1'],
43+
type: ConstraintType.PRIMARY_KEY,
44+
synchronize: true,
45+
},
46+
{
47+
name: 'constraint2',
48+
tableName: 'table1',
49+
columnNames: ['column1'],
50+
type: ConstraintType.FOREIGN_KEY,
51+
referenceTableName: 'table2',
52+
referenceColumnNames: ['parentId'],
53+
synchronize: true,
54+
},
55+
{
56+
name: 'constraint3',
57+
tableName: 'table1',
58+
columnNames: ['column1'],
59+
type: ConstraintType.UNIQUE,
60+
synchronize: true,
61+
},
62+
],
63+
triggers: [],
64+
synchronize: true,
65+
};
66+
767
describe(transformTables.name, () => {
868
describe('TableDrop', () => {
969
it('should work', () => {
@@ -22,26 +82,19 @@ describe(transformTables.name, () => {
2282
expect(
2383
transformTables(ctx, {
2484
type: 'TableCreate',
25-
table: {
26-
name: 'table1',
27-
columns: [
28-
{
29-
tableName: 'table1',
30-
name: 'column1',
31-
type: 'character varying',
32-
nullable: true,
33-
isArray: false,
34-
synchronize: true,
35-
},
36-
],
37-
indexes: [],
38-
constraints: [],
39-
triggers: [],
40-
synchronize: true,
41-
},
85+
table: table1,
4286
reason: 'unknown',
4387
}),
44-
).toEqual([`CREATE TABLE "table1" ("column1" character varying);`]);
88+
).toEqual([
89+
`CREATE TABLE "table1" (
90+
"column1" character varying,
91+
"column2" character varying,
92+
CONSTRAINT "constraint1" PRIMARY KEY ("column1"),
93+
CONSTRAINT "constraint2" FOREIGN KEY ("column1") REFERENCES "table2" ("parentId") ON UPDATE NO ACTION ON DELETE NO ACTION,
94+
CONSTRAINT "constraint3" UNIQUE ("column1")
95+
);`,
96+
`CREATE INDEX "index1" ON "table1" ("column2");`,
97+
]);
4598
});
4699

47100
it('should handle a non-nullable column', () => {
@@ -67,7 +120,11 @@ describe(transformTables.name, () => {
67120
},
68121
reason: 'unknown',
69122
}),
70-
).toEqual([`CREATE TABLE "table1" ("column1" character varying NOT NULL);`]);
123+
).toEqual([
124+
`CREATE TABLE "table1" (
125+
"column1" character varying NOT NULL
126+
);`,
127+
]);
71128
});
72129

73130
it('should handle a default value', () => {
@@ -94,7 +151,11 @@ describe(transformTables.name, () => {
94151
},
95152
reason: 'unknown',
96153
}),
97-
).toEqual([`CREATE TABLE "table1" ("column1" character varying DEFAULT uuid_generate_v4());`]);
154+
).toEqual([
155+
`CREATE TABLE "table1" (
156+
"column1" character varying DEFAULT uuid_generate_v4()
157+
);`,
158+
]);
98159
});
99160

100161
it('should handle a string with a fixed length', () => {
@@ -121,7 +182,11 @@ describe(transformTables.name, () => {
121182
},
122183
reason: 'unknown',
123184
}),
124-
).toEqual([`CREATE TABLE "table1" ("column1" character varying(2));`]);
185+
).toEqual([
186+
`CREATE TABLE "table1" (
187+
"column1" character varying(2)
188+
);`,
189+
]);
125190
});
126191

127192
it('should handle an array type', () => {
@@ -147,7 +212,11 @@ describe(transformTables.name, () => {
147212
},
148213
reason: 'unknown',
149214
}),
150-
).toEqual([`CREATE TABLE "table1" ("column1" character varying[]);`]);
215+
).toEqual([
216+
`CREATE TABLE "table1" (
217+
"column1" character varying[]
218+
);`,
219+
]);
151220
});
152221
});
153222
});

0 commit comments

Comments
 (0)