diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 7085899af7..1af9342e0b 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -11,7 +11,8 @@ import { setUnion } from 'src/utils/set'; const GeneratedUuidV7Column = (options: Omit = {}) => Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` }); -export const UpdateIdColumn = () => GeneratedUuidV7Column(); +export const UpdateIdColumn = (options: Omit = {}) => + GeneratedUuidV7Column(options); export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true }); diff --git a/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts b/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts new file mode 100644 index 0000000000..db351d5bab --- /dev/null +++ b/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddForeignKeyIndexes1744900200559 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_16294b83fa8c0149719a1f631e" ON "assets" ("livePhotoVideoId")`); + await queryRunner.query(`CREATE INDEX "IDX_9977c3c1de01c3d848039a6b90" ON "assets" ("libraryId")`); + await queryRunner.query(`CREATE INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" ON "assets" ("stackId")`); + await queryRunner.query(`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`); + await queryRunner.query(`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`); + await queryRunner.query(`CREATE INDEX "IDX_6c2e267ae764a9413b863a2934" ON "api_keys" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_5527cc99f530a547093f9e577b" ON "person" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_2bbabe31656b6778c6b87b6102" ON "person" ("faceAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_d7e875c6c60e661723dbf372fd" ON "partners" ("sharedWithId")`); + await queryRunner.query(`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_66fe3837414c5a9f1c33ca4934" ON "shared_links" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`); + await queryRunner.query(`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_66fe3837414c5a9f1c33ca4934";`); + await queryRunner.query(`DROP INDEX "IDX_91704e101438fd0653f582426d";`); + await queryRunner.query(`DROP INDEX "IDX_c05079e542fd74de3b5ecb5c1c";`); + await queryRunner.query(`DROP INDEX "IDX_5527cc99f530a547093f9e577b";`); + await queryRunner.query(`DROP INDEX "IDX_2bbabe31656b6778c6b87b6102";`); + await queryRunner.query(`DROP INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c";`); + await queryRunner.query(`DROP INDEX "IDX_9f9590cc11561f1f48ff034ef9";`); + await queryRunner.query(`DROP INDEX "IDX_2c5ac0d6fb58b238fd2068de67";`); + await queryRunner.query(`DROP INDEX "IDX_16294b83fa8c0149719a1f631e";`); + await queryRunner.query(`DROP INDEX "IDX_9977c3c1de01c3d848039a6b90";`); + await queryRunner.query(`DROP INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20";`); + await queryRunner.query(`DROP INDEX "IDX_b22c53f35ef20c28c21637c85f";`); + await queryRunner.query(`DROP INDEX "IDX_05895aa505a670300d4816debc";`); + await queryRunner.query(`DROP INDEX "IDX_57de40bc620f456c7311aa3a1e";`); + await queryRunner.query(`DROP INDEX "IDX_d8ddd9d687816cc490432b3d4b";`); + await queryRunner.query(`DROP INDEX "IDX_d7e875c6c60e661723dbf372fd";`); + await queryRunner.query(`DROP INDEX "IDX_575842846f0c28fa5da46c99b1";`); + await queryRunner.query(`DROP INDEX "IDX_6c2e267ae764a9413b863a2934";`); + await queryRunner.query(`DROP INDEX "IDX_1af8519996fbfb3684b58df280";`); + await queryRunner.query(`DROP INDEX "IDX_3571467bcbe021f66e2bdce96e";`); + await queryRunner.query(`DROP INDEX "IDX_8091ea76b12338cb4428d33d78";`); + } +} diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts index e7a144722c..802a86a303 100644 --- a/server/src/schema/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -5,7 +5,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, Index, @@ -51,7 +50,6 @@ export class ActivityTable { @Column({ type: 'boolean', default: false }) isLiked!: boolean; - @ColumnIndex('IDX_activity_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_activity_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts index 1b931e3116..8054009c39 100644 --- a/server/src/schema/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,25 +1,13 @@ import { AlbumTable } from 'src/schema/tables/album.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; @Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' }) export class AlbumAssetTable { - @ForeignKeyColumn(() => AlbumTable, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - nullable: false, - primary: true, - }) - @ColumnIndex() + @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true }) albumsId!: string; - @ForeignKeyColumn(() => AssetTable, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - nullable: false, - primary: true, - }) - @ColumnIndex() + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true }) assetsId!: string; @CreateDateColumn() diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index cdfd092b1b..428947fa51 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -4,7 +4,6 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -51,7 +50,6 @@ export class AlbumTable { @Column({ default: AssetOrder.DESC }) order!: AssetOrder; - @ColumnIndex('IDX_albums_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_albums_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts index 29c4ad2b0f..1d4cc83172 100644 --- a/server/src/schema/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -3,7 +3,6 @@ import { Permission } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -35,7 +34,6 @@ export class APIKeyTable { @Column({ array: true, type: 'character varying' }) permissions!: Permission[]; - @ColumnIndex({ name: 'IDX_api_keys_update_id' }) - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_api_keys_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts index 55d6f5c911..030256480c 100644 --- a/server/src/schema/tables/asset-audit.table.ts +++ b/server/src/schema/tables/asset-audit.table.ts @@ -1,20 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('assets_audit') export class AssetAuditTable { @PrimaryGeneratedUuidV7Column() id!: string; - @ColumnIndex('IDX_assets_audit_asset_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_assets_audit_asset_id' }) assetId!: string; - @ColumnIndex('IDX_assets_audit_owner_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_assets_audit_owner_id' }) ownerId!: string; - @ColumnIndex('IDX_assets_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_assets_audit_deleted_at' }) deletedAt!: Date; } diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 0ae99f44bf..52f4364a93 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -8,10 +8,21 @@ import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColu @Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) @Index({ columns: ['personId', 'assetId'] }) export class AssetFaceTable { - @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + // [assetId, personId] is the PK constraint + index: false, + }) assetId!: string; - @ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true }) + @ForeignKeyColumn(() => PersonTable, { + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + nullable: true, + // [personId, assetId] makes this redundant + index: false, + }) personId!: string | null; @Column({ default: 0, type: 'integer' }) diff --git a/server/src/schema/tables/asset-files.table.ts b/server/src/schema/tables/asset-files.table.ts index fb8750a8ef..0859bd5cf0 100644 --- a/server/src/schema/tables/asset-files.table.ts +++ b/server/src/schema/tables/asset-files.table.ts @@ -3,7 +3,6 @@ import { AssetFileType } from 'src/enum'; import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -19,8 +18,11 @@ export class AssetFileTable { @PrimaryGeneratedColumn() id!: string; - @ColumnIndex('IDX_asset_files_assetId') - @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + indexName: 'IDX_asset_files_assetId', + }) assetId?: string; @CreateDateColumn() @@ -35,7 +37,6 @@ export class AssetFileTable { @Column() path!: string; - @ColumnIndex('IDX_asset_files_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_asset_files_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 250c3546a2..9a9670cd0e 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -9,7 +9,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -78,8 +77,7 @@ export class AssetTable { @Column() originalPath!: string; - @ColumnIndex('idx_asset_file_created_at') - @Column({ type: 'timestamp with time zone' }) + @Column({ type: 'timestamp with time zone', indexName: 'idx_asset_file_created_at' }) fileCreatedAt!: Date; @Column({ type: 'timestamp with time zone' }) @@ -94,8 +92,7 @@ export class AssetTable { @Column({ type: 'character varying', nullable: true, default: '' }) encodedVideoPath!: string | null; - @Column({ type: 'bytea' }) - @ColumnIndex() + @Column({ type: 'bytea', index: true }) checksum!: Buffer; // sha1 checksum @Column({ type: 'boolean', default: true }) @@ -113,8 +110,7 @@ export class AssetTable { @Column({ type: 'boolean', default: false }) isArchived!: boolean; - @Column() - @ColumnIndex() + @Column({ index: true }) originalFileName!: string; @Column({ nullable: true }) @@ -141,14 +137,12 @@ export class AssetTable { @ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) stackId?: string | null; - @ColumnIndex('IDX_assets_duplicateId') - @Column({ type: 'uuid', nullable: true }) + @Column({ type: 'uuid', nullable: true, indexName: 'IDX_assets_duplicateId' }) duplicateId!: string | null; @Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE }) status!: AssetStatus; - @ColumnIndex('IDX_assets_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_assets_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/exif.table.ts b/server/src/schema/tables/exif.table.ts index e40ce94b4f..ca300945c3 100644 --- a/server/src/schema/tables/exif.table.ts +++ b/server/src/schema/tables/exif.table.ts @@ -1,6 +1,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; +import { Column, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('exif') @UpdatedAtTrigger('asset_exif_updated_at') @@ -50,8 +50,7 @@ export class ExifTable { @Column({ type: 'double precision', nullable: true }) longitude!: number | null; - @ColumnIndex('exif_city') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'exif_city' }) city!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -69,8 +68,7 @@ export class ExifTable { @Column({ type: 'character varying', nullable: true }) exposureTime!: string | null; - @ColumnIndex('IDX_live_photo_cid') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'IDX_live_photo_cid' }) livePhotoCID!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -88,8 +86,7 @@ export class ExifTable { @Column({ type: 'integer', nullable: true }) bitsPerSample!: number | null; - @ColumnIndex('IDX_auto_stack_id') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'IDX_auto_stack_id' }) autoStackId!: string | null; @Column({ type: 'integer', nullable: true }) @@ -98,7 +95,6 @@ export class ExifTable { @UpdateDateColumn({ default: () => 'clock_timestamp()' }) updatedAt?: Date; - @ColumnIndex('IDX_asset_exif_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_asset_exif_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts index 54b3752f41..8b21d5feb0 100644 --- a/server/src/schema/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -41,7 +40,6 @@ export class LibraryTable { @Column({ type: 'timestamp with time zone', nullable: true }) refreshedAt!: Date | null; - @ColumnIndex('IDX_libraries_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_libraries_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 1926405565..32dafe3384 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -3,7 +3,6 @@ import { MemoryType } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -55,7 +54,6 @@ export class MemoryTable { @Column({ type: 'timestamp with time zone', nullable: true }) hideAt?: Date; - @ColumnIndex('IDX_memories_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_memories_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/memory_asset.table.ts b/server/src/schema/tables/memory_asset.table.ts index 864e6291c7..0e5ca29a08 100644 --- a/server/src/schema/tables/memory_asset.table.ts +++ b/server/src/schema/tables/memory_asset.table.ts @@ -1,14 +1,12 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('memories_assets_assets') export class MemoryAssetTable { - @ColumnIndex() @ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) memoriesId!: string; - @ColumnIndex() @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) assetsId!: string; } diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts index 08b6e94626..da5243dc75 100644 --- a/server/src/schema/tables/partner-audit.table.ts +++ b/server/src/schema/tables/partner-audit.table.ts @@ -1,20 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('partners_audit') export class PartnerAuditTable { @PrimaryGeneratedUuidV7Column() id!: string; - @ColumnIndex('IDX_partners_audit_shared_by_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_by_id' }) sharedById!: string; - @ColumnIndex('IDX_partners_audit_shared_with_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_with_id' }) sharedWithId!: string; - @ColumnIndex('IDX_partners_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_partners_audit_deleted_at' }) deletedAt!: Date; } diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts index 770107fe7a..0da60cfc0c 100644 --- a/server/src/schema/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,15 +1,7 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { partners_delete_audit } from 'src/schema/functions'; import { UserTable } from 'src/schema/tables/user.table'; -import { - AfterDeleteTrigger, - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - Table, - UpdateDateColumn, -} from 'src/sql-tools'; +import { AfterDeleteTrigger, Column, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('partners') @UpdatedAtTrigger('partners_updated_at') @@ -21,7 +13,12 @@ import { when: 'pg_trigger_depth() = 0', }) export class PartnerTable { - @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => UserTable, { + onDelete: 'CASCADE', + primary: true, + // [sharedById, sharedWithId] is the PK constraint + index: false, + }) sharedById!: string; @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) @@ -36,7 +33,6 @@ export class PartnerTable { @Column({ type: 'boolean', default: false }) inTimeline!: boolean; - @ColumnIndex('IDX_partners_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_partners_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index b96fc5b709..1320b91f18 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -4,7 +4,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -49,7 +48,6 @@ export class PersonTable { @Column({ type: 'character varying', nullable: true, default: null }) color?: string | null; - @ColumnIndex('IDX_person_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_person_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index a66732a7d9..ad43d0d6e4 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -35,7 +34,6 @@ export class SessionTable { @Column({ default: '' }) deviceOS!: string; - @ColumnIndex('IDX_sessions_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts index 1eb294c1e8..66c9068441 100644 --- a/server/src/schema/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -1,14 +1,12 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('shared_link__asset') export class SharedLinkAssetTable { - @ColumnIndex() @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) assetsId!: string; - @ColumnIndex() @ForeignKeyColumn(() => SharedLinkTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) sharedLinksId!: string; } diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts index 36237c58ef..3bb36b36ed 100644 --- a/server/src/schema/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -1,15 +1,7 @@ import { SharedLinkType } from 'src/enum'; import { AlbumTable } from 'src/schema/tables/album.table'; import { UserTable } from 'src/schema/tables/user.table'; -import { - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - PrimaryGeneratedColumn, - Table, - Unique, -} from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools'; @Table('shared_links') @Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) @@ -23,8 +15,7 @@ export class SharedLinkTable { @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) userId!: string; - @ColumnIndex('IDX_sharedlink_key') - @Column({ type: 'bytea' }) + @Column({ type: 'bytea', indexName: 'IDX_sharedlink_key' }) key!: Buffer; // use to access the inidividual asset @Column() @@ -39,8 +30,12 @@ export class SharedLinkTable { @Column({ type: 'boolean', default: false }) allowUpload!: boolean; - @ColumnIndex('IDX_sharedlink_albumId') - @ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AlbumTable, { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + indexName: 'IDX_sharedlink_albumId', + }) albumId!: string; @Column({ type: 'boolean', default: true }) diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts index 831205ce7a..21fd7983ac 100644 --- a/server/src/schema/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -1,15 +1,7 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { SyncEntityType } from 'src/enum'; import { SessionTable } from 'src/schema/tables/session.table'; -import { - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - PrimaryColumn, - Table, - UpdateDateColumn, -} from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, PrimaryColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('session_sync_checkpoints') @UpdatedAtTrigger('session_sync_checkpoints_updated_at') @@ -29,7 +21,6 @@ export class SessionSyncCheckpointTable { @Column() ack!: string; - @ColumnIndex('IDX_session_sync_checkpoints_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_session_sync_checkpoints_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts index 5f24799cec..8793af0a8a 100644 --- a/server/src/schema/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -1,15 +1,13 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ColumnIndex, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] }) @Table('tag_asset') export class TagAssetTable { - @ColumnIndex() - @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true, index: true }) assetsId!: string; - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => TagTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true, index: true }) tagsId!: string; } diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts index acde84b91d..8829e802e1 100644 --- a/server/src/schema/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,13 +1,11 @@ import { TagTable } from 'src/schema/tables/tag.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('tags_closure') export class TagClosureTable { - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', index: true }) id_ancestor!: string; - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', index: true }) id_descendant!: string; } diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts index 5042e2eb0e..a9f2a57f27 100644 --- a/server/src/schema/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -18,7 +17,12 @@ export class TagTable { @PrimaryGeneratedColumn() id!: string; - @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + @ForeignKeyColumn(() => UserTable, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + // [userId, value] makes this redundant + index: false, + }) userId!: string; @Column() @@ -36,7 +40,6 @@ export class TagTable { @ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' }) parentId?: string; - @ColumnIndex('IDX_tags_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_tags_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts index 0f881ccc9a..e0c9afcdc3 100644 --- a/server/src/schema/tables/user-audit.table.ts +++ b/server/src/schema/tables/user-audit.table.ts @@ -1,13 +1,12 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('users_audit') export class UserAuditTable { @Column({ type: 'uuid' }) userId!: string; - @ColumnIndex('IDX_users_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_users_audit_deleted_at' }) deletedAt!: Date; @PrimaryGeneratedUuidV7Column() diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index 6d03acaf80..04b457867f 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -5,7 +5,13 @@ import { UserMetadata, UserMetadataItem } from 'src/types'; @Table('user_metadata') export class UserMetadataTable implements UserMetadataItem { - @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => UserTable, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + primary: true, + // [userId, key] is the PK constraint + index: false, + }) userId!: string; @PrimaryColumn({ type: 'character varying' }) diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 5160f979b9..eeef923796 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -5,7 +5,6 @@ import { users_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, Index, @@ -77,7 +76,6 @@ export class UserTable { @Column({ type: 'timestamp with time zone', default: () => 'now()' }) profileChangedAt!: Generated; - @ColumnIndex({ name: 'IDX_users_update_id' }) - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_users_update_id' }) updateId!: Generated; } diff --git a/server/src/sql-tools/from-code/decorators/column-index.decorator.ts b/server/src/sql-tools/from-code/decorators/column-index.decorator.ts deleted file mode 100644 index ab15292612..0000000000 --- a/server/src/sql-tools/from-code/decorators/column-index.decorator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { register } from 'src/sql-tools/from-code/register'; -import { asOptions } from 'src/sql-tools/helpers'; - -export type ColumnIndexOptions = { - name?: string; - unique?: boolean; - expression?: string; - using?: string; - with?: string; - where?: string; - synchronize?: boolean; -}; -export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => - void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/from-code/decorators/column.decorator.ts b/server/src/sql-tools/from-code/decorators/column.decorator.ts index 74a83cbcf3..7b00af80cc 100644 --- a/server/src/sql-tools/from-code/decorators/column.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/column.decorator.ts @@ -15,13 +15,15 @@ export type ColumnBaseOptions = { synchronize?: boolean; storage?: ColumnStorage; identity?: boolean; + index?: boolean; + indexName?: string; + unique?: boolean; + uniqueConstraintName?: string; }; export type ColumnOptions = ColumnBaseOptions & { enum?: DatabaseEnum; array?: boolean; - unique?: boolean; - uniqueConstraintName?: string; }; export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => { diff --git a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts index 070aa5cb51..beb3aa6fd6 100644 --- a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts @@ -7,8 +7,6 @@ export type ForeignKeyColumnOptions = ColumnBaseOptions & { onUpdate?: Action; onDelete?: Action; constraintName?: string; - unique?: boolean; - uniqueConstraintName?: string; }; export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => { diff --git a/server/src/sql-tools/from-code/decorators/index.decorator.ts b/server/src/sql-tools/from-code/decorators/index.decorator.ts index cd76b5e36d..5d90c4f58d 100644 --- a/server/src/sql-tools/from-code/decorators/index.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/index.decorator.ts @@ -1,8 +1,13 @@ -import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator'; import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; -export type IndexOptions = ColumnIndexOptions & { +export type IndexOptions = { + name?: string; + unique?: boolean; + expression?: string; + using?: string; + with?: string; + where?: string; columns?: string[]; synchronize?: boolean; }; diff --git a/server/src/sql-tools/from-code/index.ts b/server/src/sql-tools/from-code/index.ts index 3c74d2763c..95f1dbb22d 100644 --- a/server/src/sql-tools/from-code/index.ts +++ b/server/src/sql-tools/from-code/index.ts @@ -1,6 +1,5 @@ import 'reflect-metadata'; import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor'; -import { processColumnIndexes } from 'src/sql-tools/from-code/processors/column-index.processor'; import { processColumns } from 'src/sql-tools/from-code/processors/column.processor'; import { processConfigurationParameters } from 'src/sql-tools/from-code/processors/configuration-parameter.processor'; import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor'; @@ -36,14 +35,21 @@ const processors: Processor[] = [ processUniqueConstraints, processCheckConstraints, processPrimaryKeyConstraints, - processIndexes, - processColumnIndexes, processForeignKeyConstraints, + processIndexes, processTriggers, ]; -export const schemaFromCode = () => { +export type SchemaFromCodeOptions = { + /** automatically create indexes on foreign key columns */ + createForeignKeyIndexes?: boolean; +}; +export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { if (!initialized) { + const globalOptions = { + createForeignKeyIndexes: options.createForeignKeyIndexes ?? true, + }; + const builder: SchemaBuilder = { name: 'postgres', schemaName: 'public', @@ -58,7 +64,7 @@ export const schemaFromCode = () => { const items = getRegisteredItems(); for (const processor of processors) { - processor(builder, items); + processor(builder, items, globalOptions); } schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) }; diff --git a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts b/server/src/sql-tools/from-code/processors/check-constraint.processor.ts index d61ee18277..feb21b9894 100644 --- a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/check-constraint.processor.ts @@ -1,6 +1,6 @@ import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asCheckConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processCheckConstraints: Processor = (builder, items) => { @@ -24,3 +24,5 @@ export const processCheckConstraints: Processor = (builder, items) => { }); } }; + +const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); diff --git a/server/src/sql-tools/from-code/processors/column-index.processor.ts b/server/src/sql-tools/from-code/processors/column-index.processor.ts deleted file mode 100644 index 0e40fa1ee3..0000000000 --- a/server/src/sql-tools/from-code/processors/column-index.processor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; -import { onMissingTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asIndexName } from 'src/sql-tools/helpers'; - -export const processColumnIndexes: Processor = (builder, items) => { - for (const { - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'columnIndex')) { - const { table, column } = resolveColumn(builder, object, propertyName); - if (!table) { - onMissingTable(builder, '@ColumnIndex', object); - continue; - } - - if (!column) { - onMissingColumn(builder, `@ColumnIndex`, object, propertyName); - continue; - } - - table.indexes.push({ - name: options.name || asIndexName(table.name, [column.name], options.where), - tableName: table.name, - unique: options.unique ?? false, - expression: options.expression, - using: options.using, - where: options.where, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/from-code/processors/column.processor.ts b/server/src/sql-tools/from-code/processors/column.processor.ts index 37f3f5d082..e8c2544f87 100644 --- a/server/src/sql-tools/from-code/processors/column.processor.ts +++ b/server/src/sql-tools/from-code/processors/column.processor.ts @@ -1,8 +1,8 @@ import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { asMetadataKey, asUniqueConstraintName, fromColumnValue } from 'src/sql-tools/helpers'; -import { DatabaseColumn, DatabaseConstraintType } from 'src/sql-tools/types'; +import { asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers'; +import { DatabaseColumn } from 'src/sql-tools/types'; export const processColumns: Processor = (builder, items) => { for (const { @@ -54,16 +54,6 @@ export const processColumns: Processor = (builder, items) => { writeMetadata(object, propertyName, { name: column.name, options }); table.columns.push(column); - - if (type === 'column' && !options.primary && options.unique) { - table.constraints.push({ - type: DatabaseConstraintType.UNIQUE, - name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), - tableName: table.name, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } } }; diff --git a/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts b/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts index 784a8b8e99..612b74c30f 100644 --- a/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts +++ b/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts @@ -1,7 +1,7 @@ import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asForeignKeyConstraintName, asRelationKeyConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types'; export const processForeignKeyConstraints: Processor = (builder, items) => { @@ -46,7 +46,7 @@ export const processForeignKeyConstraints: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); - if (options.unique) { + if (options.unique || options.uniqueConstraintName) { table.constraints.push({ name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), tableName: table.name, @@ -57,3 +57,6 @@ export const processForeignKeyConstraints: Processor = (builder, items) => { } } }; + +const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns); +const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); diff --git a/server/src/sql-tools/from-code/processors/index.processor.ts b/server/src/sql-tools/from-code/processors/index.processor.ts index 3625bf9784..f4c9c7cec1 100644 --- a/server/src/sql-tools/from-code/processors/index.processor.ts +++ b/server/src/sql-tools/from-code/processors/index.processor.ts @@ -1,8 +1,9 @@ +import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asIndexName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; -export const processIndexes: Processor = (builder, items) => { +export const processIndexes: Processor = (builder, items, config) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'index')) { @@ -24,4 +25,66 @@ export const processIndexes: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); } + + // column indexes + for (const { + type, + item: { object, propertyName, options }, + } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { + const { table, column } = resolveColumn(builder, object, propertyName); + if (!table) { + onMissingTable(builder, '@Column', object); + continue; + } + + if (!column) { + // should be impossible since they are created in `column.processor.ts` + onMissingColumn(builder, '@Column', object, propertyName); + continue; + } + + if (options.index === false) { + continue; + } + + const isIndexRequested = + options.indexName || options.index || (type === 'foreignKeyColumn' && config.createForeignKeyIndexes); + if (!isIndexRequested) { + continue; + } + + const indexName = options.indexName || asIndexName(table.name, [column.name]); + + const isIndexPresent = table.indexes.some((index) => index.name === indexName); + if (isIndexPresent) { + continue; + } + + const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1; + if (isOnlyPrimaryColumn) { + // will have an index created by the primary key constraint + continue; + } + + table.indexes.push({ + name: indexName, + tableName: table.name, + unique: false, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); + } +}; + +const asIndexName = (table: string, columns?: string[], where?: string) => { + const items: string[] = []; + for (const columnName of columns ?? []) { + items.push(columnName); + } + + if (where) { + items.push(where); + } + + return asKey('IDX_', table, items); }; diff --git a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts index f123f2e495..74aecc5ea0 100644 --- a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts @@ -1,5 +1,5 @@ import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asPrimaryKeyConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processPrimaryKeyConstraints: Processor = (builder) => { @@ -22,3 +22,5 @@ export const processPrimaryKeyConstraints: Processor = (builder) => { } } }; + +const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); diff --git a/server/src/sql-tools/from-code/processors/trigger.processor.ts b/server/src/sql-tools/from-code/processors/trigger.processor.ts index 2f4cc04326..4b875f353b 100644 --- a/server/src/sql-tools/from-code/processors/trigger.processor.ts +++ b/server/src/sql-tools/from-code/processors/trigger.processor.ts @@ -1,6 +1,7 @@ +import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asTriggerName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; export const processTriggers: Processor = (builder, items) => { for (const { @@ -26,3 +27,6 @@ export const processTriggers: Processor = (builder, items) => { }); } }; + +const asTriggerName = (table: string, trigger: TriggerOptions) => + asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); diff --git a/server/src/sql-tools/from-code/processors/type.ts b/server/src/sql-tools/from-code/processors/type.ts index 5a69efbcf0..deb142d278 100644 --- a/server/src/sql-tools/from-code/processors/type.ts +++ b/server/src/sql-tools/from-code/processors/type.ts @@ -1,3 +1,4 @@ +import { SchemaFromCodeOptions } from 'src/sql-tools/from-code'; import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator'; import { RegisterItem } from 'src/sql-tools/from-code/register-item'; import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types'; @@ -6,4 +7,4 @@ import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types'; export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } }; export type SchemaBuilder = Omit & { tables: TableWithMetadata[] }; -export type Processor = (builder: SchemaBuilder, items: RegisterItem[]) => void; +export type Processor = (builder: SchemaBuilder, items: RegisterItem[], options: SchemaFromCodeOptions) => void; diff --git a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts b/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts index 74c0504f7e..9014378085 100644 --- a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts @@ -1,6 +1,7 @@ +import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asUniqueConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processUniqueConstraints: Processor = (builder, items) => { @@ -24,4 +25,34 @@ export const processUniqueConstraints: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); } + + // column level constraints + for (const { + type, + item: { object, propertyName, options }, + } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { + const { table, column } = resolveColumn(builder, object, propertyName); + if (!table) { + onMissingTable(builder, '@Column', object); + continue; + } + + if (!column) { + // should be impossible since they are created in `column.processor.ts` + onMissingColumn(builder, '@Column', object, propertyName); + continue; + } + + if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { + table.constraints.push({ + type: DatabaseConstraintType.UNIQUE, + name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), + tableName: table.name, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); + } + } }; + +const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); diff --git a/server/src/sql-tools/from-code/register-function.ts b/server/src/sql-tools/from-code/register-function.ts index 69e1a0f8f3..be71e0dfd7 100644 --- a/server/src/sql-tools/from-code/register-function.ts +++ b/server/src/sql-tools/from-code/register-function.ts @@ -1,5 +1,4 @@ import { register } from 'src/sql-tools/from-code/register'; -import { asFunctionExpression } from 'src/sql-tools/helpers'; import { ColumnType, DatabaseFunction } from 'src/sql-tools/types'; export type FunctionOptions = { @@ -27,3 +26,37 @@ export const registerFunction = (options: FunctionOptions) => { return item; }; + +const asFunctionExpression = (options: FunctionOptions) => { + const name = options.name; + const sql: string[] = [ + `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, + `RETURNS ${options.returnType}`, + ]; + + const flags = [ + options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, + options.strict ? 'STRICT' : undefined, + options.behavior ? options.behavior.toUpperCase() : undefined, + `LANGUAGE ${options.language ?? 'SQL'}`, + ].filter((x) => x !== undefined); + + if (flags.length > 0) { + sql.push(flags.join(' ')); + } + + if ('return' in options) { + sql.push(` RETURN ${options.return}`); + } + + if ('body' in options) { + sql.push( + // + `AS $$`, + ' ' + options.body.trim(), + `$$;`, + ); + } + + return sql.join('\n ').trim(); +}; diff --git a/server/src/sql-tools/from-code/register-item.ts b/server/src/sql-tools/from-code/register-item.ts index 08200cbc4f..4889ae34b9 100644 --- a/server/src/sql-tools/from-code/register-item.ts +++ b/server/src/sql-tools/from-code/register-item.ts @@ -1,5 +1,4 @@ import { CheckOptions } from 'src/sql-tools/from-code/decorators/check.decorator'; -import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator'; import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator'; @@ -21,7 +20,6 @@ export type RegisterItem = | { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> } | { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> } | { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> } - | { type: 'columnIndex'; item: PropertyBased<{ options: ColumnIndexOptions }> } | { type: 'function'; item: DatabaseFunction } | { type: 'enum'; item: DatabaseEnum } | { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> } diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index 364b695194..2802407ea6 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -1,7 +1,5 @@ import { createHash } from 'node:crypto'; import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; -import { FunctionOptions } from 'src/sql-tools/from-code/register-function'; import { Comparer, DatabaseColumn, @@ -18,25 +16,6 @@ export const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A // match TypeORM export const asKey = (prefix: string, tableName: string, values: string[]) => (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); -export const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); -export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns); -export const asTriggerName = (table: string, trigger: TriggerOptions) => - asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); -export const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); -export const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); -export const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); -export const asIndexName = (table: string, columns: string[] | undefined, where: string | undefined) => { - const items: string[] = []; - for (const columnName of columns ?? []) { - items.push(columnName); - } - - if (where) { - items.push(where); - } - - return asKey('IDX_', table, items); -}; export const asOptions = (options: string | T): T => { if (typeof options === 'string') { @@ -46,40 +25,6 @@ export const asOptions = (options: string | T): T = return options; }; -export const asFunctionExpression = (options: FunctionOptions) => { - const name = options.name; - const sql: string[] = [ - `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, - `RETURNS ${options.returnType}`, - ]; - - const flags = [ - options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, - options.strict ? 'STRICT' : undefined, - options.behavior ? options.behavior.toUpperCase() : undefined, - `LANGUAGE ${options.language ?? 'SQL'}`, - ].filter((x) => x !== undefined); - - if (flags.length > 0) { - sql.push(flags.join(' ')); - } - - if ('return' in options) { - sql.push(` RETURN ${options.return}`); - } - - if ('body' in options) { - sql.push( - // - `AS $$`, - ' ' + options.body.trim(), - `$$;`, - ); - } - - return sql.join('\n ').trim(); -}; - export const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); export const hasMask = (input: number, mask: number) => (input & mask) === mask; diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index d916678d4a..b41cce4ab5 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -3,7 +3,6 @@ export { schemaFromCode } from 'src/sql-tools/from-code'; export * from 'src/sql-tools/from-code/decorators/after-delete.decorator'; export * from 'src/sql-tools/from-code/decorators/before-update.decorator'; export * from 'src/sql-tools/from-code/decorators/check.decorator'; -export * from 'src/sql-tools/from-code/decorators/column-index.decorator'; export * from 'src/sql-tools/from-code/decorators/column.decorator'; export * from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; export * from 'src/sql-tools/from-code/decorators/create-date-column.decorator'; diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts index e8b36ec119..cedae006be 100644 --- a/server/test/sql-tools/column-index-name-default.ts +++ b/server/test/sql-tools/column-index-name-default.ts @@ -1,9 +1,8 @@ -import { Column, ColumnIndex, DatabaseSchema, Table } from 'src/sql-tools'; +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; @Table() export class Table1 { - @ColumnIndex() - @Column() + @Column({ index: true }) column1!: string; } diff --git a/server/test/sql-tools/column-index-name.ts b/server/test/sql-tools/column-index-name.ts new file mode 100644 index 0000000000..8ba18a8851 --- /dev/null +++ b/server/test/sql-tools/column-index-name.ts @@ -0,0 +1,46 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ indexName: 'IDX_test' }) + column1!: string; +} + +export const description = 'should create a column with an index if a name is provided'; +export const schema: DatabaseSchema = { + name: 'postgres', + schemaName: 'public', + functions: [], + enums: [], + extensions: [], + parameters: [], + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_test', + columnNames: ['column1'], + tableName: 'table1', + unique: false, + synchronize: true, + }, + ], + triggers: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts index 2ecaafdcad..0b66a1acd4 100644 --- a/server/test/sql-tools/foreign-key-inferred-type.stub.ts +++ b/server/test/sql-tools/foreign-key-inferred-type.stub.ts @@ -60,7 +60,15 @@ export const schema: DatabaseSchema = { synchronize: true, }, ], - indexes: [], + indexes: [ + { + name: 'IDX_3fcca5cc563abf256fc346e3ff', + tableName: 'table2', + columnNames: ['parentId'], + unique: false, + synchronize: true, + }, + ], triggers: [], constraints: [ { diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts index 0601a02d42..109a3dfc85 100644 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -60,7 +60,15 @@ export const schema: DatabaseSchema = { synchronize: true, }, ], - indexes: [], + indexes: [ + { + name: 'IDX_3fcca5cc563abf256fc346e3ff', + tableName: 'table2', + columnNames: ['parentId'], + unique: false, + synchronize: true, + }, + ], triggers: [], constraints: [ {