feat: dependency tracking (#73)

fix: bugs
This commit is contained in:
Jason Rasmussen
2026-04-21 13:01:34 -04:00
committed by GitHub
parent b167a24573
commit e779ff8d16
73 changed files with 2243 additions and 1265 deletions
+12
View File
@@ -34,6 +34,8 @@ export default defineConfig([
},
{
rules: {
curly: 2,
'no-shadow': 'error',
'unicorn/prevent-abbreviations': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
@@ -43,6 +45,16 @@ export default defineConfig([
'unicorn/import-style': 'off',
'unicorn/prefer-structured-clone': 'off',
'unicorn/no-for-loop': 'off',
'unicorn/no-process-exit': 'off',
'prettier/prettier': 0,
'object-shorthand': ['error', 'always'],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
},
},
{
+1
View File
@@ -28,6 +28,7 @@
},
"dependencies": {
"commander": "^14.0.3",
"graph-data-structure": "^4.5.0",
"kysely": "^0.28.11",
"kysely-postgres-js": "^3.0.0",
"pg-connection-string": "^2.11.0",
File diff suppressed because it is too large Load Diff
@@ -17,9 +17,8 @@ describe('compareColumns', () => {
it('should work', () => {
expect(compareColumns().onExtra(testColumn)).toEqual([
{
tableName: 'table1',
columnName: 'test',
type: 'ColumnDrop',
object: testColumn,
reason: Reason.MissingInSource,
},
]);
@@ -31,7 +30,7 @@ describe('compareColumns', () => {
expect(compareColumns().onMissing(testColumn)).toEqual([
{
type: 'ColumnAdd',
column: testColumn,
object: testColumn,
reason: Reason.MissingInTarget,
},
]);
@@ -48,17 +47,8 @@ describe('compareColumns', () => {
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,
},
{ type: 'ColumnDrop', object: target, reason },
{ type: 'ColumnAdd', object: source, reason },
]);
});
@@ -68,11 +58,13 @@ describe('compareColumns', () => {
const reason = `default is different (null vs '')`;
expect(compareColumns().onCompare(source, target)).toEqual([
{
columnName: 'test',
tableName: 'table1',
type: 'ColumnAlter',
changes: {
default: 'NULL',
object: {
old: target,
new: source,
changes: {
default: 'NULL',
},
},
reason,
},
@@ -85,11 +77,13 @@ describe('compareColumns', () => {
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',
object: {
old: target,
new: source,
changes: {
comment: 'new comment',
},
},
reason,
},
@@ -20,29 +20,10 @@ export const compareColumns = () =>
]);
},
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,
},
{ type: 'ColumnRename', object: { old: target, new: source }, reason: Reason.Rename },
],
onMissing: (source) => [{ type: 'ColumnAdd', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'ColumnDrop', object: target, reason: Reason.MissingInSource }],
onCompare: (source, target) => {
const sourceType = getColumnType(source);
const targetType = getColumnType(target);
@@ -58,10 +39,12 @@ export const compareColumns = () =>
if (source.nullable !== target.nullable) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
nullable: source.nullable,
object: {
old: target,
new: source,
changes: {
nullable: source.nullable,
},
},
reason: `nullable is different (${source.nullable} vs ${target.nullable})`,
});
@@ -70,10 +53,12 @@ export const compareColumns = () =>
if (!isDefaultEqual(source, target)) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
default: String(source.default ?? 'NULL'),
object: {
old: target,
new: source,
changes: {
default: String(source.default ?? 'NULL'),
},
},
reason: `default is different (${source.default ?? 'null'} vs ${target.default})`,
});
@@ -82,10 +67,12 @@ export const compareColumns = () =>
if (source.comment !== target.comment) {
items.push({
type: 'ColumnAlter',
tableName: source.tableName,
columnName: source.name,
changes: {
comment: String(source.comment),
object: {
old: target,
new: source,
changes: {
comment: String(source.comment),
},
},
reason: `comment is different (${source.comment} vs ${target.comment})`,
});
@@ -97,12 +84,7 @@ export const compareColumns = () =>
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
return [
{
type: 'ColumnDrop',
tableName: target.tableName,
columnName: target.name,
reason,
},
{ type: 'ColumnAdd', column: source, reason },
{ type: 'ColumnDrop', object: target, reason },
{ type: 'ColumnAdd', object: source, reason },
];
};
@@ -16,8 +16,7 @@ describe('compareConstraints', () => {
expect(compareConstraints().onExtra(testConstraint)).toEqual([
{
type: 'ConstraintDrop',
constraintName: 'test',
tableName: 'table1',
object: testConstraint,
reason: Reason.MissingInSource,
},
]);
@@ -29,7 +28,7 @@ describe('compareConstraints', () => {
expect(compareConstraints().onMissing(testConstraint)).toEqual([
{
type: 'ConstraintAdd',
constraint: testConstraint,
object: testConstraint,
reason: Reason.MissingInTarget,
},
]);
@@ -46,17 +45,8 @@ describe('compareConstraints', () => {
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,
},
{ type: 'ConstraintDrop', object: target, reason },
{ type: 'ConstraintAdd', object: source, reason },
]);
});
});
@@ -37,29 +37,10 @@ export const compareConstraints = (): Comparer<DatabaseConstraint> => ({
}
},
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,
},
{ type: 'ConstraintRename', object: { old: target, new: source }, reason: Reason.Rename },
],
onMissing: (source) => [{ type: 'ConstraintAdd', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'ConstraintDrop', object: target, reason: Reason.MissingInSource }],
onCompare: (source, target) => {
switch (source.type) {
case ConstraintType.PRIMARY_KEY: {
@@ -154,12 +135,7 @@ const dropAndRecreateConstraint = (
reason: string,
): SchemaDiff[] => {
return [
{
type: 'ConstraintDrop',
tableName: target.tableName,
constraintName: target.name,
reason,
},
{ type: 'ConstraintAdd', constraint: source, reason },
{ type: 'ConstraintDrop', object: target, reason },
{ type: 'ConstraintAdd', object: source, reason },
];
};
@@ -9,8 +9,8 @@ describe('compareEnums', () => {
it('should work', () => {
expect(compareEnums().onExtra(testEnum)).toEqual([
{
enumName: 'test',
type: 'EnumDrop',
object: testEnum,
reason: Reason.MissingInSource,
},
]);
@@ -22,7 +22,7 @@ describe('compareEnums', () => {
expect(compareEnums().onMissing(testEnum)).toEqual([
{
type: 'EnumCreate',
enum: testEnum,
object: testEnum,
reason: Reason.MissingInTarget,
},
]);
@@ -39,13 +39,13 @@ describe('compareEnums', () => {
const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true };
expect(compareEnums().onCompare(source, target)).toEqual([
{
enumName: 'test',
type: 'EnumDrop',
object: target,
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
},
{
type: 'EnumCreate',
enum: source,
object: source,
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
},
]);
@@ -1,35 +1,15 @@
import { Comparer, DatabaseEnum, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'EnumCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'EnumDrop', object: target, 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,
},
{ type: 'EnumDrop', object: target, reason },
{ type: 'EnumCreate', object: source, reason },
];
}
@@ -9,8 +9,8 @@ describe('compareExtensions', () => {
it('should work', () => {
expect(compareExtensions().onExtra(testExtension)).toEqual([
{
extensionName: 'test',
type: 'ExtensionDrop',
object: testExtension,
reason: Reason.MissingInSource,
},
]);
@@ -22,7 +22,7 @@ describe('compareExtensions', () => {
expect(compareExtensions().onMissing(testExtension)).toEqual([
{
type: 'ExtensionCreate',
extension: testExtension,
object: testExtension,
reason: Reason.MissingInTarget,
},
]);
@@ -1,20 +1,8 @@
import { Comparer, DatabaseExtension, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'ExtensionCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'ExtensionDrop', object: target, reason: Reason.MissingInSource }],
onCompare: () => {
// if the name matches they are the same
return [];
@@ -13,8 +13,8 @@ describe('compareFunctions', () => {
it('should work', () => {
expect(compareFunctions().onExtra(testFunction)).toEqual([
{
functionName: 'test',
type: 'FunctionDrop',
object: testFunction,
reason: Reason.MissingInSource,
},
]);
@@ -26,7 +26,7 @@ describe('compareFunctions', () => {
expect(compareFunctions().onMissing(testFunction)).toEqual([
{
type: 'FunctionCreate',
function: testFunction,
object: testFunction,
reason: Reason.MissingInTarget,
},
]);
@@ -45,7 +45,7 @@ describe('compareFunctions', () => {
{
type: 'FunctionCreate',
reason: 'function expression has changed (SELECT 1 vs SELECT 2)',
function: source,
object: source,
},
]);
});
@@ -1,30 +1,12 @@
import { Comparer, DatabaseFunction, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'FunctionCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'FunctionDrop', object: target, 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 [{ type: 'FunctionCreate', object: source, reason }];
}
return [];
@@ -16,7 +16,7 @@ describe('compareIndexes', () => {
expect(compareIndexes().onExtra(testIndex)).toEqual([
{
type: 'IndexDrop',
indexName: 'test',
object: testIndex,
reason: Reason.MissingInSource,
},
]);
@@ -28,7 +28,7 @@ describe('compareIndexes', () => {
expect(compareIndexes().onMissing(testIndex)).toEqual([
{
type: 'IndexCreate',
index: testIndex,
object: testIndex,
reason: Reason.MissingInTarget,
},
]);
@@ -57,13 +57,13 @@ describe('compareIndexes', () => {
};
expect(compareIndexes().onCompare(source, target)).toEqual([
{
indexName: 'test',
type: 'IndexDrop',
object: target,
reason: 'columns are different (column1 vs column1,column2)',
},
{
type: 'IndexCreate',
index: source,
object: source,
reason: 'columns are different (column1 vs column1,column2)',
},
]);
@@ -9,29 +9,9 @@ export const compareIndexes = (): Comparer<DatabaseIndex> => ({
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,
},
],
onRename: (source, target) => [{ type: 'IndexRename', object: { old: target, new: source }, reason: Reason.Rename }],
onMissing: (source) => [{ type: 'IndexCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'IndexDrop', object: target, reason: Reason.MissingInSource }],
onCompare: (source, target) => {
const sourceUsing = source.using ?? 'btree';
const targetUsing = target.using ?? 'btree';
@@ -52,8 +32,8 @@ export const compareIndexes = (): Comparer<DatabaseIndex> => ({
if (reason) {
return [
{ type: 'IndexDrop', indexName: target.name, reason },
{ type: 'IndexCreate', index: source, reason },
{ type: 'IndexDrop', object: target, reason },
{ type: 'IndexCreate', object: source, reason },
];
}
@@ -14,7 +14,7 @@ describe('compareOverrides', () => {
expect(compareOverrides().onExtra(testOverride)).toEqual([
{
type: 'OverrideDrop',
overrideName: 'test',
object: testOverride,
reason: Reason.MissingInSource,
},
]);
@@ -26,7 +26,7 @@ describe('compareOverrides', () => {
expect(compareOverrides().onMissing(testOverride)).toEqual([
{
type: 'OverrideCreate',
override: testOverride,
object: testOverride,
reason: Reason.MissingInTarget,
},
]);
@@ -59,8 +59,8 @@ describe('compareOverrides', () => {
};
expect(compareOverrides().onCompare(source, target)).toEqual([
{
override: source,
type: 'OverrideUpdate',
object: source,
reason: expect.stringContaining('value is different'),
},
]);
@@ -1,26 +1,14 @@
import { Comparer, DatabaseOverride, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'OverrideCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'OverrideDrop', object: target, 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})` },
{ type: 'OverrideUpdate', object: source, reason: `value is different (${sourceValue} vs ${targetValue})` },
];
}
@@ -16,8 +16,7 @@ describe('compareParameters', () => {
expect(compareParameters().onExtra(testParameter)).toEqual([
{
type: 'ParameterReset',
databaseName: 'immich',
parameterName: 'test',
object: testParameter,
reason: Reason.MissingInSource,
},
]);
@@ -29,7 +28,7 @@ describe('compareParameters', () => {
expect(compareParameters().onMissing(testParameter)).toEqual([
{
type: 'ParameterSet',
parameter: testParameter,
object: testParameter,
reason: Reason.MissingInTarget,
},
]);
@@ -1,21 +1,8 @@
import { Comparer, DatabaseParameter, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'ParameterSet', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'ParameterReset', object: target, reason: Reason.MissingInSource }],
onCompare: () => {
// TODO
return [];
@@ -17,7 +17,7 @@ describe('compareParameters', () => {
expect(compareTables({}).onExtra(testTable)).toEqual([
{
type: 'TableDrop',
tableName: 'test',
object: testTable,
reason: Reason.MissingInSource,
},
]);
@@ -29,7 +29,7 @@ describe('compareParameters', () => {
expect(compareTables({}).onMissing(testTable)).toEqual([
{
type: 'TableCreate',
table: testTable,
object: testTable,
reason: Reason.MissingInTarget,
},
]);
@@ -3,23 +3,39 @@ import { compareConstraints } from 'src/comparers/constraint.comparer';
import { compareIndexes } from 'src/comparers/index.comparer';
import { compareTriggers } from 'src/comparers/trigger.comparer';
import { compare } from 'src/helpers';
import { Comparer, DatabaseTable, Reason, SchemaDiffOptions } from 'src/types';
import {
Comparer,
ConstraintType,
DatabaseForeignKeyConstraint,
DatabaseTable,
Reason,
SchemaDiffOptions,
} from 'src/types';
const getDeferredForeignKeys = (table: DatabaseTable) =>
table.constraints.filter(
(c): c is DatabaseForeignKeyConstraint => c.type === ConstraintType.FOREIGN_KEY && !!c.deferred,
);
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,
},
],
onMissing: (source) =>
[
{ type: 'TableCreate' as const, object: source },
...source.indexes.map((object) => ({ type: 'IndexCreate' as const, object })),
...source.triggers.map((object) => ({ type: 'TriggerCreate' as const, object })),
// deferred FK constraints are not inlined in CREATE TABLE — add them separately
...getDeferredForeignKeys(source).map((object) => ({ type: 'ConstraintAdd' as const, object })),
].map((item) => ({ ...item, reason: Reason.MissingInTarget })),
onExtra: (target) =>
[
{ type: 'TableDrop' as const, object: target },
...target.triggers.map((object) => ({ type: 'TriggerDrop' as const, object })),
// deferred FK constraints must be dropped explicitly before their referenced table can be dropped
...getDeferredForeignKeys(target).map((object) => ({
type: 'ConstraintDrop' as const,
object,
})),
].map((item) => ({ ...item, reason: Reason.MissingInSource })),
onCompare: (source, target) => {
return [
...compare(source.columns, target.columns, options.columns, compareColumns()),
@@ -18,8 +18,7 @@ describe('compareTriggers', () => {
expect(compareTriggers().onExtra(testTrigger)).toEqual([
{
type: 'TriggerDrop',
tableName: 'table1',
triggerName: 'test',
object: testTrigger,
reason: Reason.MissingInSource,
},
]);
@@ -31,7 +30,7 @@ describe('compareTriggers', () => {
expect(compareTriggers().onMissing(testTrigger)).toEqual([
{
type: 'TriggerCreate',
trigger: testTrigger,
object: testTrigger,
reason: Reason.MissingInTarget,
},
]);
@@ -47,42 +46,42 @@ describe('compareTriggers', () => {
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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: 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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: 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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: 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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: 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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: 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 }]);
expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', object: source, reason }]);
});
});
});
@@ -1,21 +1,8 @@
import { Comparer, DatabaseTrigger, Reason } from 'src/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,
},
],
onMissing: (source) => [{ type: 'TriggerCreate', object: source, reason: Reason.MissingInTarget }],
onExtra: (target) => [{ type: 'TriggerDrop', object: target, reason: Reason.MissingInSource }],
onCompare: (source, target) => {
let reason = '';
if (source.functionName !== target.functionName) {
@@ -33,7 +20,7 @@ export const compareTriggers = (): Comparer<DatabaseTrigger> => ({
}
if (reason) {
return [{ type: 'TriggerCreate', trigger: source, reason }];
return [{ type: 'TriggerCreate', object: source, reason }];
}
return [];
@@ -1,5 +1,4 @@
import { DefaultNamingStrategy } from 'src/naming/default.naming';
import { HashNamingStrategy } from 'src/naming/hash.naming';
import { NamingInterface, NamingItem } from 'src/naming/naming.interface';
import {
BaseContextOptions,
@@ -20,20 +19,12 @@ const isNamingInterface = (strategy: string | NamingInterface): strategy is Nami
return typeof strategy === 'object' && typeof strategy.getName === 'function';
};
const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => {
const asNamingStrategy = (strategy: 'default' | NamingInterface): NamingInterface => {
if (isNamingInterface(strategy)) {
return strategy;
}
switch (strategy) {
case 'hash': {
return new HashNamingStrategy();
}
default: {
return new DefaultNamingStrategy();
}
}
return new DefaultNamingStrategy();
};
const asUuidFunctionStrategy = (uuidFunction: string | UuidFunctionFactory): UuidFunctionFactory => {
@@ -65,7 +56,7 @@ export class BaseContext {
this.databaseName = options.databaseName ?? 'postgres';
this.schemaName = options.schemaName ?? 'public';
this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash');
this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'default');
this.uuidFunctionFactory = asUuidFunctionStrategy(options.uuidFunction ?? defaultUuidFunction);
this.outputTarget = options.outputTarget ?? 'javascript';
}
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { BaseContext } from 'src/contexts/base-context';
import { ColumnOptions } from 'src/decorators/column.decorator';
import { TableOptions } from 'src/decorators/table.decorator';
import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/types';
@@ -53,7 +52,10 @@ export class ProcessorContext extends BaseContext {
return { table, column };
}
addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) {
addColumn(table: DatabaseTable, input: DatabaseColumn, propertyName: string | symbol) {
const column = Object.fromEntries(
Object.entries(input).filter(([_key, value]) => value !== undefined),
) as DatabaseColumn;
table.columns.push(column);
const tableMetadata = this.getTableMetadata(table);
tableMetadata.methodToColumn.set(propertyName, column);
@@ -9,8 +9,8 @@ export type ForeignKeyColumnOptions = ColumnBaseOptions & {
constraintName?: string;
};
export const ForeignKeyColumn = (target: () => Function, options: ForeignKeyColumnOptions): PropertyDecorator => {
export const ForeignKeyColumn = (target: () => Function, options?: ForeignKeyColumnOptions): PropertyDecorator => {
return (object: object, propertyName: string | symbol) => {
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
register({ type: 'foreignKeyColumn', item: { object, propertyName, options: options ?? {}, target } });
};
};
+259 -2
View File
@@ -1,6 +1,24 @@
import { depthFirstSearch, Graph, hasCycle } from 'graph-data-structure';
import { createHash } from 'node:crypto';
import { BaseContext } from 'src/contexts/base-context';
import { ColumnValue } from 'src/decorators/column.decorator';
import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, OutputTarget, SchemaDiff } from 'src/types';
import {
Comparer,
ConstraintType,
DatabaseColumn,
DatabaseConstraint,
DatabaseEnum,
DatabaseForeignKeyConstraint,
DatabaseFunction,
DatabaseIndex,
DatabaseOverride,
DatabaseTable,
DatabaseTrigger,
IgnoreOptions,
OutputTarget,
SchemaDiff,
SchemaItem,
} from 'src/types';
export const asOptions = <T extends { name?: string }>(options: string | T): T => {
if (typeof options === 'string') {
@@ -74,7 +92,20 @@ export const setIsEqual = (source: Set<unknown>, target: Set<unknown>) =>
source.size === target.size && [...source].every((x) => target.has(x));
export const haveEqualColumns = (sourceColumns?: string[], targetColumns?: string[]) => {
return setIsEqual(new Set(sourceColumns), new Set(targetColumns));
sourceColumns = sourceColumns ?? [];
targetColumns = targetColumns ?? [];
if (sourceColumns.length !== targetColumns.length) {
return false;
}
for (let i = 0; i < sourceColumns.length; i++) {
if (sourceColumns[i] !== targetColumns[i]) {
return false;
}
}
return true;
};
export const haveEqualOverrides = <T extends { override?: DatabaseOverride }>(source: T, target: T) => {
@@ -273,3 +304,229 @@ const escape = (value: string) => {
export const asRenameKey = (values: Array<string | boolean | number | undefined>) =>
values.map((value) => value ?? '').join('|');
export type NodeOptions<T> = {
getId: (item: T) => string;
getChildrenIds: (item: T) => Array<string | undefined>;
};
export const topologicalSort = <T>(items: T[], { getId, getChildrenIds }: NodeOptions<T>): T[] => {
const graph = new Graph<string>();
const itemMap = new Map<string, T>();
for (const item of items) {
const id = getId(item);
graph.addNode(id);
itemMap.set(id, item);
}
for (const item of items) {
const id = getId(item);
for (const childId of getChildrenIds(item)) {
if (childId && itemMap.has(childId)) {
graph.addEdge(id, childId);
}
}
}
if (hasCycle(graph)) {
console.log('[Warning] Circular dependency detected in schema diff items');
}
return depthFirstSearch(graph).map((id) => itemMap.get(id)!);
};
export const getSchemaItemId = (item: SchemaItem): string => {
switch (item.type) {
case 'ExtensionCreate':
case 'ExtensionDrop':
case 'FunctionCreate':
case 'FunctionDrop':
case 'EnumCreate':
case 'EnumDrop':
case 'ParameterSet':
case 'ParameterReset':
case 'OverrideCreate':
case 'OverrideUpdate':
case 'OverrideDrop':
case 'TableCreate':
case 'TableDrop': {
return `${item.type}:${item.object.name}`;
}
case 'ColumnAdd':
case 'ColumnDrop':
case 'ConstraintAdd':
case 'ConstraintDrop':
case 'IndexCreate':
case 'IndexDrop':
case 'TriggerCreate':
case 'TriggerDrop': {
return `${item.type}:${item.object.tableName}.${item.object.name}`;
}
case 'ColumnAlter':
case 'ColumnRename':
case 'ConstraintRename':
case 'IndexRename': {
return `${item.type}:${item.object.old.tableName}.${item.object.old.name}`;
}
}
};
type TableName = { name: string; tableName: string };
type Name = { name: string };
const tableCreate = (item: Name) => getSchemaItemId({ type: 'TableCreate', object: item as DatabaseTable });
const tableDrop = (item: Name) => getSchemaItemId({ type: 'TableDrop', object: item as DatabaseTable });
const constraintDrop = (item: TableName) =>
getSchemaItemId({ type: 'ConstraintDrop', object: item as DatabaseConstraint });
const functionCreate = (item: Name) => getSchemaItemId({ type: 'FunctionCreate', object: item as DatabaseFunction });
const functionDrop = (item: Name) => getSchemaItemId({ type: 'FunctionDrop', object: item as DatabaseFunction });
const triggerDrop = (item: TableName) => getSchemaItemId({ type: 'TriggerDrop', object: item as DatabaseTrigger });
const enumCreate = (item: Name) => getSchemaItemId({ type: 'EnumCreate', object: item as DatabaseEnum });
const enumDrop = (item: Name) => getSchemaItemId({ type: 'EnumDrop', object: item as DatabaseEnum });
const indexDrop = (item: TableName) => getSchemaItemId({ type: 'IndexDrop', object: item as DatabaseIndex });
const columnCreate = (item: TableName) => getSchemaItemId({ type: 'ColumnAdd', object: item as DatabaseColumn });
const columnDrop = (item: TableName) => getSchemaItemId({ type: 'ColumnDrop', object: item as DatabaseColumn });
const overrideDrop = (item: Name) => getSchemaItemId({ type: 'OverrideDrop', object: item as DatabaseOverride });
export const getSchemaItemChildrenIds = (
ctx: BaseContext,
{ type, object }: SchemaItem,
items: SchemaItem[],
): Array<string | undefined> => {
{
switch (type) {
case 'TriggerCreate': {
return [
triggerDrop(object),
tableCreate({ name: object.tableName }),
// create after referenced functions are created
functionCreate({ name: object.functionName }),
];
}
case 'FunctionCreate': {
return [functionDrop(object)];
}
case 'FunctionDrop': {
return [
// drop after referenced triggers are dropped
...items
.filter((item) => item.type === 'TriggerDrop')
.filter((item) => item.object.functionName === object.name)
.map((item) => triggerDrop(item.object)),
];
}
case 'TableCreate': {
return [
// create after non-deferred FK referenced tables exist (deferred FKs are added via ConstraintAdd after both tables exist)
...object.constraints
.filter((c): c is DatabaseForeignKeyConstraint => c.type === ConstraintType.FOREIGN_KEY && !c.deferred)
.map((constraint) => tableCreate({ name: constraint.referenceTableName })),
// create after referenced enums are created
...object.columns.map((column) => column.enumName && enumCreate({ name: column.enumName })),
];
}
case 'TableDrop': {
return [
// drop override table after all drop overrides
...(object.name === ctx.overrideTableName
? items.filter((item) => item.type === 'OverrideDrop').map((item) => overrideDrop(item.object))
: []),
// drop after my own constraints are dropped
...items
.filter((item) => item.type === 'ConstraintDrop')
.map((item) => item.object)
.filter(
(c): c is DatabaseForeignKeyConstraint => c.type === ConstraintType.FOREIGN_KEY && c.name === c.name,
)
.map((c) => constraintDrop({ name: c.name, tableName: c.tableName })),
// drop after tables with FKs to this table are dropped
...items
.filter((item) => item.type === 'TableDrop')
.filter((item) =>
item.object.constraints.some(
(c): c is DatabaseForeignKeyConstraint =>
c.type === ConstraintType.FOREIGN_KEY && c.referenceTableName === object.name && !c.deferred,
),
)
.map((item) => tableDrop({ name: item.object.name })),
// drop after constraints to this table are dropped
...items
.filter((item) => item.type === 'ConstraintDrop')
.map((item) => item.object)
.filter(
(c): c is DatabaseForeignKeyConstraint =>
c.type === ConstraintType.FOREIGN_KEY && c.referenceTableName === object.name,
)
.map((c) => constraintDrop({ name: c.name, tableName: c.referenceTableName })),
];
}
case 'ColumnAdd': {
return [columnDrop(object)];
}
case 'ColumnDrop': {
return [tableCreate({ name: object.tableName })];
}
case 'ColumnAlter': {
return [tableCreate({ name: object.new.tableName })];
}
case 'EnumCreate': {
return [enumDrop(object)];
}
case 'EnumDrop': {
return [
// drop after tables with columns with this enum are dropped
...items
.filter((item) => item.type === 'TableDrop')
.filter((item) => item.object.columns.some((column) => column.enumName === object.name))
.map((item) => tableDrop(item.object)),
];
}
case 'ConstraintAdd': {
return [
constraintDrop(object),
tableCreate({ name: object.tableName }),
object.type === ConstraintType.FOREIGN_KEY ? tableCreate({ name: object.referenceTableName }) : undefined,
];
}
case 'IndexCreate': {
return [
indexDrop(object),
// depend on the table
tableCreate({ name: object.tableName }),
// depend on columns being created
...(object.columnNames ?? []).map((name) => columnCreate({ name, tableName: object.tableName })),
];
}
case 'IndexRename': {
return [tableCreate({ name: object.old.tableName }), tableCreate({ name: object.new.tableName })];
}
case 'OverrideCreate':
case 'OverrideUpdate': {
return [tableCreate({ name: ctx.overrideTableName })];
}
default: {
return [];
}
}
}
};
-1
View File
@@ -25,7 +25,6 @@ export * from 'src/decorators/unique.decorator';
export * from 'src/decorators/update-date-column.decorator';
export * from 'src/migration';
export * from 'src/naming/default.naming';
export * from 'src/naming/hash.naming';
export * from 'src/naming/naming.interface';
export * from 'src/register-enum';
export * from 'src/register-function';
@@ -1,51 +0,0 @@
import { sha1 } from 'src/helpers';
import { DefaultNamingStrategy } from 'src/naming/default.naming';
import { NamingInterface, NamingItem } from 'src/naming/naming.interface';
const fallback = new DefaultNamingStrategy();
const asKey = (prefix: string, tableName: string, values: string[]) =>
(prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30);
export class HashNamingStrategy implements NamingInterface {
getName(item: NamingItem): string {
switch (item.type) {
case 'primaryKey': {
return asKey('PK_', item.tableName, item.columnNames);
}
case 'foreignKey': {
return asKey('FK_', item.tableName, item.columnNames);
}
case 'check': {
return asKey('CHK_', item.tableName, [item.expression]);
}
case 'unique': {
return asKey('UQ_', item.tableName, item.columnNames);
}
case 'index': {
const items: string[] = [];
for (const columnName of item.columnNames ?? []) {
items.push(columnName);
}
if (item.where) {
items.push(item.where);
}
return asKey('IDX_', item.tableName, items);
}
case 'trigger': {
return asKey('TR_', item.tableName, [...item.actions, item.scope, item.timing, item.functionName]);
}
default: {
return fallback.getName(item);
}
}
}
}
@@ -80,7 +80,6 @@ export const processColumns: Processor = (ctx, items) => {
identity: options.identity,
synchronize: options.synchronize ?? true,
},
options,
propertyName,
);
}
@@ -1,8 +1,8 @@
import { Processor } from 'src/types';
export const processEnums: Processor = (ctx, items) => {
for (const { item } of items.filter((item) => item.type === 'enum')) {
for (const result of items.filter((item) => item.type === 'enum')) {
// TODO log warnings if enum name is not unique
ctx.enums.push(item);
ctx.enums.push(result.item);
}
};
@@ -20,7 +20,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
}
const columnNames = [column.name];
const referenceColumns = referenceTable.columns.filter((column) => column.primary);
const referenceColumns = referenceTable.columns.filter(({ primary }) => primary);
// infer FK column type from reference table
if (referenceColumns.length === 1) {
@@ -28,7 +28,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
}
const referenceTableName = referenceTable.name;
const referenceColumnNames = referenceColumns.map((column) => column.name);
const referenceColumnNames = referenceColumns.map(({ name }) => name);
const name =
options.constraintName ||
ctx.getNameFor({
@@ -5,8 +5,8 @@ export const processFunctions: Processor = (ctx, items) => {
return;
}
for (const { item } of items.filter((item) => item.type === 'function')) {
for (const result of items.filter((item) => item.type === 'function')) {
// TODO log warnings if function name is not unique
ctx.functions.push(item);
ctx.functions.push(result.item);
}
};
@@ -10,6 +10,7 @@ import { processFunctions } from 'src/processors/function.processor';
import { processIndexes } from 'src/processors/index.processor';
import { processOverrides } from 'src/processors/override.processor';
import { processPrimaryKeyConstraints } from 'src/processors/primary-key-contraint.processor';
import { processTableLoops } from 'src/processors/table-loops.processor';
import { processTables } from 'src/processors/table.processor';
import { processTriggers } from 'src/processors/trigger.processor';
import { processUniqueConstraints } from 'src/processors/unique-constraint.processor';
@@ -31,4 +32,5 @@ export const processors: Processor[] = [
processIndexes,
processTriggers,
processOverrides,
processTableLoops,
];
@@ -0,0 +1,52 @@
import { depthFirstSearch, Graph } from 'graph-data-structure';
import { ConstraintType, DatabaseForeignKeyConstraint, Processor } from 'src/types';
export const processTableLoops: Processor = (ctx) => {
const tables = ctx.tables.filter((table) => table.synchronize);
const graph = new Graph<string>();
for (const table of tables) {
graph.addNode(table.name);
}
for (const table of tables) {
for (const constraint of table.constraints) {
if (constraint.type === ConstraintType.FOREIGN_KEY) {
graph.addEdge(constraint.tableName, constraint.referenceTableName);
}
}
}
while (true) {
const cycles = new Set<string>();
for (const table of tables) {
for (const adjacent of graph.adjacent(table.name) ?? []) {
const reachable = depthFirstSearch(graph, { sourceNodes: [adjacent] });
if (reachable.includes(table.name)) {
cycles.add(table.name);
break;
}
}
}
const [tableName] = cycles;
if (!tableName) {
break;
}
const constraint = (ctx.getTableByName(tableName)?.constraints ?? []).find(
(c): c is DatabaseForeignKeyConstraint =>
c.type === ConstraintType.FOREIGN_KEY && cycles.has(c.referenceTableName),
);
if (!constraint) {
throw new Error(
`Table ${tableName} was reported to be in a cycle, but does not have any foreign key references to cyclic tables`,
);
}
constraint.deferred = true;
graph.removeEdge(constraint.tableName, constraint.referenceTableName);
}
};
@@ -24,20 +24,20 @@ export const readConstraints: Reader = async (ctx, db) => {
'pg_constraint.confupdtype as update_action',
'pg_constraint.confdeltype as delete_action',
// 'pg_constraint.oid as constraint_id',
eb
.selectFrom('pg_attribute')
// matching table for PK, FK, and UQ
.whereRef('pg_attribute.attrelid', '=', 'pg_constraint.conrelid')
.whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."conkey")`)
.select((eb) => eb.fn<string[]>('json_agg', ['pg_attribute.attname']).as('column_name'))
.as('column_names'),
eb
.selectFrom('pg_attribute')
// matching foreign table for FK
.whereRef('pg_attribute.attrelid', '=', 'pg_constraint.confrelid')
.whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."confkey")`)
.select((eb) => eb.fn<string[]>('json_agg', ['pg_attribute.attname']).as('column_name'))
.as('reference_column_names'),
sql<string[]>`
(
SELECT json_agg("a"."attname" ORDER BY "k"."ord")
FROM unnest("pg_constraint"."conkey") WITH ORDINALITY AS k(attnum, ord)
JOIN pg_attribute a ON "a"."attrelid" = "pg_constraint"."conrelid" AND "a"."attnum" = "k"."attnum"
)
`.as('column_names'),
sql<string[]>`
(
SELECT json_agg("a"."attname" ORDER BY "k"."ord")
FROM unnest("pg_constraint"."confkey") WITH ORDINALITY AS k(attnum, ord)
JOIN pg_attribute a ON "a"."attrelid" = "pg_constraint"."confrelid" AND "a"."attnum" = "k"."attnum"
)
`.as('reference_column_names'),
eb.fn<string>('pg_get_constraintdef', ['pg_constraint.oid']).as('expression'),
])
.where('pg_namespace.nspname', '=', ctx.schemaName)
@@ -25,14 +25,15 @@ export const readIndexes: Reader = async (ctx, db) => {
'a.amname as using',
eb.fn<string>('pg_get_expr', ['ix.indexprs', 'ix.indrelid']).as('expression'),
eb.fn<string>('pg_get_expr', ['ix.indpred', 'ix.indrelid']).as('where'),
eb
.selectFrom('pg_attribute as a')
.where('t.relkind', '=', sql.lit('r'))
.whereRef('a.attrelid', '=', 't.oid')
// list of columns numbers in the index
.whereRef('a.attnum', '=', sql`any("ix"."indkey")`)
.select((eb) => eb.fn<string[]>('json_agg', ['a.attname']).as('column_name'))
.as('column_names'),
// preserve order from ix.indkey when mapping to column names
sql<string[]>`
(
SELECT json_agg("a"."attname" ORDER BY "k"."ord")
FROM unnest("ix"."indkey") WITH ORDINALITY AS k(attnum, ord)
JOIN pg_attribute a ON "a"."attrelid" = "t"."oid" AND "a"."attnum" = "k"."attnum"
WHERE "t"."relkind" = 'r'
)
`.as('column_names'),
])
.where('pg_namespace.nspname', '=', ctx.schemaName)
.where('ix.indisprimary', '=', sql.lit(false))
+75 -109
View File
@@ -200,7 +200,7 @@ describe(schemaDiff.name, () => {
expect(diff.items).toHaveLength(1);
expect(diff.items[0]).toEqual({
type: 'TableCreate',
table: {
object: {
name: 'table1',
columns: [column],
constraints: [],
@@ -226,7 +226,7 @@ describe(schemaDiff.name, () => {
expect(diff.items).toHaveLength(1);
expect(diff.items[0]).toEqual({
type: 'TableDrop',
tableName: 'table1',
object: expect.objectContaining({ name: 'table1' }),
reason: 'missing in source',
});
});
@@ -266,7 +266,7 @@ describe(schemaDiff.name, () => {
expect(diff.items).toEqual([
{
type: 'ColumnAdd',
column: {
object: {
tableName: 'table1',
isArray: false,
primary: false,
@@ -300,8 +300,7 @@ describe(schemaDiff.name, () => {
expect(diff.items).toEqual([
{
type: 'ColumnDrop',
tableName: 'table1',
columnName: 'column2',
object: expect.objectContaining({ name: 'column2', tableName: 'table1' }),
reason: 'missing in source',
},
]);
@@ -310,18 +309,17 @@ describe(schemaDiff.name, () => {
describe('nullable', () => {
it('should make a column nullable', () => {
const diff = schemaDiff(
fromColumn({ name: 'column1', nullable: true }),
fromColumn({ name: 'column1', nullable: false }),
);
const source = { name: 'column1', nullable: true };
const target = { name: 'column1', nullable: false };
const diff = schemaDiff(fromColumn(source), fromColumn(target));
expect(diff.items).toEqual([
{
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: {
nullable: true,
object: {
old: expect.objectContaining(target),
new: expect.objectContaining(source),
changes: { nullable: true },
},
reason: 'nullable is different (true vs false)',
},
@@ -329,18 +327,17 @@ describe(schemaDiff.name, () => {
});
it('should make a column non-nullable', () => {
const diff = schemaDiff(
fromColumn({ name: 'column1', nullable: false }),
fromColumn({ name: 'column1', nullable: true }),
);
const source = { name: 'column1', nullable: false };
const target = { name: 'column1', nullable: true };
const diff = schemaDiff(fromColumn(source), fromColumn(target));
expect(diff.items).toEqual([
{
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: {
nullable: false,
object: {
old: expect.objectContaining(target),
new: expect.objectContaining(source),
changes: { nullable: false },
},
reason: 'nullable is different (false vs true)',
},
@@ -350,18 +347,19 @@ describe(schemaDiff.name, () => {
describe('default', () => {
it('should set a default value to a function', () => {
const diff = schemaDiff(
fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }),
fromColumn({ name: 'column1' }),
);
const source = { name: 'column1', default: 'uuid_generate_v4()' };
const target = { name: 'column1' };
const diff = schemaDiff(fromColumn(source), fromColumn(target));
expect(diff.items).toEqual([
{
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: {
default: 'uuid_generate_v4()',
object: {
old: expect.objectContaining(target),
new: expect.objectContaining(source),
changes: {
default: 'uuid_generate_v4()',
},
},
reason: 'default is different (uuid_generate_v4() vs undefined)',
},
@@ -414,27 +412,20 @@ describe(schemaDiff.name, () => {
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(),
);
const source: DatabaseConstraint = {
name: 'PK_test',
type: ConstraintType.PRIMARY_KEY,
tableName: 'table1',
columnNames: ['id'],
synchronize: true,
};
const diff = schemaDiff(fromConstraint(source), fromConstraint());
expect(diff.items).toEqual([
{
type: 'ConstraintAdd',
constraint: {
type: ConstraintType.PRIMARY_KEY,
name: 'PK_test',
columnNames: ['id'],
tableName: 'table1',
synchronize: true,
},
object: source,
reason: 'missing in target',
},
]);
@@ -443,22 +434,19 @@ describe(schemaDiff.name, () => {
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,
}),
);
const target: DatabaseConstraint = {
name: 'PK_test',
type: ConstraintType.PRIMARY_KEY,
tableName: 'table1',
columnNames: ['id'],
synchronize: true,
};
const diff = schemaDiff(fromConstraint(), fromConstraint(target));
expect(diff.items).toEqual([
{
type: 'ConstraintDrop',
tableName: 'table1',
constraintName: 'PK_test',
object: target,
reason: 'missing in source',
},
]);
@@ -499,7 +487,7 @@ describe(schemaDiff.name, () => {
});
it('should drop and recreate when the column changes', () => {
const constraint: DatabaseConstraint = {
const source: DatabaseConstraint = {
type: ConstraintType.FOREIGN_KEY,
name: 'FK_test',
tableName: 'table1',
@@ -508,37 +496,26 @@ describe(schemaDiff.name, () => {
referenceColumnNames: ['id'],
synchronize: true,
};
const target = { ...source, columnNames: ['parentId2'] };
const diff = schemaDiff(
fromConstraint(constraint),
fromConstraint({ ...constraint, columnNames: ['parentId2'] }),
);
const diff = schemaDiff(fromConstraint(source), fromConstraint(target));
expect(diff.items).toEqual([
{
constraintName: 'FK_test',
reason: 'columns are different (parentId vs parentId2)',
tableName: 'table1',
type: 'ConstraintDrop',
object: target,
reason: 'columns are different (parentId vs parentId2)',
},
{
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',
object: source,
reason: 'columns are different (parentId vs parentId2)',
},
]);
});
it('should drop and recreate when the ON DELETE action changes', () => {
const constraint: DatabaseConstraint = {
const source: DatabaseConstraint = {
type: ConstraintType.FOREIGN_KEY,
name: 'FK_test',
tableName: 'table1',
@@ -548,29 +525,19 @@ describe(schemaDiff.name, () => {
onDelete: ActionType.CASCADE,
synchronize: true,
};
const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined }));
const target = { ...source, onDelete: undefined };
const diff = schemaDiff(fromConstraint(source), fromConstraint(target));
expect(diff.items).toEqual([
{
constraintName: 'FK_test',
reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
tableName: 'table1',
type: 'ConstraintDrop',
object: target,
reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
},
{
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',
object: source,
reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
},
]);
});
@@ -626,7 +593,7 @@ describe(schemaDiff.name, () => {
expect(diff.items).toEqual([
{
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
columnNames: ['id'],
tableName: 'table1',
@@ -641,21 +608,19 @@ describe(schemaDiff.name, () => {
describe('IndexDrop', () => {
it('should detect an extra index', () => {
const diff = schemaDiff(
fromIndex(),
fromIndex({
name: 'IDX_test',
unique: true,
tableName: 'table1',
columnNames: ['id'],
synchronize: true,
}),
);
const target = {
name: 'IDX_test',
unique: true,
tableName: 'table1',
columnNames: ['id'],
synchronize: true,
};
const diff = schemaDiff(fromIndex(), fromIndex(target));
expect(diff.items).toEqual([
{
type: 'IndexDrop',
indexName: 'IDX_test',
object: target,
reason: 'missing in source',
},
]);
@@ -663,24 +628,25 @@ describe(schemaDiff.name, () => {
});
it('should recreate the index if unique changes', () => {
const index: DatabaseIndex = {
const source: DatabaseIndex = {
name: 'IDX_test',
tableName: 'table1',
columnNames: ['id'],
unique: true,
synchronize: true,
};
const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false }));
const target = { ...source, unique: false };
const diff = schemaDiff(fromIndex(source), fromIndex(target));
expect(diff.items).toEqual([
{
type: 'IndexDrop',
indexName: 'IDX_test',
object: target,
reason: 'uniqueness is different (true vs false)',
},
{
type: 'IndexCreate',
index,
object: source,
reason: 'uniqueness is different (true vs false)',
},
]);
+44 -111
View File
@@ -5,16 +5,9 @@ import { compareOverrides } from 'src/comparers/override.comparer';
import { compareParameters } from 'src/comparers/parameter.comparer';
import { compareTables } from 'src/comparers/table.comparer';
import { BaseContext } from 'src/contexts/base-context';
import { compare } from 'src/helpers';
import { compare, getSchemaItemChildrenIds, getSchemaItemId, topologicalSort } from 'src/helpers';
import { transformers } from 'src/transformers';
import {
ConstraintType,
DatabaseSchema,
SchemaDiff,
SchemaDiffOptions,
SchemaDiffResult,
SchemaDiffToSqlOptions,
} from 'src/types';
import { DatabaseSchema, SchemaDiff, SchemaDiffOptions, SchemaDiffResult, SchemaDiffToSqlOptions } from 'src/types';
/**
* Compute the difference between two database schemas
@@ -24,6 +17,8 @@ export const schemaDiff = (
target: DatabaseSchema,
options: SchemaDiffOptions = {},
): SchemaDiffResult => {
const ctx = new BaseContext(options);
const items = [
...compare(source.parameters, target.parameters, options.parameters, compareParameters()),
...compare(source.extensions, target.extensions, options.extensions, compareExtensions()),
@@ -33,81 +28,19 @@ export const schemaDiff = (
...compare(source.overrides, target.overrides, options.overrides, compareOverrides()),
];
type SchemaName = SchemaDiff['type'];
const itemMap: Record<SchemaName, SchemaDiff[]> = {
ColumnRename: [],
ConstraintRename: [],
IndexRename: [],
const filteredItems = items.some((item) => item.type === 'TableDrop' && item.object.name === ctx.overrideTableName)
? // override table is being dropped, so ignore override changes
items.filter((item) => item.type !== 'OverrideCreate' && item.type !== 'OverrideUpdate')
: items;
ExtensionDrop: [],
ExtensionCreate: [],
ParameterSet: [],
ParameterReset: [],
FunctionDrop: [],
FunctionCreate: [],
EnumDrop: [],
EnumCreate: [],
TriggerDrop: [],
ConstraintDrop: [],
TableDrop: [],
ColumnDrop: [],
ColumnAdd: [],
ColumnAlter: [],
TableCreate: [],
ConstraintAdd: [],
TriggerCreate: [],
IndexCreate: [],
IndexDrop: [],
OverrideCreate: [],
OverrideUpdate: [],
OverrideDrop: [],
};
for (const item of items) {
itemMap[item.type].push(item);
}
const constraintAdds = itemMap.ConstraintAdd.filter((item) => item.type === 'ConstraintAdd');
const orderedItems = [
...itemMap.ExtensionCreate,
...itemMap.FunctionCreate,
...itemMap.ParameterSet,
...itemMap.ParameterReset,
...itemMap.EnumCreate,
...itemMap.IndexDrop,
...itemMap.ConstraintDrop,
...itemMap.TableCreate,
...itemMap.ColumnAlter,
...itemMap.ColumnAdd,
...itemMap.ColumnRename,
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE),
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK),
...itemMap.ConstraintRename,
...itemMap.IndexCreate,
...itemMap.IndexRename,
...itemMap.TriggerCreate,
...itemMap.OverrideCreate,
...itemMap.OverrideUpdate,
...itemMap.OverrideDrop,
...itemMap.ColumnDrop,
...itemMap.TriggerDrop,
...itemMap.TableDrop,
...itemMap.FunctionDrop,
...itemMap.EnumDrop,
];
const orderedItems = topologicalSort(filteredItems, {
getId: getSchemaItemId,
getChildrenIds: (item) => getSchemaItemChildrenIds(ctx, item, items),
});
return {
items: orderedItems,
asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options),
asSql: (diffOptions?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, diffOptions),
asHuman: () => schemaDiffToHuman(orderedItems),
};
};
@@ -129,95 +62,95 @@ export const schemaDiffToHuman = (items: SchemaDiff[]): string[] => {
export const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => {
const ctx = new BaseContext(options);
for (const transform of transformers) {
const result = transform(ctx, item);
if (!result) {
const results = transform(ctx, item);
if (!results) {
continue;
}
return asArray(result).map((result) => result + withComments(options.comments, item));
return asArray(results).map((result) => result + withComments(options.comments, item));
}
throw new Error(`Unhandled schema diff type: ${item.type}`);
};
export const asHuman = (item: SchemaDiff): string => {
switch (item.type) {
export const asHuman = ({ object, type }: SchemaDiff): string => {
switch (type) {
case 'ExtensionCreate': {
return `The extension "${item.extension.name}" is missing and needs to be created`;
return `The extension "${object.name}" is missing and needs to be created`;
}
case 'ExtensionDrop': {
return `The extension "${item.extensionName}" exists but is no longer needed`;
return `The extension "${object.name}" exists but is no longer needed`;
}
case 'FunctionCreate': {
return `The function "${item.function.name}" is missing and needs to be created`;
return `The function "${object.name}" is missing and needs to be created`;
}
case 'FunctionDrop': {
return `The function "${item.functionName}" exists but should be removed`;
return `The function "${object.name}" exists but should be removed`;
}
case 'TableCreate': {
return `The table "${item.table.name}" is missing and needs to be created`;
return `The table "${object.name}" is missing and needs to be created`;
}
case 'TableDrop': {
return `The table "${item.tableName}" exists but should be removed`;
return `The table "${object.name}" exists but should be removed`;
}
case 'ColumnAdd': {
return `The column "${item.column.tableName}"."${item.column.name}" is missing and needs to be created`;
return `The column "${object.tableName}"."${object.name}" is missing and needs to be created`;
}
case 'ColumnRename': {
return `The column "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
return `The column "${object.new.tableName}"."${object.old.name}" was renamed to "${object.new.tableName}"."${object.new.name}"`;
}
case 'ColumnAlter': {
return `The column "${item.tableName}"."${item.columnName}" has changes that need to be applied ${JSON.stringify(
item.changes,
return `The column "${object.new.tableName}"."${object.new.name}" has changes that need to be applied ${JSON.stringify(
object.changes,
)}`;
}
case 'ColumnDrop': {
return `The column "${item.tableName}"."${item.columnName}" exists but should be removed`;
return `The column "${object.tableName}"."${object.name}" exists but should be removed`;
}
case 'ConstraintAdd': {
return `The constraint "${item.constraint.tableName}"."${item.constraint.name}" (${item.constraint.type}) is missing and needs to be created`;
return `The constraint "${object.tableName}"."${object.name}" (${object.type}) is missing and needs to be created`;
}
case 'ConstraintRename': {
return `The constraint "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
return `The constraint "${object.old.tableName}"."${object.old.name}" was renamed to "${object.new.tableName}"."${object.new.name}"`;
}
case 'ConstraintDrop': {
return `The constraint "${item.tableName}"."${item.constraintName}" exists but should be removed`;
return `The constraint "${object.tableName}"."${object.name}" exists but should be removed`;
}
case 'IndexCreate': {
return `The index "${item.index.tableName}"."${item.index.name}" is missing and needs to be created`;
return `The index "${object.tableName}"."${object.name}" is missing and needs to be created`;
}
case 'IndexRename': {
return `The index "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
return `The index "${object.old.tableName}"."${object.old.name}" was renamed to "${object.new.tableName}"."${object.new.name}"`;
}
case 'IndexDrop': {
return `The index "${item.indexName}" exists but is no longer needed`;
return `The index "${object.name}" exists but is no longer needed`;
}
case 'TriggerCreate': {
return `The trigger "${item.trigger.tableName}"."${item.trigger.name}" is missing and needs to be created`;
return `The trigger "${object.tableName}"."${object.name}" is missing and needs to be created`;
}
case 'TriggerDrop': {
return `The trigger "${item.tableName}"."${item.triggerName}" exists but is no longer needed`;
return `The trigger "${object.tableName}"."${object.name}" exists but is no longer needed`;
}
case 'ParameterSet': {
return `The configuration parameter "${item.parameter.name}" has a different value and needs to be updated to "${item.parameter.value}"`;
return `The configuration parameter "${object.name}" has a different value and needs to be updated to "${object.value}"`;
}
case 'ParameterReset': {
return `The configuration parameter "${item.parameterName}" is set, but should be reset to the default value`;
return `The configuration parameter "${object.name}" is set, but should be reset to the default value`;
}
case 'EnumCreate': {
return `The enum "${item.enum.name}" is missing and needs to be created`;
return `The enum "${object.name}" is missing and needs to be created`;
}
case 'EnumDrop': {
return `The enum "${item.enumName}" exists but is no longer needed`;
return `The enum "${object.name}" exists but is no longer needed`;
}
case 'OverrideCreate': {
return `The override "${item.override.name}" is missing and needs to be created`;
return `The override "${object.name}" is missing and needs to be created`;
}
case 'OverrideUpdate': {
return `The override "${item.override.name}" needs to be updated`;
return `The override "${object.name}" needs to be updated`;
}
case 'OverrideDrop': {
return `The override "${item.overrideName}" exists but is no longer needed`;
return `The override "${object.name}" exists but is no longer needed`;
}
}
};
@@ -1,9 +1,9 @@
import { schemaFromCode } from 'src/schema-from-code';
import { importFixture, loadFixtures } from 'test/utils';
import { importFixture, getDirectoryFiles } from 'test/utils';
import { describe, expect, it } from 'vitest';
const fixtures = loadFixtures('test/fixtures');
const errorFixtures = loadFixtures('test/fixtures/errors');
const fixtures = getDirectoryFiles('test/fixtures');
const errorFixtures = getDirectoryFiles('test/fixtures/errors');
describe(schemaFromCode.name, () => {
it('should work', () => {
+1 -1
View File
@@ -15,7 +15,7 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
processor(ctx, items);
}
if (ctx.options.overrides) {
if (ctx.options.overrides && ctx.overrides.length > 0) {
ctx.tables.push({
name: ctx.overrideTableName,
columns: [
@@ -1,5 +1,6 @@
import { BaseContext } from 'src/contexts/base-context';
import { transformColumns } from 'src/transformers/column.transformer';
import { DatabaseColumn } from 'src/types';
import { describe, expect, it } from 'vitest';
const ctx = new BaseContext({});
@@ -10,7 +11,7 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAdd',
column: {
object: {
name: 'column1',
tableName: 'table1',
primary: false,
@@ -28,7 +29,7 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAdd',
column: {
object: {
name: 'column1',
tableName: 'table1',
primary: false,
@@ -46,7 +47,7 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAdd',
column: {
object: {
name: 'column1',
tableName: 'table1',
primary: false,
@@ -65,7 +66,7 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAdd',
column: {
object: {
name: 'column1',
tableName: 'table1',
primary: false,
@@ -85,9 +86,17 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: { nullable: true },
object: {
new: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
old: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
changes: { nullable: true },
},
reason: 'unknown',
}),
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" DROP NOT NULL;`]);
@@ -97,9 +106,17 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: { nullable: false },
object: {
new: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
old: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
changes: { nullable: false },
},
reason: 'unknown',
}),
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET NOT NULL;`]);
@@ -109,9 +126,17 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: { default: 'uuid_generate_v4()' },
object: {
new: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
old: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
changes: { default: 'uuid_generate_v4()' },
},
reason: 'unknown',
}),
).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT uuid_generate_v4();`]);
@@ -121,10 +146,16 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnAlter',
tableName: 'table1',
columnName: 'column1',
changes: {
default: 'NULL',
object: {
new: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
old: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
changes: { default: 'NULL' },
},
reason: 'unknown',
}),
@@ -137,8 +168,10 @@ describe(transformColumns.name, () => {
expect(
transformColumns(ctx, {
type: 'ColumnDrop',
tableName: 'table1',
columnName: 'column1',
object: {
name: 'column1',
tableName: 'table1',
} as DatabaseColumn,
reason: 'unknown',
}),
).toEqual(`ALTER TABLE "table1" DROP COLUMN "column1";`);
@@ -2,22 +2,22 @@ import { asColumnComment, getColumnModifiers, getColumnType } from 'src/helpers'
import { SqlTransformer } from 'src/transformers/types';
import { ColumnChanges, DatabaseColumn } from 'src/types';
export const transformColumns: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformColumns: SqlTransformer = (ctx, { type, object }) => {
switch (type) {
case 'ColumnAdd': {
return asColumnAdd(item.column);
return asColumnAdd(object);
}
case 'ColumnAlter': {
return asColumnAlter(item.tableName, item.columnName, item.changes);
return asColumnAlter(object.new.tableName, object.new.name, object.changes);
}
case 'ColumnRename': {
return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`;
return `ALTER TABLE "${object.new.tableName}" RENAME COLUMN "${object.old.name}" TO "${object.new.name}";`;
}
case 'ColumnDrop': {
return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`;
return `ALTER TABLE "${object.tableName}" DROP COLUMN "${object.name}";`;
}
default: {
@@ -1,6 +1,6 @@
import { BaseContext } from 'src/contexts/base-context';
import { transformConstraints } from 'src/transformers/constraint.transformer';
import { ConstraintType } from 'src/types';
import { ConstraintType, DatabasePrimaryKeyConstraint } from 'src/types';
import { describe, expect, it } from 'vitest';
const ctx = new BaseContext({});
@@ -12,7 +12,7 @@ describe(transformConstraints.name, () => {
expect(
transformConstraints(ctx, {
type: 'ConstraintAdd',
constraint: {
object: {
type: ConstraintType.PRIMARY_KEY,
name: 'PK_test',
tableName: 'table1',
@@ -30,7 +30,7 @@ describe(transformConstraints.name, () => {
expect(
transformConstraints(ctx, {
type: 'ConstraintAdd',
constraint: {
object: {
type: ConstraintType.FOREIGN_KEY,
name: 'FK_test',
tableName: 'table1',
@@ -52,7 +52,7 @@ describe(transformConstraints.name, () => {
expect(
transformConstraints(ctx, {
type: 'ConstraintAdd',
constraint: {
object: {
type: ConstraintType.UNIQUE,
name: 'UQ_test',
tableName: 'table1',
@@ -70,7 +70,7 @@ describe(transformConstraints.name, () => {
expect(
transformConstraints(ctx, {
type: 'ConstraintAdd',
constraint: {
object: {
type: ConstraintType.CHECK,
name: 'CHK_test',
tableName: 'table1',
@@ -89,8 +89,10 @@ describe(transformConstraints.name, () => {
expect(
transformConstraints(ctx, {
type: 'ConstraintDrop',
tableName: 'table1',
constraintName: 'PK_test',
object: {
name: 'PK_test',
tableName: 'table1',
} as DatabasePrimaryKeyConstraint,
reason: 'unknown',
}),
).toEqual(`ALTER TABLE "table1" DROP CONSTRAINT "PK_test";`);
@@ -2,18 +2,18 @@ import { asColumnList } from 'src/helpers';
import { SqlTransformer } from 'src/transformers/types';
import { ActionType, ConstraintType, DatabaseConstraint } from 'src/types';
export const transformConstraints: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformConstraints: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'ConstraintAdd': {
return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`;
return `ALTER TABLE "${object.tableName}" ADD ${asConstraintBody(object)};`;
}
case 'ConstraintRename': {
return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`;
return `ALTER TABLE "${object.new.tableName}" RENAME CONSTRAINT "${object.old.name}" TO "${object.new.name}";`;
}
case 'ConstraintDrop': {
return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`;
return `ALTER TABLE "${object.tableName}" DROP CONSTRAINT "${object.name}";`;
}
default: {
return false;
@@ -1,14 +1,13 @@
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseEnum } from 'src/types';
export const transformEnums: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformEnums: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'EnumCreate': {
return asEnumCreate(item.enum);
return `CREATE TYPE "${object.name}" AS ENUM (${object.values.map((value) => `'${value}'`)});`;
}
case 'EnumDrop': {
return asEnumDrop(item.enumName);
return `DROP TYPE "${object.name}";`;
}
default: {
@@ -16,11 +15,3 @@ export const transformEnums: SqlTransformer = (ctx, item) => {
}
}
};
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}";`;
};
@@ -10,7 +10,7 @@ describe(transformExtensions.name, () => {
expect(
transformExtensions(ctx, {
type: 'ExtensionDrop',
extensionName: 'cube',
object: { name: 'cube', synchronize: true },
reason: 'unknown',
}),
).toEqual(`DROP EXTENSION "cube";`);
@@ -22,10 +22,7 @@ describe(transformExtensions.name, () => {
expect(
transformExtensions(ctx, {
type: 'ExtensionCreate',
extension: {
name: 'cube',
synchronize: true,
},
object: { name: 'cube', synchronize: true },
reason: 'unknown',
}),
).toEqual(`CREATE EXTENSION IF NOT EXISTS "cube";`);
@@ -1,14 +1,14 @@
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseExtension } from 'src/types';
export const transformExtensions: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformExtensions: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'ExtensionCreate': {
return asExtensionCreate(item.extension);
return asExtensionCreate(object);
}
case 'ExtensionDrop': {
return asExtensionDrop(item.extensionName);
return asExtensionDrop(object.name);
}
default: {
@@ -1,5 +1,6 @@
import { BaseContext } from 'src/contexts/base-context';
import { transformFunctions } from 'src/transformers/function.transformer';
import { DatabaseFunction } from 'src/types';
import { describe, expect, it } from 'vitest';
const ctx = new BaseContext({});
@@ -10,7 +11,7 @@ describe(transformFunctions.name, () => {
expect(
transformFunctions(ctx, {
type: 'FunctionDrop',
functionName: 'test_func',
object: { name: 'test_func' } as DatabaseFunction,
reason: 'unknown',
}),
).toEqual(`DROP FUNCTION test_func;`);
@@ -1,14 +1,14 @@
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseFunction } from 'src/types';
export const transformFunctions: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformFunctions: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'FunctionCreate': {
return asFunctionCreate(item.function);
return asFunctionCreate(object);
}
case 'FunctionDrop': {
return asFunctionDrop(item.functionName);
return asFunctionDrop(object.name);
}
default: {
@@ -1,5 +1,6 @@
import { BaseContext } from 'src/contexts/base-context';
import { transformIndexes } from 'src/transformers/index.transformer';
import { DatabaseIndex } from 'src/types';
import { describe, expect, it } from 'vitest';
const ctx = new BaseContext({});
@@ -10,7 +11,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
tableName: 'table1',
columnNames: ['column1'],
@@ -26,7 +27,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
tableName: 'table1',
columnNames: ['column1'],
@@ -42,7 +43,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
tableName: 'table1',
unique: false,
@@ -58,7 +59,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
tableName: 'table1',
columnNames: ['id'],
@@ -75,7 +76,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexCreate',
index: {
object: {
name: 'IDX_test',
tableName: 'table1',
unique: false,
@@ -94,7 +95,7 @@ describe(transformIndexes.name, () => {
expect(
transformIndexes(ctx, {
type: 'IndexDrop',
indexName: 'IDX_test',
object: { name: 'IDX_test' } as DatabaseIndex,
reason: 'unknown',
}),
).toEqual(`DROP INDEX "IDX_test";`);
@@ -2,18 +2,18 @@ import { asColumnList } from 'src/helpers';
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseIndex } from 'src/types';
export const transformIndexes: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformIndexes: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'IndexCreate': {
return asIndexCreate(item.index);
return asIndexCreate(object);
}
case 'IndexRename': {
return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`;
return `ALTER INDEX "${object.old.name}" RENAME TO "${object.new.name}";`;
}
case 'IndexDrop': {
return `DROP INDEX "${item.indexName}";`;
return `DROP INDEX "${object.name}";`;
}
default: {
@@ -1,24 +1,22 @@
import { asJsonString } from 'src/helpers';
import { SqlTransformer } from 'src/transformers/types';
export const transformOverrides: SqlTransformer = (ctx, item) => {
export const transformOverrides: SqlTransformer = (ctx, { object, type }) => {
const tableName = ctx.overrideTableName;
const toJson = (value: unknown) => asJsonString(value, { outputTarget: ctx.outputTarget });
switch (item.type) {
switch (type) {
case 'OverrideCreate': {
const override = item.override;
return `INSERT INTO "${tableName}" ("name", "value") VALUES ('${override.name}', ${toJson(override.value)});`;
return `INSERT INTO "${tableName}" ("name", "value") VALUES ('${object.name}', ${toJson(object.value)});`;
}
case 'OverrideUpdate': {
const override = item.override;
return `UPDATE "${tableName}" SET "value" = ${toJson(override.value)} WHERE "name" = '${override.name}';`;
return `UPDATE "${tableName}" SET "value" = ${toJson(object.value)} WHERE "name" = '${object.name}';`;
}
case 'OverrideDrop': {
return `DELETE FROM "${tableName}" WHERE "name" = '${item.overrideName}';`;
return `DELETE FROM "${tableName}" WHERE "name" = '${object.name}';`;
}
default: {
@@ -1,14 +1,14 @@
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseParameter } from 'src/types';
export const transformParameters: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformParameters: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'ParameterSet': {
return asParameterSet(item.parameter);
return asParameterSet(object);
}
case 'ParameterReset': {
return asParameterReset(item.databaseName, item.parameterName);
return asParameterReset(object.databaseName, object.name);
}
default: {
@@ -71,7 +71,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableDrop',
tableName: 'table1',
object: { name: 'table1' } as DatabaseTable,
reason: 'unknown',
}),
).toEqual(`DROP TABLE "table1";`);
@@ -83,7 +83,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableCreate',
table: table1,
object: table1,
reason: 'unknown',
}),
).toEqual([
@@ -94,7 +94,6 @@ describe(transformTables.name, () => {
CONSTRAINT "constraint2" FOREIGN KEY ("column1") REFERENCES "table2" ("parentId") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "constraint3" UNIQUE ("column1")
);`,
`CREATE INDEX "index1" ON "table1" ("column2");`,
]);
});
@@ -102,7 +101,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableCreate',
table: {
object: {
name: 'table1',
columns: [
{
@@ -133,7 +132,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableCreate',
table: {
object: {
name: 'table1',
columns: [
{
@@ -165,7 +164,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableCreate',
table: {
object: {
name: 'table1',
columns: [
{
@@ -197,7 +196,7 @@ describe(transformTables.name, () => {
expect(
transformTables(ctx, {
type: 'TableCreate',
table: {
object: {
name: 'table1',
columns: [
{
@@ -1,19 +1,43 @@
import { asColumnComment, getColumnModifiers, getColumnType } from 'src/helpers';
import { asColumnAlter } from 'src/transformers/column.transformer';
import { asConstraintBody } from 'src/transformers/constraint.transformer';
import { asIndexCreate } from 'src/transformers/index.transformer';
import { asTriggerCreate } from 'src/transformers/trigger.transformer';
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseTable } from 'src/types';
import { ConstraintType } from 'src/types';
export const transformTables: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformTables: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'TableCreate': {
return asTableCreate(item.table);
const tableName = object.name;
const items: string[] = [];
for (const column of object.columns) {
items.push(`"${column.name}" ${getColumnType(column)}${getColumnModifiers(column)}`);
}
for (const constraint of object.constraints) {
if (constraint.type === ConstraintType.FOREIGN_KEY && constraint.deferred) {
continue;
}
items.push(asConstraintBody(constraint));
}
const sql = [`CREATE TABLE "${tableName}" (\n ${items.join(',\n ')}\n);`];
for (const column of object.columns) {
if (column.comment) {
sql.push(asColumnComment(tableName, column.name, column.comment));
}
if (column.storage) {
sql.push(...asColumnAlter(tableName, column.name, { storage: column.storage }));
}
}
return sql;
}
case 'TableDrop': {
return asTableDrop(item.tableName);
return `DROP TABLE "${object.name}";`;
}
default: {
@@ -21,42 +45,3 @@ export const transformTables: SqlTransformer = (ctx, item) => {
}
}
};
const asTableCreate = (table: DatabaseTable) => {
const tableName = table.name;
const items: string[] = [];
for (const column of table.columns) {
items.push(`"${column.name}" ${getColumnType(column)}${getColumnModifiers(column)}`);
}
for (const constraint of table.constraints) {
items.push(asConstraintBody(constraint));
}
const sql = [`CREATE TABLE "${tableName}" (\n ${items.join(',\n ')}\n);`];
for (const column of table.columns) {
if (column.comment) {
sql.push(asColumnComment(tableName, column.name, column.comment));
}
if (column.storage) {
sql.push(...asColumnAlter(tableName, column.name, { storage: column.storage }));
}
}
for (const index of table.indexes) {
sql.push(asIndexCreate(index));
}
for (const trigger of table.triggers) {
sql.push(asTriggerCreate(trigger));
}
return sql;
};
const asTableDrop = (tableName: string) => {
return `DROP TABLE "${tableName}";`;
};
@@ -1,5 +1,6 @@
import { BaseContext } from 'src/contexts/base-context';
import { transformTriggers } from 'src/transformers/trigger.transformer';
import { DatabaseTrigger } from 'src/types';
import { describe, expect, it } from 'vitest';
const ctx = new BaseContext({});
@@ -10,7 +11,7 @@ describe(transformTriggers.name, () => {
expect(
transformTriggers(ctx, {
type: 'TriggerCreate',
trigger: {
object: {
name: 'trigger1',
tableName: 'table1',
timing: 'before',
@@ -33,7 +34,7 @@ describe(transformTriggers.name, () => {
expect(
transformTriggers(ctx, {
type: 'TriggerCreate',
trigger: {
object: {
name: 'trigger1',
tableName: 'table1',
timing: 'before',
@@ -56,7 +57,7 @@ describe(transformTriggers.name, () => {
expect(
transformTriggers(ctx, {
type: 'TriggerCreate',
trigger: {
object: {
name: 'trigger1',
tableName: 'table1',
timing: 'before',
@@ -84,8 +85,10 @@ describe(transformTriggers.name, () => {
expect(
transformTriggers(ctx, {
type: 'TriggerDrop',
tableName: 'table1',
triggerName: 'trigger1',
object: {
name: 'trigger1',
tableName: 'table1',
} as DatabaseTrigger,
reason: 'unknown',
}),
).toEqual(`DROP TRIGGER "trigger1" ON "table1";`);
@@ -1,14 +1,14 @@
import { SqlTransformer } from 'src/transformers/types';
import { DatabaseTrigger } from 'src/types';
export const transformTriggers: SqlTransformer = (ctx, item) => {
switch (item.type) {
export const transformTriggers: SqlTransformer = (ctx, { object, type }) => {
switch (type) {
case 'TriggerCreate': {
return asTriggerCreate(item.trigger);
return asTriggerCreate(object);
}
case 'TriggerDrop': {
return asTriggerDrop(item.tableName, item.triggerName);
return asTriggerDrop(object.tableName, object.name);
}
default: {
+31 -28
View File
@@ -48,7 +48,7 @@ export type BaseContextOptions = {
databaseName?: string;
schemaName?: string;
overrideTableName?: string;
namingStrategy?: 'default' | 'hash' | NamingInterface;
namingStrategy?: 'default' | NamingInterface;
uuidFunction?: string | UuidFunctionFactory;
outputTarget?: OutputTarget;
};
@@ -495,6 +495,8 @@ export type DatabaseForeignKeyConstraint = ColumBasedConstraint & {
referenceColumnNames: string[];
onUpdate?: ActionType;
onDelete?: ActionType;
/** table create sql should not inline this constraint */
deferred?: boolean;
synchronize: boolean;
};
@@ -536,33 +538,34 @@ export type DatabaseIndex = {
synchronize: boolean;
};
export type SchemaDiff = { reason: string } & (
| { type: 'ExtensionCreate'; extension: DatabaseExtension }
| { type: 'ExtensionDrop'; extensionName: string }
| { type: 'FunctionCreate'; function: DatabaseFunction }
| { type: 'FunctionDrop'; functionName: string }
| { type: 'TableCreate'; table: DatabaseTable }
| { type: 'TableDrop'; tableName: string }
| { type: 'ColumnAdd'; column: DatabaseColumn }
| { type: 'ColumnRename'; tableName: string; oldName: string; newName: string }
| { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges }
| { type: 'ColumnDrop'; tableName: string; columnName: string }
| { type: 'ConstraintAdd'; constraint: DatabaseConstraint }
| { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string }
| { type: 'ConstraintDrop'; tableName: string; constraintName: string }
| { type: 'IndexCreate'; index: DatabaseIndex }
| { type: 'IndexRename'; tableName: string; oldName: string; newName: string }
| { type: 'IndexDrop'; indexName: string }
| { type: 'TriggerCreate'; trigger: DatabaseTrigger }
| { type: 'TriggerDrop'; tableName: string; triggerName: string }
| { type: 'ParameterSet'; parameter: DatabaseParameter }
| { type: 'ParameterReset'; databaseName: string; parameterName: string }
| { type: 'EnumCreate'; enum: DatabaseEnum }
| { type: 'EnumDrop'; enumName: string }
| { type: 'OverrideCreate'; override: DatabaseOverride }
| { type: 'OverrideUpdate'; override: DatabaseOverride }
| { type: 'OverrideDrop'; overrideName: string }
);
export type SchemaItem =
| { type: 'ExtensionCreate'; object: DatabaseExtension }
| { type: 'ExtensionDrop'; object: DatabaseExtension }
| { type: 'FunctionCreate'; object: DatabaseFunction }
| { type: 'FunctionDrop'; object: DatabaseFunction }
| { type: 'TableCreate'; object: DatabaseTable }
| { type: 'TableDrop'; object: DatabaseTable }
| { type: 'ColumnAdd'; object: DatabaseColumn }
| { type: 'ColumnRename'; object: { old: DatabaseColumn; new: DatabaseColumn } }
| { type: 'ColumnAlter'; object: { old: DatabaseColumn; new: DatabaseColumn; changes: ColumnChanges } }
| { type: 'ColumnDrop'; object: DatabaseColumn }
| { type: 'ConstraintAdd'; object: DatabaseConstraint }
| { type: 'ConstraintRename'; object: { old: DatabaseConstraint; new: DatabaseConstraint } }
| { type: 'ConstraintDrop'; object: DatabaseConstraint }
| { type: 'IndexCreate'; object: DatabaseIndex }
| { type: 'IndexRename'; object: { old: DatabaseIndex; new: DatabaseIndex } }
| { type: 'IndexDrop'; object: DatabaseIndex }
| { type: 'TriggerCreate'; object: DatabaseTrigger }
| { type: 'TriggerDrop'; object: DatabaseTrigger }
| { type: 'ParameterSet'; object: DatabaseParameter }
| { type: 'ParameterReset'; object: DatabaseParameter }
| { type: 'EnumCreate'; object: DatabaseEnum }
| { type: 'EnumDrop'; object: DatabaseEnum }
| { type: 'OverrideCreate'; object: DatabaseOverride }
| { type: 'OverrideUpdate'; object: DatabaseOverride }
| { type: 'OverrideDrop'; object: DatabaseOverride };
export type SchemaDiff = { reason: string } & SchemaItem;
export type CompareFunction<T> = (source: T, target: T) => SchemaDiff[];
export type Comparer<T> = {
@@ -0,0 +1,29 @@
import { ForeignKeyColumn, Index, PrimaryColumn, Table } from 'src';
@Table()
export class Table1 {
@PrimaryColumn({ type: 'uuid' })
id!: string;
}
@Table()
export class Table2 {
@PrimaryColumn({ type: 'uuid' })
id!: string;
}
@Table()
@Index({ columns: ['table1Id', 'table2Id'] })
@Index({ columns: ['table2Id', 'table1Id'] })
export class Table3 {
@PrimaryColumn({ type: 'uuid' })
id!: string;
@ForeignKeyColumn(() => Table1, { index: false })
table1Id!: string;
@ForeignKeyColumn(() => Table2, { index: false })
table2Id!: string;
}
export const description = 'should create an index using two foreign key constraints';
@@ -0,0 +1,21 @@
import { ForeignKeyColumn, PrimaryColumn, Table } from 'src';
@Table('stack')
export class StackTable {
@PrimaryColumn({ type: 'uuid' })
id!: string;
@ForeignKeyColumn(() => AssetTable)
primaryAssetId!: string;
}
@Table('asset')
export class AssetTable {
@PrimaryColumn({ type: 'uuid' })
id!: string;
@ForeignKeyColumn(() => StackTable)
stackId!: string;
}
export const description = 'should work with inter-dependent tables';
@@ -0,0 +1,40 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`schemaFromDatabase > table-create.sql 1`] = `
{
"databaseName": "postgres",
"enums": [],
"extensions": [],
"functions": [],
"overrides": [],
"parameters": [],
"schemaName": "public",
"tables": [
{
"columns": [
{
"default": "0",
"isArray": false,
"length": undefined,
"name": "column1",
"nullable": false,
"numericPrecision": 32,
"numericScale": 0,
"primary": false,
"synchronize": true,
"tableName": "table1",
"type": "integer",
},
],
"constraints": [],
"indexes": [],
"name": "table1",
"synchronize": true,
"triggers": [],
},
],
"warnings": [
"[Overrides] Error reading override table: PostgresError: relation "migration_overrides" does not exist",
],
}
`;
@@ -0,0 +1,40 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`schemaFromDatabase > table-create.sql 1`] = `
{
"databaseName": "postgres",
"enums": [],
"extensions": [],
"functions": [],
"overrides": [],
"parameters": [],
"schemaName": "public",
"tables": [
{
"columns": [
{
"default": "0",
"isArray": false,
"length": undefined,
"name": "column1",
"nullable": false,
"numericPrecision": 32,
"numericScale": 0,
"primary": false,
"synchronize": true,
"tableName": "table1",
"type": "integer",
},
],
"constraints": [],
"indexes": [],
"name": "table1",
"synchronize": true,
"triggers": [],
},
],
"warnings": [
"[Overrides] Error reading override table: PostgresError: relation "migration_overrides" does not exist",
],
}
`;
@@ -0,0 +1,645 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`fixtures > check-constraint-default-name.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_3985fc33_chk" CHECK (1=1)
);"
`;
exports[`fixtures > check-constraint-default-name.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > check-constraint-override-name.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "CHK_test" CHECK (1=1)
);"
`;
exports[`fixtures > check-constraint-override-name.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-create-date.stub.ts 1`] = `
"CREATE TABLE "table1" (
"createdAt" timestamp with time zone NOT NULL DEFAULT now()
);"
`;
exports[`fixtures > column-create-date.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-array.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column0" character varying NOT NULL DEFAULT '{}',
"column1" character varying NOT NULL DEFAULT '{a, "b\\"", "", " hello world "}',
"column2" character varying NOT NULL DEFAULT '{1, 2, 3}',
"column3" character varying NOT NULL DEFAULT '{NULL}'
);"
`;
exports[`fixtures > column-default-array.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-boolean.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" boolean NOT NULL DEFAULT true
);"
`;
exports[`fixtures > column-default-boolean.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-date.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL DEFAULT '2023-01-01T00:00:00.000Z'
);"
`;
exports[`fixtures > column-default-date.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-function.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL DEFAULT now()
);"
`;
exports[`fixtures > column-default-function.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-null.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying
);"
`;
exports[`fixtures > column-default-null.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-number.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" integer NOT NULL DEFAULT 0
);"
`;
exports[`fixtures > column-default-number.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-default-string.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL DEFAULT 'foo'
);"
`;
exports[`fixtures > column-default-string.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-delete-date.stub.ts 1`] = `
"CREATE TABLE "table1" (
"deletedAt" timestamp with time zone
);"
`;
exports[`fixtures > column-delete-date.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-enum-type.stub.ts 1`] = `
"CREATE TYPE "test_enum" AS ENUM ('foo','bar');
CREATE TABLE "table1" (
"column1" test_enum NOT NULL
);"
`;
exports[`fixtures > column-enum-type.stub.ts 2`] = `
"DROP TABLE "table1";
DROP TYPE "test_enum";"
`;
exports[`fixtures > column-generated-identity.ts 1`] = `
"CREATE TABLE "table1" (
"column1" integer NOT NULL GENERATED ALWAYS AS IDENTITY,
CONSTRAINT "table1_pkey" PRIMARY KEY ("column1")
);"
`;
exports[`fixtures > column-generated-identity.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-generated-uuid.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" uuid NOT NULL DEFAULT uuid_generate_v4(),
CONSTRAINT "table1_pkey" PRIMARY KEY ("column1")
);"
`;
exports[`fixtures > column-generated-uuid.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-index-name.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL
);
CREATE INDEX "IDX_test" ON "table1" ("column1");"
`;
exports[`fixtures > column-index-name.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-index-name-default.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL
);
CREATE INDEX "table1_column1_idx" ON "table1" ("column1");"
`;
exports[`fixtures > column-index-name-default.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-inferred-nullable.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying
);"
`;
exports[`fixtures > column-inferred-nullable.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying NOT NULL
);"
`;
exports[`fixtures > column-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-name-override.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column-1" character varying NOT NULL
);"
`;
exports[`fixtures > column-name-override.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-name-string.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column-1" character varying NOT NULL
);"
`;
exports[`fixtures > column-name-string.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-nullable.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying
);"
`;
exports[`fixtures > column-nullable.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-string-length.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying(2) NOT NULL
);"
`;
exports[`fixtures > column-string-length.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-unique-constraint-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_id_uq" UNIQUE ("id")
);"
`;
exports[`fixtures > column-unique-constraint-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-unique-constraint-name-override.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "UQ_test" UNIQUE ("id")
);"
`;
exports[`fixtures > column-unique-constraint-name-override.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > column-update-date.stub.ts 1`] = `
"CREATE TABLE "table1" (
"updatedAt" timestamp with time zone NOT NULL DEFAULT now()
);"
`;
exports[`fixtures > column-update-date.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > foreign-key-constraint.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table2" (
"parentId" uuid NOT NULL,
CONSTRAINT "table2_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "table1" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE INDEX "table2_parentId_idx" ON "table2" ("parentId");"
`;
exports[`fixtures > foreign-key-constraint.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-constraint-column-order.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id1" uuid NOT NULL,
"id2" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id1", "id2")
);
CREATE TABLE "table2" (
"parentId1" uuid NOT NULL,
"parentId2" uuid NOT NULL,
CONSTRAINT "table2_parentId1_parentId2_fkey" FOREIGN KEY ("parentId1", "parentId2") REFERENCES "table1" ("id2", "id1") ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE INDEX "table2_parentId1_parentId2_idx" ON "table2" ("parentId1", "parentId2");"
`;
exports[`fixtures > foreign-key-constraint-column-order.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-constraint-multiple-columns.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id1" uuid NOT NULL,
"id2" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id1", "id2")
);
CREATE TABLE "table2" (
"parentId1" uuid NOT NULL,
"parentId2" uuid NOT NULL,
CONSTRAINT "table2_parentId1_parentId2_fkey" FOREIGN KEY ("parentId1", "parentId2") REFERENCES "table1" ("id1", "id2") ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE INDEX "table2_parentId1_parentId2_idx" ON "table2" ("parentId1", "parentId2");"
`;
exports[`fixtures > foreign-key-constraint-multiple-columns.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-constraint-multiple-columns-index.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table2" (
"id" uuid NOT NULL,
CONSTRAINT "table2_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table3" (
"id" uuid NOT NULL,
"table1Id" uuid NOT NULL,
"table2Id" uuid NOT NULL,
CONSTRAINT "table3_table1Id_fkey" FOREIGN KEY ("table1Id") REFERENCES "table1" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "table3_table2Id_fkey" FOREIGN KEY ("table2Id") REFERENCES "table2" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "table3_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "table3_table2Id_table1Id_idx" ON "table3" ("table2Id", "table1Id");
CREATE INDEX "table3_table1Id_table2Id_idx" ON "table3" ("table1Id", "table2Id");"
`;
exports[`fixtures > foreign-key-constraint-multiple-columns-index.stub.ts 2`] = `
"DROP TABLE "table3";
DROP TABLE "table1";
DROP TABLE "table2";"
`;
exports[`fixtures > foreign-key-constraint-no-index.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table2" (
"parentId" uuid NOT NULL,
CONSTRAINT "table2_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "table1" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);"
`;
exports[`fixtures > foreign-key-constraint-no-index.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-constraint-no-primary.stub.ts 1`] = `
"CREATE TABLE "table1" (
"foo" character varying NOT NULL,
CONSTRAINT "table1_foo_uq" UNIQUE ("foo")
);
CREATE TABLE "table2" (
"bar" character varying NOT NULL,
CONSTRAINT "table2_bar_fkey" FOREIGN KEY ("bar") REFERENCES "table1" ("foo") ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE INDEX "table2_bar_idx" ON "table2" ("bar");"
`;
exports[`fixtures > foreign-key-constraint-no-primary.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-inferred-type.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table2" (
"parentId" uuid NOT NULL,
CONSTRAINT "table2_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "table1" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE INDEX "table2_parentId_idx" ON "table2" ("parentId");"
`;
exports[`fixtures > foreign-key-inferred-type.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > foreign-key-with-unique-constraint.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "table2" (
"parentId" uuid NOT NULL,
CONSTRAINT "table2_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "table1" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "table2_parentId_uq" UNIQUE ("parentId")
);
CREATE INDEX "table2_parentId_idx" ON "table2" ("parentId");"
`;
exports[`fixtures > foreign-key-with-unique-constraint.stub.ts 2`] = `
"DROP TABLE "table2";
DROP TABLE "table1";"
`;
exports[`fixtures > index-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL
);
CREATE INDEX "table1_id_idx" ON "table1" ("id");"
`;
exports[`fixtures > index-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > index-name-override.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL
);
CREATE INDEX "IDX_test" ON "table1" ("id");"
`;
exports[`fixtures > index-name-override.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > index-with-expression.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" timestamp with time zone
);
CREATE INDEX "table1_da39a3ee_idx" ON "table1" ((("column1" at time zone 'UTC')::date));
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_table1_da39a3ee_idx', $json\${"type":"index","name":"table1_da39a3ee_idx","sql":"CREATE INDEX \\"table1_da39a3ee_idx\\" ON \\"table1\\" (((\\"column1\\" at time zone 'UTC')::date));"}$json$::jsonb);"
`;
exports[`fixtures > index-with-expression.stub.ts 2`] = `
"DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'index_table1_da39a3ee_idx';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > index-with-where.stub.ts 1`] = `
"CREATE TABLE "table1" (
"column1" character varying
);
CREATE INDEX "table1_column1_idx" ON "table1" ("column1") WHERE ("column1" IS NOT NULL);
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_table1_column1_idx', $json\${"type":"index","name":"table1_column1_idx","sql":"CREATE INDEX \\"table1_column1_idx\\" ON \\"table1\\" (\\"column1\\") WHERE (\\"column1\\" IS NOT NULL);"}$json$::jsonb);"
`;
exports[`fixtures > index-with-where.stub.ts 2`] = `
"DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'index_table1_column1_idx';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > primary-key-constraint-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_pkey" PRIMARY KEY ("id")
);"
`;
exports[`fixtures > primary-key-constraint-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > primary-key-constraint-name-override.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "PK_test" PRIMARY KEY ("id")
);"
`;
exports[`fixtures > primary-key-constraint-name-override.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > table-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
);"
`;
exports[`fixtures > table-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > table-name-override.stub.ts 1`] = `
"CREATE TABLE "table-1" (
);"
`;
exports[`fixtures > table-name-override.stub.ts 2`] = `"DROP TABLE "table-1";"`;
exports[`fixtures > table-name-string-option.stub.ts 1`] = `
"CREATE TABLE "table-1" (
);"
`;
exports[`fixtures > table-name-string-option.stub.ts 2`] = `"DROP TABLE "table-1";"`;
exports[`fixtures > table-recursive-dependency.stub.ts 1`] = `
"CREATE TABLE "stack" (
"id" uuid NOT NULL,
"primaryAssetId" uuid NOT NULL,
CONSTRAINT "stack_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "stack_primaryAssetId_idx" ON "stack" ("primaryAssetId");
CREATE TABLE "asset" (
"id" uuid NOT NULL,
"stackId" uuid NOT NULL,
CONSTRAINT "asset_stackId_fkey" FOREIGN KEY ("stackId") REFERENCES "stack" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "asset_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "stack" ADD CONSTRAINT "stack_primaryAssetId_fkey" FOREIGN KEY ("primaryAssetId") REFERENCES "asset" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
CREATE INDEX "asset_stackId_idx" ON "asset" ("stackId");"
`;
exports[`fixtures > table-recursive-dependency.stub.ts 2`] = `
"ALTER TABLE "stack" DROP CONSTRAINT "stack_primaryAssetId_fkey";
DROP TABLE "asset";
DROP TABLE "stack";"
`;
exports[`fixtures > trigger-after-delete.stub.ts 1`] = `
"CREATE OR REPLACE FUNCTION test_fn()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
SELECT 1;
RETURN NULL;
END
$$;
CREATE TABLE "table1" (
);
CREATE OR REPLACE TRIGGER "my_trigger"
AFTER DELETE ON "table1"
FOR EACH ROW
EXECUTE FUNCTION test_fn();
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_test_fn', $json\${"type":"function","name":"test_fn","sql":"CREATE OR REPLACE FUNCTION test_fn()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n SELECT 1;\\n RETURN NULL;\\n END\\n $$;"}$json$::jsonb);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_my_trigger', $json\${"type":"trigger","name":"my_trigger","sql":"CREATE OR REPLACE TRIGGER \\"my_trigger\\"\\n AFTER DELETE ON \\"table1\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION test_fn();"}$json$::jsonb);"
`;
exports[`fixtures > trigger-after-delete.stub.ts 2`] = `
"DROP TRIGGER "my_trigger" ON "table1";
DROP FUNCTION test_fn;
DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'function_test_fn';
DELETE FROM "migration_overrides" WHERE "name" = 'trigger_my_trigger';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > trigger-before-update.stub.ts 1`] = `
"CREATE OR REPLACE FUNCTION test_fn()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
SELECT 1;
RETURN NULL;
END
$$;
CREATE TABLE "table1" (
);
CREATE OR REPLACE TRIGGER "my_trigger"
BEFORE UPDATE ON "table1"
FOR EACH ROW
EXECUTE FUNCTION test_fn();
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_test_fn', $json\${"type":"function","name":"test_fn","sql":"CREATE OR REPLACE FUNCTION test_fn()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n SELECT 1;\\n RETURN NULL;\\n END\\n $$;"}$json$::jsonb);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_my_trigger', $json\${"type":"trigger","name":"my_trigger","sql":"CREATE OR REPLACE TRIGGER \\"my_trigger\\"\\n BEFORE UPDATE ON \\"table1\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION test_fn();"}$json$::jsonb);"
`;
exports[`fixtures > trigger-before-update.stub.ts 2`] = `
"DROP TRIGGER "my_trigger" ON "table1";
DROP FUNCTION test_fn;
DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'function_test_fn';
DELETE FROM "migration_overrides" WHERE "name" = 'trigger_my_trigger';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > trigger-name-default.stub.ts 1`] = `
"CREATE OR REPLACE FUNCTION test_fn()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
SELECT 1;
RETURN NULL;
END
$$;
CREATE TABLE "table1" (
);
CREATE OR REPLACE TRIGGER "table1_test_fn"
BEFORE INSERT ON "table1"
FOR EACH ROW
EXECUTE FUNCTION test_fn();
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_test_fn', $json\${"type":"function","name":"test_fn","sql":"CREATE OR REPLACE FUNCTION test_fn()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n SELECT 1;\\n RETURN NULL;\\n END\\n $$;"}$json$::jsonb);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_table1_test_fn', $json\${"type":"trigger","name":"table1_test_fn","sql":"CREATE OR REPLACE TRIGGER \\"table1_test_fn\\"\\n BEFORE INSERT ON \\"table1\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION test_fn();"}$json$::jsonb);"
`;
exports[`fixtures > trigger-name-default.stub.ts 2`] = `
"DROP TRIGGER "table1_test_fn" ON "table1";
DROP FUNCTION test_fn;
DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'function_test_fn';
DELETE FROM "migration_overrides" WHERE "name" = 'trigger_table1_test_fn';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > trigger-name-override.stub.ts 1`] = `
"CREATE OR REPLACE FUNCTION test_fn()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
SELECT 1;
RETURN NULL;
END
$$;
CREATE TABLE "table1" (
);
CREATE OR REPLACE TRIGGER "trigger1"
BEFORE INSERT ON "table1"
FOR EACH ROW
EXECUTE FUNCTION test_fn();
CREATE TABLE "migration_overrides" (
"name" character varying NOT NULL,
"value" jsonb NOT NULL,
CONSTRAINT "migration_overrides_pkey" PRIMARY KEY ("name")
);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_test_fn', $json\${"type":"function","name":"test_fn","sql":"CREATE OR REPLACE FUNCTION test_fn()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n SELECT 1;\\n RETURN NULL;\\n END\\n $$;"}$json$::jsonb);
INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_trigger1', $json\${"type":"trigger","name":"trigger1","sql":"CREATE OR REPLACE TRIGGER \\"trigger1\\"\\n BEFORE INSERT ON \\"table1\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION test_fn();"}$json$::jsonb);"
`;
exports[`fixtures > trigger-name-override.stub.ts 2`] = `
"DROP TRIGGER "trigger1" ON "table1";
DROP FUNCTION test_fn;
DROP TABLE "table1";
DELETE FROM "migration_overrides" WHERE "name" = 'function_test_fn';
DELETE FROM "migration_overrides" WHERE "name" = 'trigger_trigger1';
DROP TABLE "migration_overrides";"
`;
exports[`fixtures > unique-constraint-name-default.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "table1_id_uq" UNIQUE ("id")
);"
`;
exports[`fixtures > unique-constraint-name-default.stub.ts 2`] = `"DROP TABLE "table1";"`;
exports[`fixtures > unique-constraint-name-override.stub.ts 1`] = `
"CREATE TABLE "table1" (
"id" uuid NOT NULL,
CONSTRAINT "UQ_test" UNIQUE ("id")
);"
`;
exports[`fixtures > unique-constraint-name-override.stub.ts 2`] = `"DROP TABLE "table1";"`;
@@ -0,0 +1,121 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`schemaFromDatabase > index-create.sql 1`] = `
{
"databaseName": "postgres",
"enums": [],
"extensions": [],
"functions": [],
"overrides": [],
"parameters": [],
"schemaName": "public",
"tables": [
{
"columns": [
{
"default": undefined,
"isArray": false,
"length": undefined,
"name": "column1",
"nullable": false,
"numericPrecision": undefined,
"numericScale": undefined,
"primary": false,
"synchronize": true,
"tableName": "table1",
"type": "uuid",
},
{
"default": undefined,
"isArray": false,
"length": undefined,
"name": "column2",
"nullable": false,
"numericPrecision": undefined,
"numericScale": undefined,
"primary": false,
"synchronize": true,
"tableName": "table1",
"type": "uuid",
},
],
"constraints": [],
"indexes": [
{
"columnNames": [
"column1",
"column2",
],
"expression": undefined,
"name": "index1",
"override": undefined,
"synchronize": true,
"tableName": "table1",
"unique": false,
"using": "btree",
"where": undefined,
},
{
"columnNames": [
"column2",
"column1",
],
"expression": undefined,
"name": "index2",
"override": undefined,
"synchronize": true,
"tableName": "table1",
"unique": false,
"using": "btree",
"where": undefined,
},
],
"name": "table1",
"synchronize": true,
"triggers": [],
},
],
"warnings": [
"[Overrides] Error reading override table: PostgresError: relation "migration_overrides" does not exist",
],
}
`;
exports[`schemaFromDatabase > table-create.sql 1`] = `
{
"databaseName": "postgres",
"enums": [],
"extensions": [],
"functions": [],
"overrides": [],
"parameters": [],
"schemaName": "public",
"tables": [
{
"columns": [
{
"default": "0",
"isArray": false,
"length": undefined,
"name": "column1",
"nullable": false,
"numericPrecision": 32,
"numericScale": 0,
"primary": false,
"synchronize": true,
"tableName": "table1",
"type": "integer",
},
],
"constraints": [],
"indexes": [],
"name": "table1",
"synchronize": true,
"triggers": [],
},
],
"warnings": [
"[Overrides] Error reading override table: PostgresError: relation "migration_overrides" does not exist",
],
}
`;
@@ -0,0 +1,8 @@
CREATE TABLE "table1" (
"column1" uuid NOT NULL,
"column2" uuid NOT NULL
);
CREATE INDEX "index1" ON "table1" ("column1", "column2");
CREATE INDEX "index2" ON "table1" ("column2", "column1");
@@ -0,0 +1 @@
CREATE TABLE "table1" ("column1" integer NOT NULL DEFAULT 0);
@@ -0,0 +1,62 @@
import { schemaFromCode } from 'src/schema-from-code';
import { TestDatabase } from 'test/medium/test-database';
import { getDirectoryFiles, importFixture } from 'test/utils';
import { afterEach, beforeEach, describe, expect, it, onTestFailed } from 'vitest';
const DEBUG_NAME = '';
const fixtures = getDirectoryFiles('test/fixtures').filter(([name]) => !DEBUG_NAME || name === DEBUG_NAME);
describe('fixtures', () => {
let db: TestDatabase;
beforeEach(async () => {
schemaFromCode({ reset: true });
db = await TestDatabase.create();
});
afterEach(async () => {
await db.destroy();
});
it('should work', () => {
expect(1).toEqual(1);
});
it.each(fixtures)('%s', async (name, filename) => {
const { options } = await importFixture(filename);
const diff1 = await db.diff(options);
onTestFailed(() => {
const up = db.debug(diff1.up.asSql({ outputTarget: 'sql' }).join('\n'));
const down = db.debug(diff1.down.asSql({ outputTarget: 'sql' }).join('\n'));
// (filename, diff1.up, diff1.down)
console.log(
`DEBUG: ${filename}
# up sql
${up}
# down sql
${down}
`,
);
});
// apply the migration
await expect(db.query(diff1.up)).resolves.toMatchSnapshot();
// verify changes
const diff2 = await db.diff(options);
expect(diff2.up.items).toEqual([]);
expect(diff2.down.items).toEqual([]);
// apply the down migration
await expect(db.query(diff1.down)).resolves.toMatchSnapshot();
// verify changes
const diff3 = await db.diff(options);
expect(diff3.up.items).toEqual(diff1.up.items);
expect(diff3.down.items).toEqual(diff1.down.items);
});
});
@@ -0,0 +1,30 @@
import { sql } from 'kysely';
import { readFile } from 'node:fs/promises';
import { TestDatabase } from 'test/medium/test-database';
import { getDirectoryFiles } from 'test/utils';
import { afterEach, beforeEach, describe, expect, it, onTestFailed } from 'vitest';
const queries = getDirectoryFiles('test/medium/queries');
describe('schemaFromDatabase', () => {
let db: TestDatabase;
beforeEach(async () => {
db = await TestDatabase.create();
});
afterEach(async () => {
await db.destroy();
});
it.each(queries)('%s', async (name, filename) => {
const bytes = await readFile(filename);
const query = bytes.toString();
onTestFailed(() => console.log(`DEBUG: ${filename}\n${db.debug(query)}`));
await sql.raw(query).execute(db.kysely);
const schema = await db.schemaFromDatabase();
expect({ ...schema, databaseName: 'postgres', extensions: [] }).toMatchSnapshot();
});
});
@@ -0,0 +1,79 @@
import { sql } from 'kysely';
import { schemaDiff } from 'src/schema-diff';
import { schemaFromCode } from 'src/schema-from-code';
import { schemaFromDatabase } from 'src/schema-from-database';
import {
DatabaseClient,
DatabaseConnectionParams,
PostgresDB,
SchemaDiffResult,
SchemaFromCodeOptions,
} from 'src/types';
import { getKyselyDB } from 'test/utils';
import { assert, expect } from 'vitest';
export class TestDatabase {
private constructor(
public readonly name: string,
public kysely: DatabaseClient,
public connection: DatabaseConnectionParams,
) {}
static async create() {
const { name, kysely, connection } = await getKyselyDB<PostgresDB>();
return new TestDatabase(name, kysely, connection);
}
async destroy() {
await this.kysely.destroy();
}
async diff(options: SchemaFromCodeOptions) {
const source = schemaFromCode({
...options,
outputTarget: 'sql',
overrides: true,
uuidFunction: 'uuid_generate_v4()',
});
expect(source.warnings).toEqual([]);
const target = await this.schemaFromDatabase();
const up = schemaDiff(source, target, {
tables: { ignoreExtra: true },
functions: { ignoreExtra: false },
parameters: { ignoreExtra: true },
extensions: { ignoreExtra: true },
});
const down = schemaDiff(target, source, {
tables: { ignoreExtra: false, ignoreMissing: true },
functions: { ignoreExtra: false },
extensions: { ignoreMissing: true },
parameters: { ignoreMissing: true },
});
return { up, down };
}
async query({ asSql }: SchemaDiffResult) {
const query = asSql({ outputTarget: 'sql' }).join('\n');
try {
const results = await sql.raw(query).execute(this.kysely);
expect(results).toBeDefined();
} catch (error) {
assert.fail(`${this.debug('Unable to run SQL: \n' + query)}\nError: ${error}`);
}
return query;
}
debug(message: string) {
return '\n> ' + message.split('\n').join('\n> ');
}
schemaFromDatabase() {
return schemaFromDatabase({ connection: this.connection });
}
}
-143
View File
@@ -1,143 +0,0 @@
import { Kysely, sql } from 'kysely';
import { schemaDiff } from 'src/schema-diff';
import { schemaFromCode } from 'src/schema-from-code';
import { schemaFromDatabase } from 'src/schema-from-database';
import { DatabaseConnectionParams, SchemaDiffResult, SchemaFromCodeOptions } from 'src/types';
import { getKyselyDB, importFixture, loadFixtures } from 'test/utils';
import { afterEach, beforeEach, describe, expect, it, onTestFailed } from 'vitest';
const DEBUG_FILENAME = '';
const currentBugs = new Set([
'foreign-key-constraint-column-order.stub.ts',
'foreign-key-constraint-multiple-columns.stub.ts',
'foreign-key-constraint-no-index.stub.ts,',
'foreign-key-constraint-no-primary.stub.ts',
'foreign-key-constraint.stub.ts',
'foreign-key-inferred-type.stub.ts',
'foreign-key-with-unique-constraint.stub.ts',
'foreign-key-constraint-no-index.stub.ts',
]);
const fixtures = loadFixtures('test/fixtures').filter(([name]) => {
if (currentBugs.has(name)) {
return false;
}
if (DEBUG_FILENAME && DEBUG_FILENAME === name) {
return false;
}
return true;
});
class TestDatabase {
private constructor(
public kysely: Kysely<unknown>,
public connection: DatabaseConnectionParams,
) {}
static async create() {
const { kysely, connection } = await getKyselyDB();
return new TestDatabase(kysely, connection);
}
async destroy() {
await this.kysely.destroy();
}
async diff(options: SchemaFromCodeOptions) {
const source = schemaFromCode({
...options,
outputTarget: 'sql',
overrides: true,
uuidFunction: 'uuid_generate_v4()',
});
expect(source.warnings).toEqual([]);
const target = await this.#schemaFromDatabase();
const up = schemaDiff(source, target, {
tables: { ignoreExtra: true },
functions: { ignoreExtra: false },
parameters: { ignoreExtra: true },
});
const down = schemaDiff(target, source, {
tables: { ignoreExtra: false, ignoreMissing: true },
functions: { ignoreExtra: false },
extensions: { ignoreMissing: true },
parameters: { ignoreMissing: true },
});
return { up, down };
}
async query({ asSql }: SchemaDiffResult) {
const query = asSql({ outputTarget: 'sql' }).join('\n');
const results = await sql.raw(query).execute(this.kysely);
expect(results).toBeDefined();
}
#schemaFromDatabase() {
return schemaFromDatabase({ connection: this.connection });
}
}
describe('postgres', () => {
describe('test files', () => {
let db: TestDatabase;
beforeEach(async () => {
schemaFromCode({ reset: true });
db = await TestDatabase.create();
});
afterEach(async () => {
await db.destroy();
});
it('should work', () => {
expect(1).toEqual(1);
});
it.each(fixtures)('%s', async (name, filename) => {
const { options } = await importFixture(filename);
const diff1 = await db.diff(options);
onTestFailed(() => {
console.log(
`DEBUG: ${filename}
up sql
=========================================
${diff1.up.asSql({ outputTarget: 'sql' }).join('\n')}
=========================================
down sql
=========================================
${diff1.down.asSql({ outputTarget: 'sql' }).join('\n')}
=========================================
`
.split('\n')
.join('\n> '),
);
});
// apply the migration
await db.query(diff1.up);
// verify changes
const diff2 = await db.diff(options);
expect(diff2.up.items).toEqual([]);
expect(diff2.down.items).toEqual([]);
// apply the down migration
await db.query(diff1.down);
// verify changes
const diff3 = await db.diff(options);
expect(diff3.up.items).toEqual(diff1.up.items);
expect(diff3.down.items).toEqual(diff1.down.items);
});
});
});
+2 -1
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { createPostgres } from 'src/connections/postgres-connection';
import { DatabaseConnectionParams } from 'src/types';
export const loadFixtures = (directory: string) => {
export const getDirectoryFiles = (directory: string) => {
const fixtures: Array<[string, string]> = [];
for (const file of readdirSync(directory, { withFileTypes: true })) {
if (file.isDirectory()) {
@@ -43,6 +43,7 @@ export const getKyselyDB = async <T = unknown>(suffix?: string) => {
const connection = { connectionType: 'url', url: withDatabase(testUrl, dbName) } as DatabaseConnectionParams;
return {
name: dbName,
connection,
kysely: new Kysely<T>({ dialect: new PostgresJSDialect({ postgres: createPostgres({ connection }) }) }),
};
+8
View File
@@ -80,6 +80,9 @@ importers:
commander:
specifier: ^14.0.3
version: 14.0.3
graph-data-structure:
specifier: ^4.5.0
version: 4.5.0
kysely:
specifier: ^0.28.11
version: 0.28.16
@@ -1545,6 +1548,9 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
graph-data-structure@4.5.0:
resolution: {integrity: sha512-OCeIzpK9JnV5js4gtDJgwebRbcOsZpoN9CNIwEooHkV/FNol+OykWPOugSTXBH/QICEW2N6U+6L2d9DcK4YBcw==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@@ -3861,6 +3867,8 @@ snapshots:
graceful-fs@4.2.11: {}
graph-data-structure@4.5.0: {}
has-flag@4.0.0: {}
hasown@2.0.2: