chore: migrate to sql-tools library

This commit is contained in:
Daniel Dietzler
2026-02-20 19:30:23 +01:00
committed by Jason Rasmussen
parent 82c6302549
commit cbb9e89e41
231 changed files with 201 additions and 9151 deletions

25
pnpm-lock.yaml generated
View File

@@ -343,6 +343,9 @@ importers:
'@extism/extism':
specifier: 2.0.0-rc13
version: 2.0.0-rc13
'@immich/sql-tools':
specifier: ^0.2.0
version: 0.2.0
'@nestjs/bullmq':
specifier: ^11.0.1
version: 11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(bullmq@5.68.0)
@@ -3013,6 +3016,9 @@ packages:
'@immich/justified-layout-wasm@0.4.3':
resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==}
'@immich/sql-tools@0.2.0':
resolution: {integrity: sha512-AH0GRIUYrckNKuid5uO33vgRbGaznhRtArdQ91K310A1oUFjaoNzOaZyZhXwEmft3WYeC1bx4fdgUeois2QH5A==}
'@immich/svelte-markdown-preprocess@0.2.1':
resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==}
peerDependencies:
@@ -8291,6 +8297,10 @@ packages:
postgres:
optional: true
kysely@0.28.11:
resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==}
engines: {node: '>=20.0.0'}
kysely@0.28.2:
resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==}
engines: {node: '>=18.0.0'}
@@ -14812,6 +14822,13 @@ snapshots:
'@immich/justified-layout-wasm@0.4.3': {}
'@immich/sql-tools@0.2.0':
dependencies:
kysely: 0.28.11
kysely-postgres-js: 3.0.0(kysely@0.28.11)(postgres@3.4.8)
pg-connection-string: 2.11.0
postgres: 3.4.8
'@immich/svelte-markdown-preprocess@0.2.1(svelte@5.51.5)':
dependencies:
front-matter: 4.0.2
@@ -20822,12 +20839,20 @@ snapshots:
type-is: 2.0.1
vary: 1.1.2
kysely-postgres-js@3.0.0(kysely@0.28.11)(postgres@3.4.8):
dependencies:
kysely: 0.28.11
optionalDependencies:
postgres: 3.4.8
kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.8):
dependencies:
kysely: 0.28.2
optionalDependencies:
postgres: 3.4.8
kysely@0.28.11: {}
kysely@0.28.2: {}
langium@3.3.1:

View File

@@ -35,6 +35,7 @@
},
"dependencies": {
"@extism/extism": "2.0.0-rc13",
"@immich/sql-tools": "^0.2.0",
"@nestjs/bullmq": "^11.0.1",
"@nestjs/common": "^11.0.4",
"@nestjs/core": "^11.0.4",

View File

@@ -1,16 +1,15 @@
#!/usr/bin/env node
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools';
import { Kysely, sql } from 'kysely';
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { basename, dirname, extname, join } from 'node:path';
import postgres from 'postgres';
import { ConfigRepository } from 'src/repositories/config.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import 'src/schema';
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
import { getKyselyConfig } from 'src/utils/database';
const main = async () => {
const command = process.argv[2];
@@ -130,10 +129,9 @@ const create = (path: string, up: string[], down: string[]) => {
const compare = async () => {
const configRepository = new ConfigRepository();
const { database } = configRepository.getEnv();
const db = postgres(asPostgresConnectionConfig(database.config));
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
const target = await schemaFromDatabase(db, {});
const target = await schemaFromDatabase({ connection: database.config });
console.log(source.warnings.join('\n'));

View File

@@ -1,7 +1,7 @@
import { asHuman } from '@immich/sql-tools';
import { Command, CommandRunner } from 'nest-commander';
import { ErrorMessages } from 'src/constants';
import { CliService } from 'src/services/cli.service';
import { asHuman } from 'src/sql-tools/schema-diff';
@Command({
name: 'schema-check',

View File

@@ -1,10 +1,10 @@
import { BeforeUpdateTrigger, Column, ColumnOptions } from '@immich/sql-tools';
import { SetMetadata, applyDecorators } from '@nestjs/common';
import { ApiOperation, ApiOperationOptions, ApiProperty, ApiPropertyOptions, ApiTags } from '@nestjs/swagger';
import _ from 'lodash';
import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
import { EmitEvent } from 'src/repositories/event.repository';
import { immich_uuid_v7, updated_at } from 'src/schema/functions';
import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools';
import { setUnion } from 'src/utils/set';
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>

View File

@@ -1,6 +1,7 @@
import { DatabaseSslMode } from '@immich/sql-tools';
import { Transform, Type } from 'class-transformer';
import { IsEnum, IsInt, IsString, Matches } from 'class-validator';
import { DatabaseSslMode, ImmichEnvironment, LogFormat, LogLevel } from 'src/enum';
import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum';
import { IsIPRange, Optional, ValidateBoolean } from 'src/validation';
export class EnvDto {

View File

@@ -821,14 +821,6 @@ export enum OAuthTokenEndpointAuthMethod {
ClientSecretBasic = 'client_secret_basic',
}
export enum DatabaseSslMode {
Disable = 'disable',
Allow = 'allow',
Prefer = 'prefer',
Require = 'require',
VerifyFull = 'verify-full',
}
export enum AssetVisibility {
Archive = 'archive',
Timeline = 'timeline',

View File

@@ -52,9 +52,9 @@ class Workers {
try {
const value = await systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode);
return value?.isMaintenanceMode || false;
} catch (error) {
} catch (error: Error | any) {
// Table doesn't exist (migrations haven't run yet)
if (error instanceof PostgresError && error.code === '42P01') {
if ((error as PostgresError).code === '42P01') {
return false;
}

View File

@@ -1,3 +1,4 @@
import { DatabaseConnectionParams } from '@immich/sql-tools';
import { RegisterQueueOptions } from '@nestjs/bullmq';
import { Inject, Injectable, Optional } from '@nestjs/common';
import { QueueOptions } from 'bullmq';
@@ -21,7 +22,7 @@ import {
LogLevel,
QueueName,
} from 'src/enum';
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
import { VectorExtension } from 'src/types';
import { setDifference } from 'src/utils/set';
export interface EnvData {

View File

@@ -1,3 +1,4 @@
import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools';
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
@@ -21,7 +22,6 @@ import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode
import { DB } from 'src/schema';
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types';
import { vectorIndexQuery } from 'src/utils/database';
import { isValidInteger } from 'src/validation';
@@ -289,7 +289,8 @@ export class DatabaseRepository {
async getSchemaDrift() {
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
const target = await schemaFromDatabase(this.db, {});
const { database } = this.configRepository.getEnv();
const target = await schemaFromDatabase({ connection: database.config });
const drift = schemaDiff(source, target, {
tables: { ignoreExtra: true },

View File

@@ -1,5 +1,5 @@
import { registerEnum } from '@immich/sql-tools';
import { AssetStatus, AssetVisibility, SourceType } from 'src/enum';
import { registerEnum } from 'src/sql-tools';
export const assets_status_enum = registerEnum({
name: 'assets_status_enum',

View File

@@ -1,4 +1,4 @@
import { registerFunction } from 'src/sql-tools';
import { registerFunction } from '@immich/sql-tools';
export const immich_uuid_v7 = registerFunction({
name: 'immich_uuid_v7',

View File

@@ -1,3 +1,4 @@
import { Database, Extensions, Generated, Int8 } from '@immich/sql-tools';
import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import {
album_delete_audit,
@@ -72,7 +73,6 @@ import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
import { UserTable } from 'src/schema/tables/user.table';
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
import { Database, Extensions, Generated, Int8 } from 'src/sql-tools';
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@Database({ name: 'immich' })

View File

@@ -1,8 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Check,
Column,
@@ -15,7 +10,12 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table('activity')
@UpdatedAtTrigger('activity_updatedAt')

View File

@@ -1,6 +1,6 @@
import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { AlbumTable } from 'src/schema/tables/album.table';
import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('album_asset_audit')
export class AlbumAssetAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { album_asset_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import {
AfterDeleteTrigger,
CreateDateColumn,
@@ -10,7 +6,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { album_asset_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
@Table({ name: 'album_asset' })
@UpdatedAtTrigger('album_asset_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('album_audit')
export class AlbumAuditTable {

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('album_user_audit')
export class AlbumUserAuditTable {

View File

@@ -1,8 +1,3 @@
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
AfterInsertTrigger,
@@ -13,7 +8,12 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table({ name: 'album_user' })
// Pre-existing indices from original album <--> user ManyToMany mapping

View File

@@ -1,8 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetOrder } from 'src/enum';
import { album_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
@@ -14,7 +9,12 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetOrder } from 'src/enum';
import { album_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table({ name: 'album' })
@UpdatedAtTrigger('album_updatedAt')

View File

@@ -1,6 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { Permission } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -10,7 +7,10 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { Permission } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
@Table('api_key')
@UpdatedAtTrigger('api_key_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('asset_audit')
export class AssetAuditTable {

View File

@@ -1,6 +1,3 @@
import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto';
import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import {
AfterDeleteTrigger,
AfterInsertTrigger,
@@ -10,7 +7,10 @@ import {
PrimaryGeneratedColumn,
Table,
Unique,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto';
import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
@Table('asset_edit')
@AfterInsertTrigger({ scope: 'statement', function: asset_edit_insert, referencingNewTableAs: 'inserted_edit' })

View File

@@ -1,7 +1,7 @@
import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from '@immich/sql-tools';
import { LockableProperty } from 'src/database';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools';
@Table('asset_exif')
@UpdatedAtTrigger('asset_exif_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('asset_face_audit')
export class AssetFaceAuditTable {

View File

@@ -1,9 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { SourceType } from 'src/enum';
import { asset_face_source_type } from 'src/schema/enums';
import { asset_face_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { PersonTable } from 'src/schema/tables/person.table';
import {
AfterDeleteTrigger,
Column,
@@ -15,7 +9,13 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { SourceType } from 'src/enum';
import { asset_face_source_type } from 'src/schema/enums';
import { asset_face_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { PersonTable } from 'src/schema/tables/person.table';
@Table({ name: 'asset_face' })
@UpdatedAtTrigger('asset_face_updatedAt')

View File

@@ -1,6 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetFileType } from 'src/enum';
import { AssetTable } from 'src/schema/tables/asset.table';
import {
Column,
CreateDateColumn,
@@ -11,7 +8,10 @@ import {
Timestamp,
Unique,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetFileType } from 'src/enum';
import { AssetTable } from 'src/schema/tables/asset.table';
@Table('asset_file')
@Unique({ columns: ['assetId', 'type', 'isEdited'] })

View File

@@ -1,5 +1,5 @@
import { Column, ForeignKeyColumn, Table, Timestamp } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Table, Timestamp } from 'src/sql-tools';
@Table('asset_job_status')
export class AssetJobStatusTable {

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('asset_metadata_audit')
export class AssetMetadataAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetMetadataKey } from 'src/enum';
import { asset_metadata_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import {
AfterDeleteTrigger,
Column,
@@ -11,7 +7,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetMetadataKey } from 'src/enum';
import { asset_metadata_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
@UpdatedAtTrigger('asset_metadata_updated_at')
@Table('asset_metadata')

View File

@@ -1,5 +1,5 @@
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
@Table('asset_ocr')
export class AssetOcrTable {

View File

@@ -1,10 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import { asset_delete_audit } from 'src/schema/functions';
import { LibraryTable } from 'src/schema/tables/library.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
@@ -17,7 +10,14 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import { asset_delete_audit } from 'src/schema/functions';
import { LibraryTable } from 'src/schema/tables/library.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { UserTable } from 'src/schema/tables/user.table';
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
@Table('asset')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools';
import { DatabaseAction, EntityType } from 'src/enum';
import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools';
@Table('audit')
@Index({ columns: ['ownerId', 'createdAt'] })

View File

@@ -1,5 +1,5 @@
import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
@Table({ name: 'face_search' })
@Index({

View File

@@ -1,4 +1,4 @@
import { Column, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools';
import { Column, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools';
@Table({ name: 'geodata_places', primaryConstraintName: 'geodata_places_pkey' })
@Index({

View File

@@ -1,5 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -10,7 +8,9 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
@Table('library')
@UpdatedAtTrigger('library_updatedAt')

View File

@@ -1,6 +1,6 @@
import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('memory_asset_audit')
export class MemoryAssetAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { memory_asset_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import {
AfterDeleteTrigger,
CreateDateColumn,
@@ -10,7 +6,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { memory_asset_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
@Table('memory_asset')
@UpdatedAtTrigger('memory_asset_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('memory_audit')
export class MemoryAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { MemoryType } from 'src/enum';
import { memory_delete_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
@@ -13,7 +9,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { MemoryType } from 'src/enum';
import { memory_delete_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
@Table('memory')
@UpdatedAtTrigger('memory_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from '@immich/sql-tools';
import { PathType } from 'src/enum';
import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools';
@Table('move_history')
// path lock (per entity)

View File

@@ -1,4 +1,4 @@
import { Column, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
import { Column, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools';
@Table({ name: 'naturalearth_countries', primaryConstraintName: 'naturalearth_countries_pkey' })
export class NaturalEarthCountriesTable {

View File

@@ -1,6 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { NotificationLevel, NotificationType } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -11,7 +8,10 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { NotificationLevel, NotificationType } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
@Table('notification')
@UpdatedAtTrigger('notification_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
@Table('ocr_search')
@Index({

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('partner_audit')
export class PartnerAuditTable {

View File

@@ -1,6 +1,3 @@
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { partner_delete_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
@@ -10,7 +7,10 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { partner_delete_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
@Table('partner')
@UpdatedAtTrigger('partner_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('person_audit')
export class PersonAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { person_delete_audit } from 'src/schema/functions';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Check,
@@ -13,7 +9,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { person_delete_audit } from 'src/schema/functions';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table('person')
@UpdatedAtTrigger('person_updatedAt')

View File

@@ -1,4 +1,3 @@
import { PluginContext } from 'src/enum';
import {
Column,
CreateDateColumn,
@@ -9,7 +8,8 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { PluginContext } from 'src/enum';
import type { JSONSchema } from 'src/types/plugin-schema.types';
@Table('plugin')

View File

@@ -1,5 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -9,7 +7,9 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
@Table({ name: 'session' })
@UpdatedAtTrigger('session_updatedAt')

View File

@@ -1,6 +1,6 @@
import { ForeignKeyColumn, Table } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { ForeignKeyColumn, Table } from 'src/sql-tools';
@Table('shared_link_asset')
export class SharedLinkAssetTable {

View File

@@ -1,6 +1,3 @@
import { SharedLinkType } from 'src/enum';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -9,7 +6,10 @@ import {
PrimaryGeneratedColumn,
Table,
Timestamp,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { SharedLinkType } from 'src/enum';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table('shared_link')
export class SharedLinkTable {

View File

@@ -1,5 +1,5 @@
import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
@Table({ name: 'smart_search' })
@Index({

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('stack_audit')
export class StackAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { stack_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
CreateDateColumn,
@@ -11,7 +7,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { stack_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
@Table('stack')
@UpdatedAtTrigger('stack_updatedAt')

View File

@@ -1,6 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { SyncEntityType } from 'src/enum';
import { SessionTable } from 'src/schema/tables/session.table';
import {
Column,
CreateDateColumn,
@@ -10,7 +7,10 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { SyncEntityType } from 'src/enum';
import { SessionTable } from 'src/schema/tables/session.table';
@Table('session_sync_checkpoint')
@UpdatedAtTrigger('session_sync_checkpoint_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, PrimaryColumn, Table } from '@immich/sql-tools';
import { SystemMetadataKey } from 'src/enum';
import { Column, PrimaryColumn, Table } from 'src/sql-tools';
import { SystemMetadata } from 'src/types';
@Table('system_metadata')

View File

@@ -1,6 +1,6 @@
import { ForeignKeyColumn, Index, Table } from '@immich/sql-tools';
import { AssetTable } from 'src/schema/tables/asset.table';
import { TagTable } from 'src/schema/tables/tag.table';
import { ForeignKeyColumn, Index, Table } from 'src/sql-tools';
@Index({ columns: ['assetId', 'tagId'] })
@Table('tag_asset')

View File

@@ -1,5 +1,5 @@
import { ForeignKeyColumn, Table } from '@immich/sql-tools';
import { TagTable } from 'src/schema/tables/tag.table';
import { ForeignKeyColumn, Table } from 'src/sql-tools';
@Table('tag_closure')
export class TagClosureTable {

View File

@@ -1,5 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -10,7 +8,9 @@ import {
Timestamp,
Unique,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserTable } from 'src/schema/tables/user.table';
@Table('tag')
@UpdatedAtTrigger('tag_updatedAt')

View File

@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('user_audit')
export class UserAuditTable {

View File

@@ -1,6 +1,6 @@
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { UserMetadataKey } from 'src/enum';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('user_metadata_audit')
export class UserMetadataAuditTable {

View File

@@ -1,7 +1,3 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserMetadataKey } from 'src/enum';
import { user_metadata_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
@@ -11,7 +7,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserMetadataKey } from 'src/enum';
import { user_metadata_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
import { UserMetadata, UserMetadataItem } from 'src/types';
@UpdatedAtTrigger('user_metadata_updated_at')

View File

@@ -1,7 +1,3 @@
import { ColumnType } from 'kysely';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserAvatarColor, UserStatus } from 'src/enum';
import { user_delete_audit } from 'src/schema/functions';
import {
AfterDeleteTrigger,
Column,
@@ -13,7 +9,11 @@ import {
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { ColumnType } from 'kysely';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserAvatarColor, UserStatus } from 'src/enum';
import { user_delete_audit } from 'src/schema/functions';
@Table('user')
@UpdatedAtTrigger('user_updatedAt')

View File

@@ -1,4 +1,4 @@
import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from 'src/sql-tools';
import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from '@immich/sql-tools';
@Table('version_history')
export class VersionHistoryTable {

View File

@@ -1,6 +1,3 @@
import { PluginTriggerType } from 'src/enum';
import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
CreateDateColumn,
@@ -10,7 +7,10 @@ import {
PrimaryGeneratedColumn,
Table,
Timestamp,
} from 'src/sql-tools';
} from '@immich/sql-tools';
import { PluginTriggerType } from 'src/enum';
import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table';
import { UserTable } from 'src/schema/tables/user.table';
import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types';
@Table('workflow')

View File

@@ -1,3 +1,4 @@
import { schemaDiff } from '@immich/sql-tools';
import { Injectable } from '@nestjs/common';
import { isAbsolute, join } from 'node:path';
import { SALT_ROUNDS } from 'src/constants';
@@ -5,7 +6,6 @@ import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { schemaDiff } from 'src/sql-tools';
import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance';
import { getExternalDomain } from 'src/utils/misc';

View File

@@ -1,99 +0,0 @@
import { compareColumns } from 'src/sql-tools/comparers/column.comparer';
import { DatabaseColumn, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testColumn: DatabaseColumn = {
name: 'test',
tableName: 'table1',
primary: false,
nullable: false,
isArray: false,
type: 'character varying',
synchronize: true,
};
describe('compareColumns', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareColumns().onExtra(testColumn)).toEqual([
{
tableName: 'table1',
columnName: 'test',
type: 'ColumnDrop',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareColumns().onMissing(testColumn)).toEqual([
{
type: 'ColumnAdd',
column: testColumn,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareColumns().onCompare(testColumn, testColumn)).toEqual([]);
});
it('should detect a change in type', () => {
const source: DatabaseColumn = { ...testColumn };
const target: DatabaseColumn = { ...testColumn, type: 'text' };
const reason = 'column type is different (character varying vs text)';
expect(compareColumns().onCompare(source, target)).toEqual([
{
columnName: 'test',
tableName: 'table1',
type: 'ColumnDrop',
reason,
},
{
type: 'ColumnAdd',
column: source,
reason,
},
]);
});
it('should detect a change in default', () => {
const source: DatabaseColumn = { ...testColumn, nullable: true };
const target: DatabaseColumn = { ...testColumn, nullable: true, default: "''" };
const reason = `default is different (null vs '')`;
expect(compareColumns().onCompare(source, target)).toEqual([
{
columnName: 'test',
tableName: 'table1',
type: 'ColumnAlter',
changes: {
default: 'NULL',
},
reason,
},
]);
});
it('should detect a comment change', () => {
const source: DatabaseColumn = { ...testColumn, comment: 'new comment' };
const target: DatabaseColumn = { ...testColumn, comment: 'old comment' };
const reason = 'comment is different (new comment vs old comment)';
expect(compareColumns().onCompare(source, target)).toEqual([
{
columnName: 'test',
tableName: 'table1',
type: 'ColumnAlter',
changes: {
comment: 'new comment',
},
reason,
},
]);
});
});
});

View File

@@ -1,108 +0,0 @@
import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers';
import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
export const compareColumns = () =>
({
getRenameKey: (column) => {
return asRenameKey([
column.tableName,
column.type,
column.nullable,
column.default,
column.storage,
column.primary,
column.isArray,
column.length,
column.identity,
column.enumName,
column.numericPrecision,
column.numericScale,
]);
},
onRename: (source, target) => [
{
type: 'ColumnRename',
tableName: source.tableName,
oldName: target.name,
newName: source.name,
reason: Reason.Rename,
},
],
onMissing: (source) => [
{
type: 'ColumnAdd',
column: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'ColumnDrop',
tableName: target.tableName,
columnName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
const sourceType = getColumnType(source);
const targetType = getColumnType(target);
const isTypeChanged = sourceType !== targetType;
if (isTypeChanged) {
// TODO: convert between types via UPDATE when possible
return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`);
}
const items: SchemaDiff[] = [];
if (source.nullable !== target.nullable) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
nullable: source.nullable,
},
reason: `nullable is different (${source.nullable} vs ${target.nullable})`,
});
}
if (!isDefaultEqual(source, target)) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
default: String(source.default ?? 'NULL'),
},
reason: `default is different (${source.default ?? 'null'} vs ${target.default})`,
});
}
if (source.comment !== target.comment) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
comment: String(source.comment),
},
reason: `comment is different (${source.comment} vs ${target.comment})`,
});
}
return items;
},
}) satisfies Comparer<DatabaseColumn>;
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
return [
{
type: 'ColumnDrop',
tableName: target.tableName,
columnName: target.name,
reason,
},
{ type: 'ColumnAdd', column: source, reason },
];
};

View File

@@ -1,63 +0,0 @@
import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer';
import { ConstraintType, DatabaseConstraint, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testConstraint: DatabaseConstraint = {
type: ConstraintType.PRIMARY_KEY,
name: 'test',
tableName: 'table1',
columnNames: ['column1'],
synchronize: true,
};
describe('compareConstraints', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareConstraints().onExtra(testConstraint)).toEqual([
{
type: 'ConstraintDrop',
constraintName: 'test',
tableName: 'table1',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareConstraints().onMissing(testConstraint)).toEqual([
{
type: 'ConstraintAdd',
constraint: testConstraint,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareConstraints().onCompare(testConstraint, testConstraint)).toEqual([]);
});
it('should detect a change in type', () => {
const source: DatabaseConstraint = { ...testConstraint };
const target: DatabaseConstraint = { ...testConstraint, columnNames: ['column1', 'column2'] };
const reason = 'Primary key columns are different: (column1 vs column1,column2)';
expect(compareConstraints().onCompare(source, target)).toEqual([
{
constraintName: 'test',
tableName: 'table1',
type: 'ConstraintDrop',
reason,
},
{
type: 'ConstraintAdd',
constraint: source,
reason,
},
]);
});
});
});

View File

@@ -1,165 +0,0 @@
import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers';
import {
CompareFunction,
Comparer,
ConstraintType,
DatabaseCheckConstraint,
DatabaseConstraint,
DatabaseForeignKeyConstraint,
DatabasePrimaryKeyConstraint,
DatabaseUniqueConstraint,
Reason,
SchemaDiff,
} from 'src/sql-tools/types';
export const compareConstraints = (): Comparer<DatabaseConstraint> => ({
getRenameKey: (constraint) => {
switch (constraint.type) {
case ConstraintType.PRIMARY_KEY:
case ConstraintType.UNIQUE: {
return asRenameKey([constraint.type, constraint.tableName, ...constraint.columnNames.toSorted()]);
}
case ConstraintType.FOREIGN_KEY: {
return asRenameKey([
constraint.type,
constraint.tableName,
...constraint.columnNames.toSorted(),
constraint.referenceTableName,
...constraint.referenceColumnNames.toSorted(),
]);
}
case ConstraintType.CHECK: {
const expression = constraint.expression.replaceAll('(', '').replaceAll(')', '');
return asRenameKey([constraint.type, constraint.tableName, expression]);
}
}
},
onRename: (source, target) => [
{
type: 'ConstraintRename',
tableName: target.tableName,
oldName: target.name,
newName: source.name,
reason: Reason.Rename,
},
],
onMissing: (source) => [
{
type: 'ConstraintAdd',
constraint: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'ConstraintDrop',
tableName: target.tableName,
constraintName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
switch (source.type) {
case ConstraintType.PRIMARY_KEY: {
return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint);
}
case ConstraintType.FOREIGN_KEY: {
return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint);
}
case ConstraintType.UNIQUE: {
return compareUniqueConstraint(source, target as DatabaseUniqueConstraint);
}
case ConstraintType.CHECK: {
return compareCheckConstraint(source, target as DatabaseCheckConstraint);
}
default: {
return [];
}
}
},
});
const comparePrimaryKeyConstraint: CompareFunction<DatabasePrimaryKeyConstraint> = (source, target) => {
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
return dropAndRecreateConstraint(
source,
target,
`Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`,
);
}
return [];
};
const compareForeignKeyConstraint: CompareFunction<DatabaseForeignKeyConstraint> = (source, target) => {
let reason = '';
const sourceDeleteAction = source.onDelete ?? 'NO ACTION';
const targetDeleteAction = target.onDelete ?? 'NO ACTION';
const sourceUpdateAction = source.onUpdate ?? 'NO ACTION';
const targetUpdateAction = target.onUpdate ?? 'NO ACTION';
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
} else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) {
reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`;
} else if (source.referenceTableName !== target.referenceTableName) {
reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`;
} else if (sourceDeleteAction !== targetDeleteAction) {
reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`;
} else if (sourceUpdateAction !== targetUpdateAction) {
reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`;
}
if (reason) {
return dropAndRecreateConstraint(source, target, reason);
}
return [];
};
const compareUniqueConstraint: CompareFunction<DatabaseUniqueConstraint> = (source, target) => {
let reason = '';
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
}
if (reason) {
return dropAndRecreateConstraint(source, target, reason);
}
return [];
};
const compareCheckConstraint: CompareFunction<DatabaseCheckConstraint> = (source, target) => {
if (source.expression !== target.expression) {
// comparing expressions is hard because postgres reconstructs it with different formatting
// for now if the constraint exists with the same name, we will just skip it
}
return [];
};
const dropAndRecreateConstraint = (
source: DatabaseConstraint,
target: DatabaseConstraint,
reason: string,
): SchemaDiff[] => {
return [
{
type: 'ConstraintDrop',
tableName: target.tableName,
constraintName: target.name,
reason,
},
{ type: 'ConstraintAdd', constraint: source, reason },
];
};

View File

@@ -1,54 +0,0 @@
import { compareEnums } from 'src/sql-tools/comparers/enum.comparer';
import { DatabaseEnum, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testEnum: DatabaseEnum = { name: 'test', values: ['foo', 'bar'], synchronize: true };
describe('compareEnums', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareEnums().onExtra(testEnum)).toEqual([
{
enumName: 'test',
type: 'EnumDrop',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareEnums().onMissing(testEnum)).toEqual([
{
type: 'EnumCreate',
enum: testEnum,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareEnums().onCompare(testEnum, testEnum)).toEqual([]);
});
it('should drop and recreate when values list is different', () => {
const source = { name: 'test', values: ['foo', 'bar'], synchronize: true };
const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true };
expect(compareEnums().onCompare(source, target)).toEqual([
{
enumName: 'test',
type: 'EnumDrop',
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
},
{
type: 'EnumCreate',
enum: source,
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
},
]);
});
});
});

View File

@@ -1,38 +0,0 @@
import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types';
export const compareEnums = (): Comparer<DatabaseEnum> => ({
onMissing: (source) => [
{
type: 'EnumCreate',
enum: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'EnumDrop',
enumName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
if (source.values.toString() !== target.values.toString()) {
// TODO add or remove values if the lists are different or the order has changed
const reason = `enum values has changed (${source.values} vs ${target.values})`;
return [
{
type: 'EnumDrop',
enumName: source.name,
reason,
},
{
type: 'EnumCreate',
enum: source,
reason,
},
];
}
return [];
},
});

View File

@@ -1,37 +0,0 @@
import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer';
import { Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testExtension = { name: 'test', synchronize: true };
describe('compareExtensions', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareExtensions().onExtra(testExtension)).toEqual([
{
extensionName: 'test',
type: 'ExtensionDrop',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareExtensions().onMissing(testExtension)).toEqual([
{
type: 'ExtensionCreate',
extension: testExtension,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareExtensions().onCompare(testExtension, testExtension)).toEqual([]);
});
});
});

View File

@@ -1,22 +0,0 @@
import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types';
export const compareExtensions = (): Comparer<DatabaseExtension> => ({
onMissing: (source) => [
{
type: 'ExtensionCreate',
extension: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'ExtensionDrop',
extensionName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: () => {
// if the name matches they are the same
return [];
},
});

View File

@@ -1,53 +0,0 @@
import { compareFunctions } from 'src/sql-tools/comparers/function.comparer';
import { DatabaseFunction, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testFunction: DatabaseFunction = {
name: 'test',
expression: 'CREATE FUNCTION something something something',
synchronize: true,
};
describe('compareFunctions', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareFunctions().onExtra(testFunction)).toEqual([
{
functionName: 'test',
type: 'FunctionDrop',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareFunctions().onMissing(testFunction)).toEqual([
{
type: 'FunctionCreate',
function: testFunction,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should ignore functions with the same hash', () => {
expect(compareFunctions().onCompare(testFunction, testFunction)).toEqual([]);
});
it('should report differences if functions have different hashes', () => {
const source: DatabaseFunction = { ...testFunction, expression: 'SELECT 1' };
const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' };
expect(compareFunctions().onCompare(source, target)).toEqual([
{
type: 'FunctionCreate',
reason: 'function expression has changed (SELECT 1 vs SELECT 2)',
function: source,
},
]);
});
});
});

View File

@@ -1,32 +0,0 @@
import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types';
export const compareFunctions = (): Comparer<DatabaseFunction> => ({
onMissing: (source) => [
{
type: 'FunctionCreate',
function: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'FunctionDrop',
functionName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
if (source.expression !== target.expression) {
const reason = `function expression has changed (${source.expression} vs ${target.expression})`;
return [
{
type: 'FunctionCreate',
function: source,
reason,
},
];
}
return [];
},
});

View File

@@ -1,72 +0,0 @@
import { compareIndexes } from 'src/sql-tools/comparers/index.comparer';
import { DatabaseIndex, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testIndex: DatabaseIndex = {
name: 'test',
tableName: 'table1',
columnNames: ['column1', 'column2'],
unique: false,
synchronize: true,
};
describe('compareIndexes', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareIndexes().onExtra(testIndex)).toEqual([
{
type: 'IndexDrop',
indexName: 'test',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareIndexes().onMissing(testIndex)).toEqual([
{
type: 'IndexCreate',
index: testIndex,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareIndexes().onCompare(testIndex, testIndex)).toEqual([]);
});
it('should drop and recreate when column list is different', () => {
const source = {
name: 'test',
tableName: 'table1',
columnNames: ['column1'],
unique: true,
synchronize: true,
};
const target = {
name: 'test',
tableName: 'table1',
columnNames: ['column1', 'column2'],
unique: true,
synchronize: true,
};
expect(compareIndexes().onCompare(source, target)).toEqual([
{
indexName: 'test',
type: 'IndexDrop',
reason: 'columns are different (column1 vs column1,column2)',
},
{
type: 'IndexCreate',
index: source,
reason: 'columns are different (column1 vs column1,column2)',
},
]);
});
});
});

View File

@@ -1,62 +0,0 @@
import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers';
import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
export const compareIndexes = (): Comparer<DatabaseIndex> => ({
getRenameKey: (index) => {
if (index.override) {
return index.override.value.sql.replace(index.name, 'INDEX_NAME');
}
return asRenameKey([index.tableName, ...(index.columnNames || []), index.unique]);
},
onRename: (source, target) => [
{
type: 'IndexRename',
tableName: source.tableName,
oldName: target.name,
newName: source.name,
reason: Reason.Rename,
},
],
onMissing: (source) => [
{
type: 'IndexCreate',
index: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'IndexDrop',
indexName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
const sourceUsing = source.using ?? 'btree';
const targetUsing = target.using ?? 'btree';
let reason = '';
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
} else if (source.unique !== target.unique) {
reason = `uniqueness is different (${source.unique} vs ${target.unique})`;
} else if (sourceUsing !== targetUsing) {
reason = `using method is different (${source.using} vs ${target.using})`;
} else if (source.where !== target.where) {
reason = `where clause is different (${source.where} vs ${target.where})`;
} else if (source.expression !== target.expression) {
reason = `expression is different (${source.expression} vs ${target.expression})`;
}
if (reason) {
return [
{ type: 'IndexDrop', indexName: target.name, reason },
{ type: 'IndexCreate', index: source, reason },
];
}
return [];
},
});

View File

@@ -1,69 +0,0 @@
import { compareOverrides } from 'src/sql-tools/comparers/override.comparer';
import { DatabaseOverride, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testOverride: DatabaseOverride = {
name: 'test',
value: { type: 'function', name: 'test_func', sql: 'func implementation' },
synchronize: true,
};
describe('compareOverrides', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareOverrides().onExtra(testOverride)).toEqual([
{
type: 'OverrideDrop',
overrideName: 'test',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareOverrides().onMissing(testOverride)).toEqual([
{
type: 'OverrideCreate',
override: testOverride,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareOverrides().onCompare(testOverride, testOverride)).toEqual([]);
});
it('should drop and recreate when the value changes', () => {
const source: DatabaseOverride = {
name: 'test',
value: {
type: 'function',
name: 'test_func',
sql: 'func implementation',
},
synchronize: true,
};
const target: DatabaseOverride = {
name: 'test',
value: {
type: 'function',
name: 'test_func',
sql: 'func implementation2',
},
synchronize: true,
};
expect(compareOverrides().onCompare(source, target)).toEqual([
{
override: source,
type: 'OverrideUpdate',
reason: expect.stringContaining('value is different'),
},
]);
});
});
});

View File

@@ -1,29 +0,0 @@
import { Comparer, DatabaseOverride, Reason } from 'src/sql-tools/types';
export const compareOverrides = (): Comparer<DatabaseOverride> => ({
onMissing: (source) => [
{
type: 'OverrideCreate',
override: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'OverrideDrop',
overrideName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
if (source.value.name !== target.value.name || source.value.sql !== target.value.sql) {
const sourceValue = JSON.stringify(source.value);
const targetValue = JSON.stringify(target.value);
return [
{ type: 'OverrideUpdate', override: source, reason: `value is different (${sourceValue} vs ${targetValue})` },
];
}
return [];
},
});

View File

@@ -1,44 +0,0 @@
import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer';
import { DatabaseParameter, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testParameter: DatabaseParameter = {
name: 'test',
databaseName: 'immich',
value: 'on',
scope: 'database',
synchronize: true,
};
describe('compareParameters', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareParameters().onExtra(testParameter)).toEqual([
{
type: 'ParameterReset',
databaseName: 'immich',
parameterName: 'test',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareParameters().onMissing(testParameter)).toEqual([
{
type: 'ParameterSet',
parameter: testParameter,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareParameters().onCompare(testParameter, testParameter)).toEqual([]);
});
});
});

View File

@@ -1,23 +0,0 @@
import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types';
export const compareParameters = (): Comparer<DatabaseParameter> => ({
onMissing: (source) => [
{
type: 'ParameterSet',
parameter: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'ParameterReset',
databaseName: target.databaseName,
parameterName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: () => {
// TODO
return [];
},
});

View File

@@ -1,44 +0,0 @@
import { compareTables } from 'src/sql-tools/comparers/table.comparer';
import { DatabaseTable, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testTable: DatabaseTable = {
name: 'test',
columns: [],
constraints: [],
indexes: [],
triggers: [],
synchronize: true,
};
describe('compareParameters', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareTables({}).onExtra(testTable)).toEqual([
{
type: 'TableDrop',
tableName: 'test',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareTables({}).onMissing(testTable)).toEqual([
{
type: 'TableCreate',
table: testTable,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareTables({}).onCompare(testTable, testTable)).toEqual([]);
});
});
});

View File

@@ -1,31 +0,0 @@
import { compareColumns } from 'src/sql-tools/comparers/column.comparer';
import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer';
import { compareIndexes } from 'src/sql-tools/comparers/index.comparer';
import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer';
import { compare } from 'src/sql-tools/helpers';
import { Comparer, DatabaseTable, Reason, SchemaDiffOptions } from 'src/sql-tools/types';
export const compareTables = (options: SchemaDiffOptions): Comparer<DatabaseTable> => ({
onMissing: (source) => [
{
type: 'TableCreate',
table: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'TableDrop',
tableName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
return [
...compare(source.columns, target.columns, options.columns, compareColumns()),
...compare(source.indexes, target.indexes, options.indexes, compareIndexes()),
...compare(source.constraints, target.constraints, options.constraints, compareConstraints()),
...compare(source.triggers, target.triggers, options.triggers, compareTriggers()),
];
},
});

View File

@@ -1,88 +0,0 @@
import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer';
import { DatabaseTrigger, Reason } from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';
const testTrigger: DatabaseTrigger = {
name: 'test',
tableName: 'table1',
timing: 'before',
actions: ['delete'],
scope: 'row',
functionName: 'my_trigger_function',
synchronize: true,
};
describe('compareTriggers', () => {
describe('onExtra', () => {
it('should work', () => {
expect(compareTriggers().onExtra(testTrigger)).toEqual([
{
type: 'TriggerDrop',
tableName: 'table1',
triggerName: 'test',
reason: Reason.MissingInSource,
},
]);
});
});
describe('onMissing', () => {
it('should work', () => {
expect(compareTriggers().onMissing(testTrigger)).toEqual([
{
type: 'TriggerCreate',
trigger: testTrigger,
reason: Reason.MissingInTarget,
},
]);
});
});
describe('onCompare', () => {
it('should work', () => {
expect(compareTriggers().onCompare(testTrigger, testTrigger)).toEqual([]);
});
it('should detect a change in function name', () => {
const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' };
const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' };
const reason = `function is different (my_new_name vs my_old_name)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
it('should detect a change in actions', () => {
const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] };
const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] };
const reason = `action is different (delete vs delete,insert)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
it('should detect a change in timing', () => {
const source: DatabaseTrigger = { ...testTrigger, timing: 'before' };
const target: DatabaseTrigger = { ...testTrigger, timing: 'after' };
const reason = `timing method is different (before vs after)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
it('should detect a change in scope', () => {
const source: DatabaseTrigger = { ...testTrigger, scope: 'row' };
const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' };
const reason = `scope is different (row vs statement)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
it('should detect a change in new table reference', () => {
const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' };
const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined };
const reason = `new table reference is different (new_table vs undefined)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
it('should detect a change in old table reference', () => {
const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' };
const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined };
const reason = `old table reference is different (old_table vs undefined)`;
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]);
});
});
});

View File

@@ -1,41 +0,0 @@
import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types';
export const compareTriggers = (): Comparer<DatabaseTrigger> => ({
onMissing: (source) => [
{
type: 'TriggerCreate',
trigger: source,
reason: Reason.MissingInTarget,
},
],
onExtra: (target) => [
{
type: 'TriggerDrop',
tableName: target.tableName,
triggerName: target.name,
reason: Reason.MissingInSource,
},
],
onCompare: (source, target) => {
let reason = '';
if (source.functionName !== target.functionName) {
reason = `function is different (${source.functionName} vs ${target.functionName})`;
} else if (source.actions.join(' OR ') !== target.actions.join(' OR ')) {
reason = `action is different (${source.actions} vs ${target.actions})`;
} else if (source.timing !== target.timing) {
reason = `timing method is different (${source.timing} vs ${target.timing})`;
} else if (source.scope !== target.scope) {
reason = `scope is different (${source.scope} vs ${target.scope})`;
} else if (source.referencingNewTableAs !== target.referencingNewTableAs) {
reason = `new table reference is different (${source.referencingNewTableAs} vs ${target.referencingNewTableAs})`;
} else if (source.referencingOldTableAs !== target.referencingOldTableAs) {
reason = `old table reference is different (${source.referencingOldTableAs} vs ${target.referencingOldTableAs})`;
}
if (reason) {
return [{ type: 'TriggerCreate', trigger: source, reason }];
}
return [];
},
});

View File

@@ -1,104 +0,0 @@
import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming';
import { HashNamingStrategy } from 'src/sql-tools/naming/hash.naming';
import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface';
import {
BaseContextOptions,
DatabaseEnum,
DatabaseExtension,
DatabaseFunction,
DatabaseOverride,
DatabaseParameter,
DatabaseSchema,
DatabaseTable,
} from 'src/sql-tools/types';
const asOverrideKey = (type: string, name: string) => `${type}:${name}`;
const isNamingInterface = (strategy: any): strategy is NamingInterface => {
return typeof strategy === 'object' && typeof strategy.getName === 'function';
};
const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => {
if (isNamingInterface(strategy)) {
return strategy;
}
switch (strategy) {
case 'hash': {
return new HashNamingStrategy();
}
default: {
return new DefaultNamingStrategy();
}
}
};
export class BaseContext {
databaseName: string;
schemaName: string;
overrideTableName: string;
tables: DatabaseTable[] = [];
functions: DatabaseFunction[] = [];
enums: DatabaseEnum[] = [];
extensions: DatabaseExtension[] = [];
parameters: DatabaseParameter[] = [];
overrides: DatabaseOverride[] = [];
warnings: string[] = [];
private namingStrategy: NamingInterface;
constructor(options: BaseContextOptions) {
this.databaseName = options.databaseName ?? 'postgres';
this.schemaName = options.schemaName ?? 'public';
this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash');
}
getNameFor(item: NamingItem) {
return this.namingStrategy.getName(item);
}
getTableByName(name: string) {
return this.tables.find((table) => table.name === name);
}
warn(context: string, message: string) {
this.warnings.push(`[${context}] ${message}`);
}
build(): DatabaseSchema {
const overrideMap = new Map<string, DatabaseOverride>();
for (const override of this.overrides) {
const { type, name } = override.value;
overrideMap.set(asOverrideKey(type, name), override);
}
for (const func of this.functions) {
func.override = overrideMap.get(asOverrideKey('function', func.name));
}
for (const { indexes, triggers } of this.tables) {
for (const index of indexes) {
index.override = overrideMap.get(asOverrideKey('index', index.name));
}
for (const trigger of triggers) {
trigger.override = overrideMap.get(asOverrideKey('trigger', trigger.name));
}
}
return {
databaseName: this.databaseName,
schemaName: this.schemaName,
tables: this.tables,
functions: this.functions,
enums: this.enums,
extensions: this.extensions,
parameters: this.parameters,
overrides: this.overrides,
warnings: this.warnings,
};
}
}

View File

@@ -1,71 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { BaseContext } from 'src/sql-tools/contexts/base-context';
import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
import { TableOptions } from 'src/sql-tools/decorators/table.decorator';
import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types';
type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> };
export class ProcessorContext extends BaseContext {
constructor(public options: SchemaFromCodeOptions) {
options.createForeignKeyIndexes = options.createForeignKeyIndexes ?? true;
options.overrides = options.overrides ?? false;
super(options);
}
classToTable: WeakMap<Function, DatabaseTable> = new WeakMap();
tableToMetadata: WeakMap<DatabaseTable, TableMetadata> = new WeakMap();
getTableByObject(object: Function) {
return this.classToTable.get(object);
}
getTableMetadata(table: DatabaseTable) {
const metadata = this.tableToMetadata.get(table);
if (!metadata) {
throw new Error(`Table metadata not found for table: ${table.name}`);
}
return metadata;
}
addTable(table: DatabaseTable, options: TableOptions, object: Function) {
this.tables.push(table);
this.classToTable.set(object, table);
this.tableToMetadata.set(table, { options, object, methodToColumn: new Map() });
}
getColumnByObjectAndPropertyName(
object: object,
propertyName: string | symbol,
): { table?: DatabaseTable; column?: DatabaseColumn } {
const table = this.getTableByObject(object.constructor);
if (!table) {
return {};
}
const tableMetadata = this.tableToMetadata.get(table);
if (!tableMetadata) {
return {};
}
const column = tableMetadata.methodToColumn.get(propertyName);
return { table, column };
}
addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) {
table.columns.push(column);
const tableMetadata = this.getTableMetadata(table);
tableMetadata.methodToColumn.set(propertyName, column);
}
warnMissingTable(context: string, object: object, propertyName?: symbol | string) {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
this.warn(context, `Unable to find table (${label})`);
}
warnMissingColumn(context: string, object: object, propertyName?: symbol | string) {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
this.warn(context, `Unable to find column (${label})`);
}
}

View File

@@ -1,8 +0,0 @@
import { BaseContext } from 'src/sql-tools/contexts/base-context';
import { SchemaFromDatabaseOptions } from 'src/sql-tools/types';
export class ReaderContext extends BaseContext {
constructor(public options: SchemaFromDatabaseOptions) {
super(options);
}
}

View File

@@ -1,8 +0,0 @@
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
export const AfterDeleteTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
TriggerFunction({
timing: 'after',
actions: ['delete'],
...options,
});

View File

@@ -1,8 +0,0 @@
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
export const AfterInsertTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
TriggerFunction({
timing: 'after',
actions: ['insert'],
...options,
});

View File

@@ -1,8 +0,0 @@
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
export const BeforeUpdateTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
TriggerFunction({
timing: 'before',
actions: ['update'],
...options,
});

View File

@@ -1,11 +0,0 @@
import { register } from 'src/sql-tools/register';
export type CheckOptions = {
name?: string;
expression: string;
synchronize?: boolean;
};
export const Check = (options: CheckOptions): ClassDecorator => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } });
};

View File

@@ -1,32 +0,0 @@
import { asOptions } from 'src/sql-tools/helpers';
import { register } from 'src/sql-tools/register';
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
export type ColumnValue = null | boolean | string | number | Array<unknown> | object | Date | (() => string);
export type ColumnBaseOptions = {
name?: string;
primary?: boolean;
type?: ColumnType;
nullable?: boolean;
length?: number;
default?: ColumnValue;
comment?: string;
synchronize?: boolean;
storage?: ColumnStorage;
identity?: boolean;
index?: boolean;
indexName?: string;
unique?: boolean;
uniqueConstraintName?: string;
};
export type ColumnOptions = ColumnBaseOptions & {
enum?: DatabaseEnum;
array?: boolean;
};
export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => {
return (object: object, propertyName: string | symbol) =>
void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } });
};

View File

@@ -1,14 +0,0 @@
import { ColumnValue } from 'src/sql-tools/decorators/column.decorator';
import { register } from 'src/sql-tools/register';
import { ParameterScope } from 'src/sql-tools/types';
export type ConfigurationParameterOptions = {
name: string;
value: ColumnValue;
scope: ParameterScope;
synchronize?: boolean;
};
export const ConfigurationParameter = (options: ConfigurationParameterOptions): ClassDecorator => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (object: Function) => void register({ type: 'configurationParameter', item: { object, options } });
};

View File

@@ -1,9 +0,0 @@
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
return Column({
type: 'timestamp with time zone',
default: () => 'now()',
...options,
});
};

View File

@@ -1,10 +0,0 @@
import { register } from 'src/sql-tools/register';
export type DatabaseOptions = {
name?: string;
synchronize?: boolean;
};
export const Database = (options: DatabaseOptions): ClassDecorator => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (object: Function) => void register({ type: 'database', item: { object, options } });
};

View File

@@ -1,9 +0,0 @@
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
return Column({
type: 'timestamp with time zone',
nullable: true,
...options,
});
};

View File

@@ -1,11 +0,0 @@
import { asOptions } from 'src/sql-tools/helpers';
import { register } from 'src/sql-tools/register';
export type ExtensionOptions = {
name: string;
synchronize?: boolean;
};
export const Extension = (options: string | ExtensionOptions): ClassDecorator => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (object: Function) => void register({ type: 'extension', item: { object, options: asOptions(options) } });
};

Some files were not shown because too many files have changed in this diff Show More