mirror of
https://github.com/immich-app/immich.git
synced 2025-12-13 09:13:17 +03:00
690 lines
18 KiB
TypeScript
690 lines
18 KiB
TypeScript
import { schemaDiff } from 'src/sql-tools/schema-diff';
|
|
import {
|
|
ActionType,
|
|
ColumnType,
|
|
ConstraintType,
|
|
DatabaseColumn,
|
|
DatabaseConstraint,
|
|
DatabaseIndex,
|
|
DatabaseSchema,
|
|
DatabaseTable,
|
|
} from 'src/sql-tools/types';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): DatabaseSchema => {
|
|
const tableName = 'table1';
|
|
|
|
return {
|
|
databaseName: 'postgres',
|
|
schemaName: 'public',
|
|
functions: [],
|
|
enums: [],
|
|
extensions: [],
|
|
parameters: [],
|
|
overrides: [],
|
|
tables: [
|
|
{
|
|
name: tableName,
|
|
columns: [
|
|
{
|
|
name: 'column1',
|
|
primary: false,
|
|
synchronize: true,
|
|
isArray: false,
|
|
type: 'character varying',
|
|
nullable: false,
|
|
...column,
|
|
tableName,
|
|
},
|
|
],
|
|
indexes: [],
|
|
triggers: [],
|
|
constraints: [],
|
|
synchronize: true,
|
|
},
|
|
],
|
|
warnings: [],
|
|
};
|
|
};
|
|
|
|
const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
|
|
const tableName = constraint?.tableName || 'table1';
|
|
|
|
return {
|
|
databaseName: 'postgres',
|
|
schemaName: 'public',
|
|
functions: [],
|
|
enums: [],
|
|
extensions: [],
|
|
parameters: [],
|
|
overrides: [],
|
|
tables: [
|
|
{
|
|
name: tableName,
|
|
columns: [
|
|
{
|
|
name: 'column1',
|
|
primary: false,
|
|
synchronize: true,
|
|
isArray: false,
|
|
type: 'character varying',
|
|
nullable: false,
|
|
tableName,
|
|
},
|
|
],
|
|
indexes: [],
|
|
triggers: [],
|
|
constraints: constraint ? [constraint] : [],
|
|
synchronize: true,
|
|
},
|
|
],
|
|
warnings: [],
|
|
};
|
|
};
|
|
|
|
const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
|
|
const tableName = index?.tableName || 'table1';
|
|
|
|
return {
|
|
databaseName: 'postgres',
|
|
schemaName: 'public',
|
|
functions: [],
|
|
enums: [],
|
|
extensions: [],
|
|
parameters: [],
|
|
overrides: [],
|
|
tables: [
|
|
{
|
|
name: tableName,
|
|
columns: [
|
|
{
|
|
name: 'column1',
|
|
primary: false,
|
|
synchronize: true,
|
|
isArray: false,
|
|
type: 'character varying',
|
|
nullable: false,
|
|
tableName,
|
|
},
|
|
],
|
|
indexes: index ? [index] : [],
|
|
constraints: [],
|
|
triggers: [],
|
|
synchronize: true,
|
|
},
|
|
],
|
|
warnings: [],
|
|
};
|
|
};
|
|
|
|
const newSchema = (schema: {
|
|
name?: string;
|
|
tables: Array<{
|
|
name: string;
|
|
columns?: Array<{
|
|
name: string;
|
|
type?: ColumnType;
|
|
nullable?: boolean;
|
|
isArray?: boolean;
|
|
}>;
|
|
indexes?: DatabaseIndex[];
|
|
constraints?: DatabaseConstraint[];
|
|
}>;
|
|
}): DatabaseSchema => {
|
|
const tables: DatabaseTable[] = [];
|
|
|
|
for (const table of schema.tables || []) {
|
|
const tableName = table.name;
|
|
const columns: DatabaseColumn[] = [];
|
|
|
|
for (const column of table.columns || []) {
|
|
const columnName = column.name;
|
|
|
|
columns.push({
|
|
tableName,
|
|
name: columnName,
|
|
primary: false,
|
|
type: column.type || 'character varying',
|
|
isArray: column.isArray ?? false,
|
|
nullable: column.nullable ?? false,
|
|
synchronize: true,
|
|
});
|
|
}
|
|
|
|
tables.push({
|
|
name: tableName,
|
|
columns,
|
|
indexes: table.indexes ?? [],
|
|
constraints: table.constraints ?? [],
|
|
triggers: [],
|
|
synchronize: true,
|
|
});
|
|
}
|
|
|
|
return {
|
|
databaseName: 'immich',
|
|
schemaName: schema?.name || 'public',
|
|
functions: [],
|
|
enums: [],
|
|
extensions: [],
|
|
parameters: [],
|
|
overrides: [],
|
|
tables,
|
|
warnings: [],
|
|
};
|
|
};
|
|
|
|
describe(schemaDiff.name, () => {
|
|
it('should work', () => {
|
|
const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] }));
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
|
|
describe('table', () => {
|
|
describe('TableCreate', () => {
|
|
it('should find a missing table', () => {
|
|
const column: DatabaseColumn = {
|
|
type: 'character varying',
|
|
tableName: 'table1',
|
|
primary: false,
|
|
name: 'column1',
|
|
isArray: false,
|
|
nullable: false,
|
|
synchronize: true,
|
|
};
|
|
const diff = schemaDiff(
|
|
newSchema({ tables: [{ name: 'table1', columns: [column] }] }),
|
|
newSchema({ tables: [] }),
|
|
);
|
|
|
|
expect(diff.items).toHaveLength(1);
|
|
expect(diff.items[0]).toEqual({
|
|
type: 'TableCreate',
|
|
table: {
|
|
name: 'table1',
|
|
columns: [column],
|
|
constraints: [],
|
|
indexes: [],
|
|
triggers: [],
|
|
synchronize: true,
|
|
},
|
|
reason: 'missing in target',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('TableDrop', () => {
|
|
it('should find an extra table', () => {
|
|
const diff = schemaDiff(
|
|
newSchema({ tables: [] }),
|
|
newSchema({
|
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
|
}),
|
|
{ tables: { ignoreExtra: false } },
|
|
);
|
|
|
|
expect(diff.items).toHaveLength(1);
|
|
expect(diff.items[0]).toEqual({
|
|
type: 'TableDrop',
|
|
tableName: 'table1',
|
|
reason: 'missing in source',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should skip identical tables', () => {
|
|
const diff = schemaDiff(
|
|
newSchema({
|
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
|
}),
|
|
newSchema({
|
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('column', () => {
|
|
describe('ColumnAdd', () => {
|
|
it('should find a new column', () => {
|
|
const diff = schemaDiff(
|
|
newSchema({
|
|
tables: [
|
|
{
|
|
name: 'table1',
|
|
columns: [{ name: 'column1' }, { name: 'column2' }],
|
|
},
|
|
],
|
|
}),
|
|
newSchema({
|
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ColumnAdd',
|
|
column: {
|
|
tableName: 'table1',
|
|
isArray: false,
|
|
primary: false,
|
|
name: 'column2',
|
|
nullable: false,
|
|
type: 'character varying',
|
|
synchronize: true,
|
|
},
|
|
reason: 'missing in target',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('ColumnDrop', () => {
|
|
it('should find an extra column', () => {
|
|
const diff = schemaDiff(
|
|
newSchema({
|
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
|
}),
|
|
newSchema({
|
|
tables: [
|
|
{
|
|
name: 'table1',
|
|
columns: [{ name: 'column1' }, { name: 'column2' }],
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ColumnDrop',
|
|
tableName: 'table1',
|
|
columnName: 'column2',
|
|
reason: 'missing in source',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('nullable', () => {
|
|
it('should make a column nullable', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', nullable: true }),
|
|
fromColumn({ name: 'column1', nullable: false }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ColumnAlter',
|
|
tableName: 'table1',
|
|
columnName: 'column1',
|
|
changes: {
|
|
nullable: true,
|
|
},
|
|
reason: 'nullable is different (true vs false)',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should make a column non-nullable', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', nullable: false }),
|
|
fromColumn({ name: 'column1', nullable: true }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ColumnAlter',
|
|
tableName: 'table1',
|
|
columnName: 'column1',
|
|
changes: {
|
|
nullable: false,
|
|
},
|
|
reason: 'nullable is different (false vs true)',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('default', () => {
|
|
it('should set a default value to a function', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }),
|
|
fromColumn({ name: 'column1' }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ColumnAlter',
|
|
tableName: 'table1',
|
|
columnName: 'column1',
|
|
changes: {
|
|
default: 'uuid_generate_v4()',
|
|
},
|
|
reason: 'default is different (uuid_generate_v4() vs undefined)',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should ignore explicit casts for strings', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', type: 'character varying', default: `''` }),
|
|
fromColumn({ name: 'column1', type: 'character varying', default: `''::character varying` }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
|
|
it('should ignore explicit casts for numbers', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', type: 'bigint', default: `0` }),
|
|
fromColumn({ name: 'column1', type: 'bigint', default: `'0'::bigint` }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
|
|
it('should ignore explicit casts for enums', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `test` }),
|
|
fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `'test'::enum1` }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
|
|
it('should support arrays, ignoring types', () => {
|
|
const diff = schemaDiff(
|
|
fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }),
|
|
fromColumn({
|
|
name: 'column1',
|
|
type: 'character varying',
|
|
isArray: true,
|
|
default: "'{}'::character varying[]",
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('constraint', () => {
|
|
describe('ConstraintAdd', () => {
|
|
it('should detect a new constraint', () => {
|
|
const diff = schemaDiff(
|
|
fromConstraint({
|
|
name: 'PK_test',
|
|
type: ConstraintType.PRIMARY_KEY,
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
synchronize: true,
|
|
}),
|
|
fromConstraint(),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ConstraintAdd',
|
|
constraint: {
|
|
type: ConstraintType.PRIMARY_KEY,
|
|
name: 'PK_test',
|
|
columnNames: ['id'],
|
|
tableName: 'table1',
|
|
synchronize: true,
|
|
},
|
|
reason: 'missing in target',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('ConstraintDrop', () => {
|
|
it('should detect an extra constraint', () => {
|
|
const diff = schemaDiff(
|
|
fromConstraint(),
|
|
fromConstraint({
|
|
name: 'PK_test',
|
|
type: ConstraintType.PRIMARY_KEY,
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
synchronize: true,
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'ConstraintDrop',
|
|
tableName: 'table1',
|
|
constraintName: 'PK_test',
|
|
reason: 'missing in source',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('primary key', () => {
|
|
it('should skip identical primary key constraints', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.PRIMARY_KEY,
|
|
name: 'PK_test',
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('foreign key', () => {
|
|
it('should skip identical foreign key constraints', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.FOREIGN_KEY,
|
|
name: 'FK_test',
|
|
tableName: 'table1',
|
|
columnNames: ['parentId'],
|
|
referenceTableName: 'table2',
|
|
referenceColumnNames: ['id'],
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(fromConstraint(constraint), fromConstraint(constraint));
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
|
|
it('should drop and recreate when the column changes', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.FOREIGN_KEY,
|
|
name: 'FK_test',
|
|
tableName: 'table1',
|
|
columnNames: ['parentId'],
|
|
referenceTableName: 'table2',
|
|
referenceColumnNames: ['id'],
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(
|
|
fromConstraint(constraint),
|
|
fromConstraint({ ...constraint, columnNames: ['parentId2'] }),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
constraintName: 'FK_test',
|
|
reason: 'columns are different (parentId vs parentId2)',
|
|
tableName: 'table1',
|
|
type: 'ConstraintDrop',
|
|
},
|
|
{
|
|
constraint: {
|
|
columnNames: ['parentId'],
|
|
name: 'FK_test',
|
|
referenceColumnNames: ['id'],
|
|
referenceTableName: 'table2',
|
|
synchronize: true,
|
|
tableName: 'table1',
|
|
type: 'foreign-key',
|
|
},
|
|
reason: 'columns are different (parentId vs parentId2)',
|
|
type: 'ConstraintAdd',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should drop and recreate when the ON DELETE action changes', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.FOREIGN_KEY,
|
|
name: 'FK_test',
|
|
tableName: 'table1',
|
|
columnNames: ['parentId'],
|
|
referenceTableName: 'table2',
|
|
referenceColumnNames: ['id'],
|
|
onDelete: ActionType.CASCADE,
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined }));
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
constraintName: 'FK_test',
|
|
reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
|
|
tableName: 'table1',
|
|
type: 'ConstraintDrop',
|
|
},
|
|
{
|
|
constraint: {
|
|
columnNames: ['parentId'],
|
|
name: 'FK_test',
|
|
referenceColumnNames: ['id'],
|
|
referenceTableName: 'table2',
|
|
onDelete: ActionType.CASCADE,
|
|
synchronize: true,
|
|
tableName: 'table1',
|
|
type: 'foreign-key',
|
|
},
|
|
reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
|
|
type: 'ConstraintAdd',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('unique', () => {
|
|
it('should skip identical unique constraints', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.UNIQUE,
|
|
name: 'UQ_test',
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('check', () => {
|
|
it('should skip identical check constraints', () => {
|
|
const constraint: DatabaseConstraint = {
|
|
type: ConstraintType.CHECK,
|
|
name: 'CHK_test',
|
|
tableName: 'table1',
|
|
expression: 'column1 > 0',
|
|
synchronize: true,
|
|
};
|
|
|
|
const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));
|
|
|
|
expect(diff.items).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('index', () => {
|
|
describe('IndexCreate', () => {
|
|
it('should detect a new index', () => {
|
|
const diff = schemaDiff(
|
|
fromIndex({
|
|
name: 'IDX_test',
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
unique: false,
|
|
synchronize: true,
|
|
}),
|
|
fromIndex(),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'IndexCreate',
|
|
index: {
|
|
name: 'IDX_test',
|
|
columnNames: ['id'],
|
|
tableName: 'table1',
|
|
unique: false,
|
|
synchronize: true,
|
|
},
|
|
reason: 'missing in target',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('IndexDrop', () => {
|
|
it('should detect an extra index', () => {
|
|
const diff = schemaDiff(
|
|
fromIndex(),
|
|
fromIndex({
|
|
name: 'IDX_test',
|
|
unique: true,
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
synchronize: true,
|
|
}),
|
|
);
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'IndexDrop',
|
|
indexName: 'IDX_test',
|
|
reason: 'missing in source',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('should recreate the index if unique changes', () => {
|
|
const index: DatabaseIndex = {
|
|
name: 'IDX_test',
|
|
tableName: 'table1',
|
|
columnNames: ['id'],
|
|
unique: true,
|
|
synchronize: true,
|
|
};
|
|
const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false }));
|
|
|
|
expect(diff.items).toEqual([
|
|
{
|
|
type: 'IndexDrop',
|
|
indexName: 'IDX_test',
|
|
reason: 'uniqueness is different (true vs false)',
|
|
},
|
|
{
|
|
type: 'IndexCreate',
|
|
index,
|
|
reason: 'uniqueness is different (true vs false)',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|