mirror of
https://github.com/immich-app/immich.git
synced 2025-12-26 01:11:47 +03:00
refactor: sql-tools (#19717)
This commit is contained in:
81
server/src/sql-tools/comparers/column.comparer.spec.ts
Normal file
81
server/src/sql-tools/comparers/column.comparer.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
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',
|
||||
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 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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
82
server/src/sql-tools/comparers/column.comparer.ts
Normal file
82
server/src/sql-tools/comparers/column.comparer.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { getColumnType, isDefaultEqual } from 'src/sql-tools/helpers';
|
||||
import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const compareColumns: Comparer<DatabaseColumn> = {
|
||||
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),
|
||||
},
|
||||
reason: `default is different (${source.default} 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;
|
||||
},
|
||||
};
|
||||
|
||||
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
||||
return [
|
||||
{
|
||||
type: 'ColumnDrop',
|
||||
tableName: target.tableName,
|
||||
columnName: target.name,
|
||||
reason,
|
||||
},
|
||||
{ type: 'ColumnAdd', column: source, reason },
|
||||
];
|
||||
};
|
||||
63
server/src/sql-tools/comparers/constraint.comparer.spec.ts
Normal file
63
server/src/sql-tools/comparers/constraint.comparer.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
133
server/src/sql-tools/comparers/constraint.comparer.ts
Normal file
133
server/src/sql-tools/comparers/constraint.comparer.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { 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> = {
|
||||
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 },
|
||||
];
|
||||
};
|
||||
54
server/src/sql-tools/comparers/enum.comparer.spec.ts
Normal file
54
server/src/sql-tools/comparers/enum.comparer.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
server/src/sql-tools/comparers/enum.comparer.ts
Normal file
38
server/src/sql-tools/comparers/enum.comparer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
37
server/src/sql-tools/comparers/extension.comparer.spec.ts
Normal file
37
server/src/sql-tools/comparers/extension.comparer.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
server/src/sql-tools/comparers/extension.comparer.ts
Normal file
22
server/src/sql-tools/comparers/extension.comparer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
53
server/src/sql-tools/comparers/function.comparer.spec.ts
Normal file
53
server/src/sql-tools/comparers/function.comparer.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
32
server/src/sql-tools/comparers/function.comparer.ts
Normal file
32
server/src/sql-tools/comparers/function.comparer.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
72
server/src/sql-tools/comparers/index.comparer.spec.ts
Normal file
72
server/src/sql-tools/comparers/index.comparer.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
server/src/sql-tools/comparers/index.comparer.ts
Normal file
46
server/src/sql-tools/comparers/index.comparer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { haveEqualColumns } from 'src/sql-tools/helpers';
|
||||
import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
|
||||
|
||||
export const compareIndexes: Comparer<DatabaseIndex> = {
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
44
server/src/sql-tools/comparers/parameter.comparer.spec.ts
Normal file
44
server/src/sql-tools/comparers/parameter.comparer.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
23
server/src/sql-tools/comparers/parameter.comparer.ts
Normal file
23
server/src/sql-tools/comparers/parameter.comparer.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
44
server/src/sql-tools/comparers/table.comparer.spec.ts
Normal file
44
server/src/sql-tools/comparers/table.comparer.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
45
server/src/sql-tools/comparers/table.comparer.ts
Normal file
45
server/src/sql-tools/comparers/table.comparer.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
const newTable = (name: string) => ({
|
||||
name,
|
||||
columns: [],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
});
|
||||
|
||||
export const compareTables: Comparer<DatabaseTable> = {
|
||||
onMissing: (source) => [
|
||||
{
|
||||
type: 'TableCreate',
|
||||
table: source,
|
||||
reason: Reason.MissingInTarget,
|
||||
},
|
||||
// TODO merge constraints into table create record when possible
|
||||
...compareTable(source, newTable(source.name), { columns: false }),
|
||||
],
|
||||
onExtra: (target) => [
|
||||
...compareTable(newTable(target.name), target, { columns: false }),
|
||||
{
|
||||
type: 'TableDrop',
|
||||
tableName: target.name,
|
||||
reason: Reason.MissingInSource,
|
||||
},
|
||||
],
|
||||
onCompare: (source, target) => compareTable(source, target, { columns: true }),
|
||||
};
|
||||
|
||||
const compareTable = (source: DatabaseTable, target: DatabaseTable, options: { columns?: boolean }): SchemaDiff[] => {
|
||||
return [
|
||||
...(options.columns ? compare(source.columns, target.columns, {}, compareColumns) : []),
|
||||
...compare(source.indexes, target.indexes, {}, compareIndexes),
|
||||
...compare(source.constraints, target.constraints, {}, compareConstraints),
|
||||
...compare(source.triggers, target.triggers, {}, compareTriggers),
|
||||
];
|
||||
};
|
||||
88
server/src/sql-tools/comparers/trigger.comparer.spec.ts
Normal file
88
server/src/sql-tools/comparers/trigger.comparer.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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 }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
41
server/src/sql-tools/comparers/trigger.comparer.ts
Normal file
41
server/src/sql-tools/comparers/trigger.comparer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user