mirror of
https://github.com/immich-app/packages.git
synced 2026-09-30 13:33:01 +08:00
@@ -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: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 } });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
+29
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 }) }) }),
|
||||
};
|
||||
|
||||
Generated
+8
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user