UNPKG

ddl-manager

Version:

store postgres procedures and triggers in files

989 lines (877 loc) 28.9 kB
import assert from "assert"; import { Pool } from "pg"; import { getDBClient } from "./getDbClient"; import { MainMigrator } from "../../lib/Migrator/MainMigrator"; import { DatabaseFunction, IDatabaseFunctionParams } from "../../lib/database/schema/DatabaseFunction"; import { DatabaseTrigger, IDatabaseTriggerParams } from "../../lib/database/schema/DatabaseTrigger"; import { TableID } from "../../lib/database/schema/TableID"; import {expect, use} from "chai"; import chaiShallowDeepEqualPlugin from "chai-shallow-deep-equal"; import { PostgresDriver } from "../../lib/database/PostgresDriver"; import { Migration } from "../../lib/Migrator/Migration"; import { FileParser } from "../../lib/parser"; import { MainComparator } from "../../lib/Comparator/MainComparator"; import { FilesState } from "../../lib/fs/FilesState"; use(chaiShallowDeepEqualPlugin); describe("integration/MainMigrator", () => { let db!: Pool; beforeEach(async() => { db = await getDBClient(); await db.query(` drop schema public cascade; create schema public; `); }); afterEach(async() => { await db.end(); }); interface IMigrationParams { drop: { functions: IDatabaseFunctionParams[]; triggers: IDatabaseTriggerParams[]; }; create: { functions: IDatabaseFunctionParams[]; triggers: IDatabaseTriggerParams[]; }; } async function migrate(params: {migration: IMigrationParams}) { const migration = Migration.empty(); dropFunctions(migration, params.migration.drop.functions); dropTriggers(migration, params.migration.drop.triggers); createFunctions(migration, params.migration.create.functions); createTriggers(migration, params.migration.create.triggers); const postgres = new PostgresDriver(db); const database = await postgres.load(); await MainMigrator.migrate( postgres, database, migration ); } function dropFunctions(migration: Migration, functions: IDatabaseFunctionParams[]) { functions.map(funcJson => { const func = new DatabaseFunction(funcJson); migration.drop({functions: [func]}); }); } function dropTriggers(migration: Migration, triggers: IDatabaseTriggerParams[]) { triggers.map(triggerJson => { const trigger = new DatabaseTrigger(triggerJson); migration.drop({triggers: [trigger]}); }); } function createFunctions(migration: Migration, functions: IDatabaseFunctionParams[]) { functions.map(funcJson => { const func = new DatabaseFunction(funcJson); migration.create({functions: [func]}); }); } function createTriggers(migration: Migration, triggers: IDatabaseTriggerParams[]) { triggers.map(triggerJson => { const trigger = new DatabaseTrigger(triggerJson); migration.create({triggers: [trigger]}); }); } it("migrate simple function", async() => { const rnd = Math.round( 10000 * Math.random() ); await migrate({ migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_migrate_function", args: [], returns: {type: "bigint"}, body: `begin return ${ rnd }; end` } ], triggers: [] } } }); let result = await db.query("select test_migrate_function()"); const row = result && result.rows[0]; result = row.test_migrate_function; assert.equal(result, rnd); }); it("migrate function and trigger", async() => { await db.query(` create table ddl_manager_test ( name text, note text ); `); await migrate({ migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "some_action_on_diu_test", args: [], returns: {type: "trigger"}, body: `begin raise exception 'success'; end` } ], triggers: [ { table: new TableID( "public", "ddl_manager_test" ), after: true, insert: true, updateOf: ["name", "note"], delete: true, name: "some_action_on_diu_test_trigger", procedure: { schema: "public", name: "some_action_on_diu_test", args: [] } } ] } } }); // check trigger on table try { await db.query(` insert into ddl_manager_test default values `); assert.ok(false, "expected special error from trigger"); } catch(err) { assert.equal(err.message, "success"); } }); it("twice migrate function and trigger", async() => { await db.query(` create table ddl_manager_test ( name text, note text ); `); const migration: IMigrationParams = { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "some_action_on_diu_test", args: [], returns: {type: "trigger"}, body: `begin raise exception 'success'; end` } ], triggers: [ { table: new TableID( "public", "ddl_manager_test" ), after: true, insert: true, updateOf: ["name", "note"], delete: true, name: "some_action_on_diu_test_trigger", procedure: { schema: "public", name: "some_action_on_diu_test", args: [] } } ] } }; // do it twice without errors await migrate({migration: migration}); await migrate({migration: migration}); // check trigger on table try { await db.query(` insert into ddl_manager_test default values `); assert.ok(false, "expected special error from trigger"); } catch(err) { assert.equal(err.message, "success"); } }); it("no error on replace frozen function", async() => { await db.query(` create function test() returns integer as $$select 1$$ language sql; `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "sql", schema: "public", name: "test", args: [], returns: {type: "integer"}, body: "select 2" } ], triggers: [] } }}); let result = await db.query("select test()"); const row = result && result.rows[0]; result = row.test; assert.equal(result, 2); }); it("error on drop frozen function", async() => { await db.query(` create function test() returns integer as $$select 1$$ language sql; `); try { await migrate({migration: { drop: { functions: [ { language: "sql", schema: "public", name: "test", args: [], returns: {type: "integer"}, body: "select 2" } ], triggers: [] }, create: { functions: [], triggers: [] } }}); } catch(err) { assert.equal(err.message, "public.test()\ncannot drop frozen function public.test()"); } }); it("frozen function with another args", async() => { await db.query(` create function test(a integer) returns integer as $$select 1$$ language sql; `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "sql", schema: "public", name: "test", args: [ { name: "a", type: "integer" }, { name: "b", type: "integer" } ], returns: {type: "integer"}, body: "select 2" } ], triggers: [] } }}); let result = await db.query("select test(1, 2)"); const row = result && result.rows[0]; result = row.test; assert.equal(result, 2); }); it("frozen function with another arg type", async() => { await db.query(` create function test(a numeric) returns integer as $$select 1$$ language sql; `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "sql", schema: "public", name: "test", args: [ { name: "a", type: "bigint" } ], returns: {type: "integer"}, body: "select 2" } ], triggers: [] } }}); let result = await db.query("select test(1::bigint)"); const row = result && result.rows[0]; result = row.test; assert.equal(result, 2); }); it("error on replace frozen trigger", async() => { await db.query(` create table company ( id serial primary key ); create function test() returns trigger as $$ begin return new; end $$ language plpgsql; create trigger x after insert on company for each row execute procedure test() `); try { await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test2", args: [], returns: {type: "trigger"}, body: ` begin return new; end ` } ], triggers: [ { table: new TableID( "public", "company" ), name: "x", after: true, delete: true, procedure: { schema: "public", name: "test", args: [] } } ] } }}); } catch(err) { assert.equal(err.message, "x on public.company\ncannot replace frozen trigger x on public.company"); } }); it("error on drop frozen trigger", async() => { await db.query(` create table company ( id serial primary key ); create function test() returns trigger as $$ begin return new; end $$ language plpgsql; create trigger x after insert on company for each row execute procedure test() `); try { await migrate({migration: { drop: { functions: [ { language: "plpgsql", schema: "public", name: "test2", args: [], returns: {type: "trigger"}, body: ` begin return new; end ` } ], triggers: [ { table: new TableID( "public", "company" ), name: "x", after: true, delete: true, procedure: { schema: "public", name: "test", args: [] } } ] }, create: { functions: [], triggers: [] } }}); } catch(err) { assert.equal(err.message, "x on public.company\ncannot drop frozen trigger x on public.company"); } }); it("migrate function with returns table", async() => { await db.query(` create table some_table ( id serial primary key ); `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_func", args: [], returns: { type: "public.some_table" }, body: ` declare some_table_row some_table; begin select * from some_table into some_table_row; return some_table_row; end` } ], triggers: [] } }}); // expected execute without errors await db.query("select * from test_func()"); }); it("migrate function with returns setof table", async() => { await db.query(` create table some_table ( id serial primary key ); insert into some_table default values; insert into some_table default values; `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_func", args: [], returns: { setof: true, type: "public.some_table" }, body: ` begin return query select * from some_table; end` } ], triggers: [] } }}); // expected execute without errors const result = await db.query("select * from test_func()"); expect(result.rows).to.be.shallowDeepEqual([ {id: 1}, {id: 2} ]); }); it("migrate function with arg table", async() => { await db.query(` create table some_table ( id serial primary key ); `); await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_func", args: [ { name: "some_table", type: "public.some_table" } ], returns: {type: "void"}, body: ` begin end` } ], triggers: [] } }}); // expected execute without errors await db.query("select test_func(some_table) from some_table"); }); it("migrate function, arg without name", async() => { await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_func", args: [ { name: undefined, type: "text" } ], returns: {type: "void"}, body: ` begin end` } ], triggers: [] } }}); // expected execute without errors await db.query("select test_func('')"); }); it("migrate function, in/out arg", async() => { await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ { language: "plpgsql", schema: "public", name: "test_func", args: [ { in: true, name: "id", type: "integer" }, { out: true, name: "name", type: "text" } ], returns: {type: "text"}, body: ` begin name = 'nice' || id::text; end` } ], triggers: [] } }}); const result = await db.query("select test_func(1) as test"); expect(result.rows[0]).to.be.shallowDeepEqual({ test: "nice1" }); }); it("migrate function, arg default null", async() => { const func: IDatabaseFunctionParams = { language: "plpgsql", schema: "public", name: "test_func", args: [ { name: "id", type: "integer", default: "null" } ], returns: {type: "text"}, body: ` begin return 'nice' || coalesce(id, 2)::text; end` }; await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ func ], triggers: [] } }}); const result = await db.query("select test_func() as test"); expect(result.rows[0]).to.be.shallowDeepEqual({ test: "nice2" }); await migrate({migration: { drop: { functions: [ func ], triggers: [] }, create: { functions: [], triggers: [] } }}); // old function must be dropped try { await db.query("select test_func(1) as nice"); assert.ok(false, "expected error"); } catch(err) { assert.equal(err.message, "function test_func(integer) does not exist"); } try { await db.query("select test_func() as nice"); assert.ok(false, "expected error"); } catch(err) { assert.equal(err.message, "function test_func() does not exist"); } }); it("migrate two functions, same name, migration args", async() => { const func1: IDatabaseFunctionParams = { language: "plpgsql", schema: "public", name: "test_func", args: [ { name: "x", type: "integer", default: "null" } ], returns: {type: "integer"}, body: ` begin return 1; end` }; const func2: IDatabaseFunctionParams = { language: "plpgsql", schema: "public", name: "test_func", args: [ { name: "x", type: "boolean", default: "null" } ], returns: {type: "integer"}, body: ` begin return 2; end` }; await migrate({migration: { drop: { functions: [], triggers: [] }, create: { functions: [ func1, func2 ], triggers: [] } }}); let result; result = await db.query("select test_func(1) as test1"); expect(result.rows[0]).to.be.shallowDeepEqual({ test1: 1 }); result = await db.query("select test_func(true) as test2"); expect(result.rows[0]).to.be.shallowDeepEqual({ test2: 2 }); await migrate({migration: { drop: { functions: [ func1, func2 ], triggers: [] }, create: { functions: [], triggers: [] } }}); // old functions must be dropped try { await db.query("select test_func(1) as nice"); assert.ok(false, "expected error"); } catch(err) { assert.equal(err.message, "function test_func(integer) does not exist"); } try { await db.query("select test_func(true) as nice"); assert.ok(false, "expected error"); } catch(err) { assert.equal(err.message, "function test_func(boolean) does not exist"); } }); it("update cache helpers columns", async() => { await db.query(` create table companies ( id serial primary key ); create table orders ( id serial primary key, id_client integer, doc_number text, profit numeric, xxx numeric ); insert into companies default values; insert into orders (id_client, doc_number, profit, xxx) values (1, 'o1', 100, 10), (1, 'o2', 200, 20) `); const cache = FileParser.parseCache(` cache test for companies ( select string_agg( distinct orders.doc_number, ', ' ) as doc_numbers, sum( orders.profit ) * 2 + sum( orders.xxx ) as some_profit, max( orders.profit ) as max_profit from orders where orders.id_client = companies.id ) `); const postgres = new PostgresDriver(db); const database = await postgres.load(); const fs = new FilesState(); fs.addFile({ name: "test.sql", path: "test.sql", folder: "", content: { cache: [cache] } }); const migration = await MainComparator.compare( postgres, database, fs ); await MainMigrator.migrate( postgres, database, migration ); const {rows} = await db.query(` select id, doc_numbers, some_profit, max_profit from companies `); assert.deepStrictEqual(rows[0], { id: 1, doc_numbers: "o1, o2", some_profit: "630", max_profit: "200" }); }); });