mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: extension, triggers, functions, comments, parameters management in sql-tools (#17269)
feat: sql-tools extension, triggers, functions, comments, parameters
This commit is contained in:
parent
51c2c60231
commit
e7a5b96ed0
170 changed files with 5205 additions and 2295 deletions
21
server/src/sql-tools/to-sql/index.spec.ts
Normal file
21
server/src/sql-tools/to-sql/index.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { schemaDiffToSql } from 'src/sql-tools';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(schemaDiffToSql.name, () => {
|
||||
describe('comments', () => {
|
||||
it('should include the reason in a SQL comment', () => {
|
||||
expect(
|
||||
schemaDiffToSql(
|
||||
[
|
||||
{
|
||||
type: 'index.drop',
|
||||
indexName: 'IDX_test',
|
||||
reason: 'unknown',
|
||||
},
|
||||
],
|
||||
{ comments: true },
|
||||
),
|
||||
).toEqual([`DROP INDEX "IDX_test"; -- unknown`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
59
server/src/sql-tools/to-sql/index.ts
Normal file
59
server/src/sql-tools/to-sql/index.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { transformColumns } from 'src/sql-tools/to-sql/transformers/column.transformer';
|
||||
import { transformConstraints } from 'src/sql-tools/to-sql/transformers/constraint.transformer';
|
||||
import { transformEnums } from 'src/sql-tools/to-sql/transformers/enum.transformer';
|
||||
import { transformExtensions } from 'src/sql-tools/to-sql/transformers/extension.transformer';
|
||||
import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer';
|
||||
import { transformIndexes } from 'src/sql-tools/to-sql/transformers/index.transformer';
|
||||
import { transformParameters } from 'src/sql-tools/to-sql/transformers/parameter.transformer';
|
||||
import { transformTables } from 'src/sql-tools/to-sql/transformers/table.transformer';
|
||||
import { transformTriggers } from 'src/sql-tools/to-sql/transformers/trigger.transformer';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SchemaDiff, SchemaDiffToSqlOptions } from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Convert schema diffs into SQL statements
|
||||
*/
|
||||
export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => {
|
||||
return items.flatMap((item) => asSql(item).map((result) => result + withComments(options.comments, item)));
|
||||
};
|
||||
|
||||
const transformers: SqlTransformer[] = [
|
||||
transformColumns,
|
||||
transformConstraints,
|
||||
transformEnums,
|
||||
transformExtensions,
|
||||
transformFunctions,
|
||||
transformIndexes,
|
||||
transformParameters,
|
||||
transformTables,
|
||||
transformTriggers,
|
||||
];
|
||||
|
||||
const asSql = (item: SchemaDiff): string[] => {
|
||||
for (const transform of transformers) {
|
||||
const result = transform(item);
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return asArray(result);
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled schema diff type: ${item.type}`);
|
||||
};
|
||||
|
||||
const withComments = (comments: boolean | undefined, item: SchemaDiff): string => {
|
||||
if (!comments) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` -- ${item.reason}`;
|
||||
};
|
||||
|
||||
const asArray = <T>(items: T | T[]): T[] => {
|
||||
if (Array.isArray(items)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return [items];
|
||||
};
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { transformColumns } from 'src/sql-tools/to-sql/transformers/column.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformColumns.name, () => {
|
||||
describe('column.add', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.add',
|
||||
column: {
|
||||
name: 'column1',
|
||||
tableName: 'table1',
|
||||
type: 'character varying',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD "column1" character varying NOT NULL;');
|
||||
});
|
||||
|
||||
it('should add a nullable column', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.add',
|
||||
column: {
|
||||
name: 'column1',
|
||||
tableName: 'table1',
|
||||
type: 'character varying',
|
||||
nullable: true,
|
||||
isArray: false,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD "column1" character varying;');
|
||||
});
|
||||
|
||||
it('should add a column with an enum type', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.add',
|
||||
column: {
|
||||
name: 'column1',
|
||||
tableName: 'table1',
|
||||
type: 'character varying',
|
||||
enumName: 'table1_column1_enum',
|
||||
nullable: true,
|
||||
isArray: false,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD "column1" table1_column1_enum;');
|
||||
});
|
||||
|
||||
it('should add a column that is an array type', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.add',
|
||||
column: {
|
||||
name: 'column1',
|
||||
tableName: 'table1',
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
isArray: true,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD "column1" boolean[];');
|
||||
});
|
||||
});
|
||||
|
||||
describe('column.alter', () => {
|
||||
it('should make a column nullable', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.alter',
|
||||
tableName: 'table1',
|
||||
columnName: 'column1',
|
||||
changes: { nullable: true },
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" DROP NOT NULL;`]);
|
||||
});
|
||||
|
||||
it('should make a column non-nullable', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.alter',
|
||||
tableName: 'table1',
|
||||
columnName: 'column1',
|
||||
changes: { nullable: false },
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET NOT NULL;`]);
|
||||
});
|
||||
|
||||
it('should update the default value', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.alter',
|
||||
tableName: 'table1',
|
||||
columnName: 'column1',
|
||||
changes: { default: 'uuid_generate_v4()' },
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT uuid_generate_v4();`]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('column.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformColumns({
|
||||
type: 'column.drop',
|
||||
tableName: 'table1',
|
||||
columnName: 'column1',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`ALTER TABLE "table1" DROP COLUMN "column1";`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { ColumnChanges, DatabaseColumn, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformColumns: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'column.add': {
|
||||
return asColumnAdd(item.column);
|
||||
}
|
||||
|
||||
case 'column.alter': {
|
||||
return asColumnAlter(item.tableName, item.columnName, item.changes);
|
||||
}
|
||||
|
||||
case 'column.drop': {
|
||||
return asColumnDrop(item.tableName, item.columnName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asColumnAdd = (column: DatabaseColumn): string => {
|
||||
return (
|
||||
`ALTER TABLE "${column.tableName}" ADD "${column.name}" ${getColumnType(column)}` + getColumnModifiers(column) + ';'
|
||||
);
|
||||
};
|
||||
|
||||
const asColumnDrop = (tableName: string, columnName: string): string => {
|
||||
return `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}";`;
|
||||
};
|
||||
|
||||
export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => {
|
||||
const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`;
|
||||
const items: string[] = [];
|
||||
if (changes.nullable !== undefined) {
|
||||
items.push(changes.nullable ? `${base} DROP NOT NULL;` : `${base} SET NOT NULL;`);
|
||||
}
|
||||
|
||||
if (changes.default !== undefined) {
|
||||
items.push(`${base} SET DEFAULT ${changes.default};`);
|
||||
}
|
||||
|
||||
if (changes.storage !== undefined) {
|
||||
items.push(`${base} SET STORAGE ${changes.storage.toUpperCase()};`);
|
||||
}
|
||||
|
||||
if (changes.comment !== undefined) {
|
||||
items.push(asColumnComment(tableName, columnName, changes.comment));
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { transformConstraints } from 'src/sql-tools/to-sql/transformers/constraint.transformer';
|
||||
import { DatabaseConstraintType } from 'src/sql-tools/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformConstraints.name, () => {
|
||||
describe('constraint.add', () => {
|
||||
describe('primary keys', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformConstraints({
|
||||
type: 'constraint.add',
|
||||
constraint: {
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "PK_test" PRIMARY KEY ("id");');
|
||||
});
|
||||
});
|
||||
|
||||
describe('foreign keys', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformConstraints({
|
||||
type: 'constraint.add',
|
||||
constraint: {
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['parentId'],
|
||||
referenceColumnNames: ['id'],
|
||||
referenceTableName: 'table2',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(
|
||||
'ALTER TABLE "table1" ADD CONSTRAINT "FK_test" FOREIGN KEY ("parentId") REFERENCES "table2" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unique', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformConstraints({
|
||||
type: 'constraint.add',
|
||||
constraint: {
|
||||
type: DatabaseConstraintType.UNIQUE,
|
||||
name: 'UQ_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "UQ_test" UNIQUE ("id");');
|
||||
});
|
||||
});
|
||||
|
||||
describe('check', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformConstraints({
|
||||
type: 'constraint.add',
|
||||
constraint: {
|
||||
type: DatabaseConstraintType.CHECK,
|
||||
name: 'CHK_test',
|
||||
tableName: 'table1',
|
||||
expression: '"id" IS NOT NULL',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "CHK_test" CHECK ("id" IS NOT NULL);');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('constraint.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformConstraints({
|
||||
type: 'constraint.drop',
|
||||
tableName: 'table1',
|
||||
constraintName: 'PK_test',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`ALTER TABLE "table1" DROP CONSTRAINT "PK_test";`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { asColumnList } from 'src/sql-tools/helpers';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseActionType, DatabaseConstraint, DatabaseConstraintType, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformConstraints: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'constraint.add': {
|
||||
return asConstraintAdd(item.constraint);
|
||||
}
|
||||
|
||||
case 'constraint.drop': {
|
||||
return asConstraintDrop(item.tableName, item.constraintName);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const withAction = (constraint: { onDelete?: DatabaseActionType; onUpdate?: DatabaseActionType }) =>
|
||||
` ON UPDATE ${constraint.onUpdate ?? DatabaseActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? DatabaseActionType.NO_ACTION}`;
|
||||
|
||||
export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => {
|
||||
const base = `ALTER TABLE "${constraint.tableName}" ADD CONSTRAINT "${constraint.name}"`;
|
||||
switch (constraint.type) {
|
||||
case DatabaseConstraintType.PRIMARY_KEY: {
|
||||
const columnNames = asColumnList(constraint.columnNames);
|
||||
return `${base} PRIMARY KEY (${columnNames});`;
|
||||
}
|
||||
|
||||
case DatabaseConstraintType.FOREIGN_KEY: {
|
||||
const columnNames = asColumnList(constraint.columnNames);
|
||||
const referenceColumnNames = asColumnList(constraint.referenceColumnNames);
|
||||
return (
|
||||
`${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` +
|
||||
withAction(constraint) +
|
||||
';'
|
||||
);
|
||||
}
|
||||
|
||||
case DatabaseConstraintType.UNIQUE: {
|
||||
const columnNames = asColumnList(constraint.columnNames);
|
||||
return `${base} UNIQUE (${columnNames});`;
|
||||
}
|
||||
|
||||
case DatabaseConstraintType.CHECK: {
|
||||
return `${base} CHECK (${constraint.expression});`;
|
||||
}
|
||||
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const asConstraintDrop = (tableName: string, constraintName: string): string => {
|
||||
return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`;
|
||||
};
|
||||
26
server/src/sql-tools/to-sql/transformers/enum.transformer.ts
Normal file
26
server/src/sql-tools/to-sql/transformers/enum.transformer.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseEnum, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformEnums: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'enum.create': {
|
||||
return asEnumCreate(item.enum);
|
||||
}
|
||||
|
||||
case 'enum.drop': {
|
||||
return asEnumDrop(item.enumName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asEnumCreate = ({ name, values }: DatabaseEnum): string => {
|
||||
return `CREATE TYPE "${name}" AS ENUM (${values.map((value) => `'${value}'`)});`;
|
||||
};
|
||||
|
||||
const asEnumDrop = (enumName: string): string => {
|
||||
return `DROP TYPE "${enumName}";`;
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { transformExtensions } from 'src/sql-tools/to-sql/transformers/extension.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformExtensions.name, () => {
|
||||
describe('extension.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformExtensions({
|
||||
type: 'extension.drop',
|
||||
extensionName: 'cube',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`DROP EXTENSION "cube";`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension.create', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformExtensions({
|
||||
type: 'extension.create',
|
||||
extension: {
|
||||
name: 'cube',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`CREATE EXTENSION IF NOT EXISTS "cube";`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseExtension, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformExtensions: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'extension.create': {
|
||||
return asExtensionCreate(item.extension);
|
||||
}
|
||||
|
||||
case 'extension.drop': {
|
||||
return asExtensionDrop(item.extensionName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asExtensionCreate = (extension: DatabaseExtension): string => {
|
||||
return `CREATE EXTENSION IF NOT EXISTS "${extension.name}";`;
|
||||
};
|
||||
|
||||
const asExtensionDrop = (extensionName: string): string => {
|
||||
return `DROP EXTENSION "${extensionName}";`;
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformFunctions.name, () => {
|
||||
describe('function.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformFunctions({
|
||||
type: 'function.drop',
|
||||
functionName: 'test_func',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`DROP FUNCTION test_func;`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseFunction, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformFunctions: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'function.create': {
|
||||
return asFunctionCreate(item.function);
|
||||
}
|
||||
|
||||
case 'function.drop': {
|
||||
return asFunctionDrop(item.functionName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asFunctionCreate = (func: DatabaseFunction): string => {
|
||||
return func.expression;
|
||||
};
|
||||
|
||||
const asFunctionDrop = (functionName: string): string => {
|
||||
return `DROP FUNCTION ${functionName};`;
|
||||
};
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { transformIndexes } from 'src/sql-tools/to-sql/transformers/index.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformIndexes.name, () => {
|
||||
describe('index.create', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.create',
|
||||
index: {
|
||||
name: 'IDX_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['column1'],
|
||||
unique: false,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("column1")');
|
||||
});
|
||||
|
||||
it('should create an unique index', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.create',
|
||||
index: {
|
||||
name: 'IDX_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['column1'],
|
||||
unique: true,
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1")');
|
||||
});
|
||||
|
||||
it('should create an index with a custom expression', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.create',
|
||||
index: {
|
||||
name: 'IDX_test',
|
||||
tableName: 'table1',
|
||||
unique: false,
|
||||
expression: '"id" IS NOT NULL',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL)');
|
||||
});
|
||||
|
||||
it('should create an index with a where clause', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.create',
|
||||
index: {
|
||||
name: 'IDX_test',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
unique: false,
|
||||
where: '("id" IS NOT NULL)',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL)');
|
||||
});
|
||||
|
||||
it('should create an index with a custom expression', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.create',
|
||||
index: {
|
||||
name: 'IDX_test',
|
||||
tableName: 'table1',
|
||||
unique: false,
|
||||
using: 'gin',
|
||||
expression: '"id" IS NOT NULL',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual('CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('index.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformIndexes({
|
||||
type: 'index.drop',
|
||||
indexName: 'IDX_test',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`DROP INDEX "IDX_test";`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { asColumnList } from 'src/sql-tools/helpers';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseIndex, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformIndexes: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'index.create': {
|
||||
return asIndexCreate(item.index);
|
||||
}
|
||||
|
||||
case 'index.drop': {
|
||||
return asIndexDrop(item.indexName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const asIndexCreate = (index: DatabaseIndex): string => {
|
||||
let sql = `CREATE`;
|
||||
|
||||
if (index.unique) {
|
||||
sql += ' UNIQUE';
|
||||
}
|
||||
|
||||
sql += ` INDEX "${index.name}" ON "${index.tableName}"`;
|
||||
|
||||
if (index.columnNames) {
|
||||
const columnNames = asColumnList(index.columnNames);
|
||||
sql += ` (${columnNames})`;
|
||||
}
|
||||
|
||||
if (index.using && index.using !== 'btree') {
|
||||
sql += ` USING ${index.using}`;
|
||||
}
|
||||
|
||||
if (index.expression) {
|
||||
sql += ` (${index.expression})`;
|
||||
}
|
||||
|
||||
if (index.with) {
|
||||
sql += ` WITH (${index.with})`;
|
||||
}
|
||||
|
||||
if (index.where) {
|
||||
sql += ` WHERE ${index.where}`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
};
|
||||
|
||||
export const asIndexDrop = (indexName: string): string => {
|
||||
return `DROP INDEX "${indexName}";`;
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseParameter, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformParameters: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'parameter.set': {
|
||||
return asParameterSet(item.parameter);
|
||||
}
|
||||
|
||||
case 'parameter.reset': {
|
||||
return asParameterReset(item.databaseName, item.parameterName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asParameterSet = (parameter: DatabaseParameter): string => {
|
||||
let sql = '';
|
||||
if (parameter.scope === 'database') {
|
||||
sql += `ALTER DATABASE "${parameter.databaseName}" `;
|
||||
}
|
||||
|
||||
sql += `SET ${parameter.name} TO ${parameter.value}`;
|
||||
|
||||
return sql;
|
||||
};
|
||||
|
||||
const asParameterReset = (databaseName: string, parameterName: string): string => {
|
||||
return `ALTER DATABASE "${databaseName}" RESET "${parameterName}"`;
|
||||
};
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { transformTables } from 'src/sql-tools/to-sql/transformers/table.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformTables.name, () => {
|
||||
describe('table.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.drop',
|
||||
tableName: 'table1',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`DROP TABLE "table1";`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('table.create', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.create',
|
||||
table: {
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
tableName: 'table1',
|
||||
name: 'column1',
|
||||
type: 'character varying',
|
||||
nullable: true,
|
||||
isArray: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`CREATE TABLE "table1" ("column1" character varying);`]);
|
||||
});
|
||||
|
||||
it('should handle a non-nullable column', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.create',
|
||||
table: {
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
tableName: 'table1',
|
||||
name: 'column1',
|
||||
type: 'character varying',
|
||||
isArray: false,
|
||||
nullable: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`CREATE TABLE "table1" ("column1" character varying NOT NULL);`]);
|
||||
});
|
||||
|
||||
it('should handle a default value', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.create',
|
||||
table: {
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
tableName: 'table1',
|
||||
name: 'column1',
|
||||
type: 'character varying',
|
||||
isArray: false,
|
||||
nullable: true,
|
||||
default: 'uuid_generate_v4()',
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`CREATE TABLE "table1" ("column1" character varying DEFAULT uuid_generate_v4());`]);
|
||||
});
|
||||
|
||||
it('should handle a string with a fixed length', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.create',
|
||||
table: {
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
tableName: 'table1',
|
||||
name: 'column1',
|
||||
type: 'character varying',
|
||||
length: 2,
|
||||
isArray: false,
|
||||
nullable: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`CREATE TABLE "table1" ("column1" character varying(2));`]);
|
||||
});
|
||||
|
||||
it('should handle an array type', () => {
|
||||
expect(
|
||||
transformTables({
|
||||
type: 'table.create',
|
||||
table: {
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
tableName: 'table1',
|
||||
name: 'column1',
|
||||
type: 'character varying',
|
||||
isArray: true,
|
||||
nullable: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual([`CREATE TABLE "table1" ("column1" character varying[]);`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers';
|
||||
import { asColumnAlter } from 'src/sql-tools/to-sql/transformers/column.transformer';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseTable, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformTables: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'table.create': {
|
||||
return asTableCreate(item.table);
|
||||
}
|
||||
|
||||
case 'table.drop': {
|
||||
return asTableDrop(item.tableName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asTableCreate = (table: DatabaseTable): string[] => {
|
||||
const tableName = table.name;
|
||||
const columnsTypes = table.columns
|
||||
.map((column) => `"${column.name}" ${getColumnType(column)}` + getColumnModifiers(column))
|
||||
.join(', ');
|
||||
const items = [`CREATE TABLE "${tableName}" (${columnsTypes});`];
|
||||
|
||||
for (const column of table.columns) {
|
||||
if (column.comment) {
|
||||
items.push(asColumnComment(tableName, column.name, column.comment));
|
||||
}
|
||||
|
||||
if (column.storage) {
|
||||
items.push(...asColumnAlter(tableName, column.name, { storage: column.storage }));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const asTableDrop = (tableName: string): string => {
|
||||
return `DROP TABLE "${tableName}";`;
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { transformTriggers } from 'src/sql-tools/to-sql/transformers/trigger.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformTriggers.name, () => {
|
||||
describe('trigger.create', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformTriggers({
|
||||
type: 'trigger.create',
|
||||
trigger: {
|
||||
name: 'trigger1',
|
||||
tableName: 'table1',
|
||||
timing: 'before',
|
||||
actions: ['update'],
|
||||
scope: 'row',
|
||||
functionName: 'function1',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(
|
||||
`CREATE OR REPLACE TRIGGER "trigger1"
|
||||
BEFORE UPDATE ON "table1"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION function1();`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with multiple actions', () => {
|
||||
expect(
|
||||
transformTriggers({
|
||||
type: 'trigger.create',
|
||||
trigger: {
|
||||
name: 'trigger1',
|
||||
tableName: 'table1',
|
||||
timing: 'before',
|
||||
actions: ['update', 'delete'],
|
||||
scope: 'row',
|
||||
functionName: 'function1',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(
|
||||
`CREATE OR REPLACE TRIGGER "trigger1"
|
||||
BEFORE UPDATE OR DELETE ON "table1"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION function1();`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with old/new reference table aliases', () => {
|
||||
expect(
|
||||
transformTriggers({
|
||||
type: 'trigger.create',
|
||||
trigger: {
|
||||
name: 'trigger1',
|
||||
tableName: 'table1',
|
||||
timing: 'before',
|
||||
actions: ['update'],
|
||||
referencingNewTableAs: 'new',
|
||||
referencingOldTableAs: 'old',
|
||||
scope: 'row',
|
||||
functionName: 'function1',
|
||||
synchronize: true,
|
||||
},
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(
|
||||
`CREATE OR REPLACE TRIGGER "trigger1"
|
||||
BEFORE UPDATE ON "table1"
|
||||
REFERENCING OLD TABLE AS "old" NEW TABLE AS "new"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION function1();`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger.drop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformTriggers({
|
||||
type: 'trigger.drop',
|
||||
tableName: 'table1',
|
||||
triggerName: 'trigger1',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
).toEqual(`DROP TRIGGER "trigger1" ON "table1";`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { DatabaseTrigger, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformTriggers: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'trigger.create': {
|
||||
return asTriggerCreate(item.trigger);
|
||||
}
|
||||
|
||||
case 'trigger.drop': {
|
||||
return asTriggerDrop(item.tableName, item.triggerName);
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const asTriggerCreate = (trigger: DatabaseTrigger): string => {
|
||||
const sql: string[] = [
|
||||
`CREATE OR REPLACE TRIGGER "${trigger.name}"`,
|
||||
`${trigger.timing.toUpperCase()} ${trigger.actions.map((action) => action.toUpperCase()).join(' OR ')} ON "${trigger.tableName}"`,
|
||||
];
|
||||
|
||||
if (trigger.referencingOldTableAs || trigger.referencingNewTableAs) {
|
||||
let statement = `REFERENCING`;
|
||||
if (trigger.referencingOldTableAs) {
|
||||
statement += ` OLD TABLE AS "${trigger.referencingOldTableAs}"`;
|
||||
}
|
||||
if (trigger.referencingNewTableAs) {
|
||||
statement += ` NEW TABLE AS "${trigger.referencingNewTableAs}"`;
|
||||
}
|
||||
sql.push(statement);
|
||||
}
|
||||
|
||||
if (trigger.scope) {
|
||||
sql.push(`FOR EACH ${trigger.scope.toUpperCase()}`);
|
||||
}
|
||||
|
||||
if (trigger.when) {
|
||||
sql.push(`WHEN (${trigger.when})`);
|
||||
}
|
||||
|
||||
sql.push(`EXECUTE FUNCTION ${trigger.functionName}();`);
|
||||
|
||||
return sql.join('\n ');
|
||||
};
|
||||
|
||||
export const asTriggerDrop = (tableName: string, triggerName: string): string => {
|
||||
return `DROP TRIGGER "${triggerName}" ON "${tableName}";`;
|
||||
};
|
||||
3
server/src/sql-tools/to-sql/transformers/types.ts
Normal file
3
server/src/sql-tools/to-sql/transformers/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export type SqlTransformer = (item: SchemaDiff) => string | string[] | false;
|
||||
Loading…
Add table
Add a link
Reference in a new issue