ddl-manager
Version:
store postgres procedures and triggers in files
875 lines (759 loc) • 27.5 kB
text/typescript
import { Pool } from "pg";
import fs from "fs";
import {prepare, buildDDL} from "./fixture";
import { CacheAuditor, CacheScanner } from "../../../../lib/Auditor";
import { FileReader } from "../../../../lib/fs/FileReader";
import { Database } from "../../../../lib/database/schema/Database";
import { PostgresDriver } from "../../../../lib/database/PostgresDriver";
import { CacheColumnGraph } from "../../../../lib/Comparator/graph/CacheColumnGraph";
import { shouldBeBetween } from "./utils";
import { strict } from "assert";
import { DDLManager } from "../../../../lib/DDLManager";
const ROOT_TMP_PATH = __dirname + "/tmp";
describe("CacheAuditor: create/recreate logger for broken columns", () => {
let db: Pool;
beforeEach(async () => {
db = await prepare();
});
describe("audit simple broken cache", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/cache.sql", `
cache totals for companies (
select
1 as some_column
)
`);
await build();
await db.query(`
update companies set
some_column = 0
`);
});
it("need recreate changes columns", async() => {
await db.query(`
create table ddl_manager_audit_column_changes (
id bigserial primary key
);
`);
await audit();
await db.query(`
update companies set
some_column = 0
where id = 1;
`);
await expectedChanges("changed_new_row", {
id: 1,
some_column: 0
});
});
});
describe("log changes for agg cache", async () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/cache.sql", `
cache totals for companies (
select
string_agg( distinct orders.doc_number, ', ' ) as orders_numbers,
sum( orders.profit ) as orders_profit
from orders
where
orders.id_client = companies.id
)
`);
await build();
await db.query(`
update companies set
orders_numbers = 'wrong'
`);
await audit();
});
it("log callstack", async () => {
await db.query(`
create or replace function inner_func()
returns void as $body$
begin
-- some code
-- here
update orders set
doc_number = 'some new value';
end
$body$ language plpgsql;
create or replace function main_func()
returns void as $body$
begin
perform inner_func();
end
$body$ language plpgsql;
select main_func();
`);
await expectedChangesFor("public.orders", 1, "callstack", [
"inner_func:6",
"main_func:3"
]);
});
it("log entry query", async() => {
const sql = `/* test */update orders set doc_number = 'test!'/* test */`;
await db.query(sql);
await expectedChanges("entry_query", sql);
});
it("log update of cache column", async() => {
await db.query(`
update companies set
orders_numbers = 'wrong'
where id = 1
`);
await expectedChanges("changed_old_row", {
id: 1,
orders_numbers: "order-1, order-2"
});
await expectedChanges("changed_new_row", {
id: 1,
orders_numbers: "wrong"
});
});
describe("log insert into source table", () => {
beforeEach(async () => {
await db.query(`
insert into orders (id_client, doc_number)
values (2, 'order-4');
`);
});
it("log TG_OP", async () => {
await expectedChangesFor("public.orders", 4, "changes_type", "INSERT");
});
it("log source table", async () => {
await expectedChangesFor("public.orders", 4, "changed_table", "public.orders");
});
it("log source row id", async () => {
await expectedChangesFor("public.orders", 4, "changed_row_id", "4");
});
it("log source changes (need columns for cache only)", async () => {
await expectedChangesFor("public.orders", 4, "changed_old_row", null);
await expectedChangesFor("public.orders", 4, "changed_new_row", {
id: 4,
id_client: 2,
doc_number: "order-4",
profit: null
});
});
it("log update cache row", async () => {
await expectedChangesFor("public.companies", 2, "changed_new_row", {
orders_numbers: "order-3, order-4"
});
});
});
describe("log delete from source table", () => {
beforeEach(async () => {
await db.query(`
delete from orders
where id_client = 2;
`);
});
it("log TG_OP", async () => {
await expectedChangesFor("public.orders", 3, "changes_type", "DELETE");
});
it("log source table", async () => {
await expectedChangesFor("public.orders", 3, "changed_table", "public.orders");
});
it("log source row id", async () => {
await expectedChangesFor("public.orders", 3, "changed_row_id", "3");
});
it("log source changes (need columns for cache only)", async () => {
await expectedChangesFor("public.orders", 3, "changed_old_row", {
id: 3,
id_client: 2,
doc_number: "order-3",
profit: 300
});
await expectedChangesFor("public.orders", 3, "changed_new_row", null);
});
it("log update cache row", async () => {
await expectedChangesFor("public.companies", 2, "changed_new_row", {
orders_numbers: null
});
});
});
describe("log update of source column", () => {
beforeEach(async () => {
await db.query(`
update orders set
doc_number = 'update order 1'
where id = 1
`);
});
it("log TG_OP", async () => {
await expectedChangesFor("public.orders", 1, "changes_type", "UPDATE");
});
it("log source table", async () => {
await expectedChangesFor("public.orders", 1, "changed_table", "public.orders");
});
it("log source row id", async () => {
await expectedChangesFor("public.orders", 1, "changed_row_id", "1");
});
it("log source changes (need columns for cache only)", async () => {
await expectedChangesFor("public.orders", 1, "changed_old_row", {
id: 1,
id_client: 1,
doc_number: "order-1",
profit: 100
});
await expectedChangesFor("public.orders", 1, "changed_new_row", {
id: 1,
id_client: 1,
doc_number: "update order 1",
profit: 100
});
});
it("log changes date", async () => {
const dateStart = new Date();
await db.query(`
update orders set
doc_number = 'order A'
where id = 1
`);
const dateEnd = new Date();
const lastChanges = await loadLastColumnChanges();
shouldBeBetween(lastChanges.changes_date, dateStart, dateEnd);
shouldBeBetween(lastChanges.transaction_date, dateStart, dateEnd);
});
it("log update cache row", async () => {
await expectedChangesFor("public.companies", 1, "changed_new_row", {
orders_numbers: "order-2, update order 1"
});
});
});
});
describe("log self row cache", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/cache.sql", `
cache totals for companies (
select
'#' || companies.id ||
coalesce(' ' || companies.name, '') as ref_name
)
`);
await build();
await db.query(`
update companies set
ref_name = 'wrong'
`);
await audit();
});
it("log update of cache row", async () => {
await db.query(`
update companies set
name = 'new name'
where id = 1;
`);
await expectedChanges("changed_old_row", {
id: 1,
name: "client",
ref_name: "#1 client"
});
await expectedChanges("changed_new_row", {
id: 1,
name: "new name",
ref_name: "#1 new name"
});
});
it("log insert of cache row", async () => {
await db.query(`
insert into companies (name)
values ('test');
`);
await expectedChanges("changes_type", "INSERT");
await expectedChanges("changed_new_row", {
id: 3,
name: "test",
ref_name: "#3 test"
});
});
it("log delete of cache row", async () => {
await db.query(`
delete from companies
where id = 1;
`);
await expectedChanges("changes_type", "DELETE");
await expectedChanges("changed_old_row", {
id: 1,
name: "client",
ref_name: "#1 client"
});
});
it("don't ignore update-ddl-cache operation (big migration)", async() => {
await db.query(`
alter table companies disable trigger all;
update companies set
ref_name = null;
alter table companies enable trigger all;
`);
await DDLManager.refreshCache({
db,
folder: ROOT_TMP_PATH,
throwError: true,
needLogs: false
});
const lastChanges = await loadLastColumnChanges();
strict.ok(lastChanges, "exists log");
});
});
describe("many broken columns per table", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/self.sql", `
cache self for companies (
select
companies.id - 1 as index,
coalesce(companies.name, '') as not_null_name
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/profit.sql", `
cache profit for companies (
select
sum( orders.profit ) as orders_profit
from orders
where
orders.id_client = companies.id
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/orders_numbers.sql", `
cache orders_numbers for companies (
select
string_agg( orders.doc_number, ', ' ) as orders_numbers
from orders
where
orders.id_client = companies.id
)
`);
await build();
await db.query(`
update companies set
index = -1,
orders_profit = 666
`);
await audit();
});
it("log update agg source table", async() => {
await db.query(`
update orders set
profit = 303
where id = 3
`);
await expectedChangesFor("public.orders", 3, "changed_new_row", {
profit: 303
});
});
it("log update self cache table", async() => {
await db.query(`
update companies set
name = 'test'
where id = 1
`);
await expectedChangesFor("public.companies", 1, "changed_new_row", {
name: "test"
});
});
});
describe("cache with array reference", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/payments.sql", `
cache payments for orders (
select
array_agg( distinct link.id_payment ) as payments_ids
from order_payment_link as link
where
link.id_order = orders.id
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/orders.sql", `
cache orders for payments (
select
string_agg( distinct orders.doc_number, ', ' ) as orders_numbers
from orders
where
orders.payments_ids && array[ payments.id ]
)
`);
await build();
await db.query(`
insert into payments default values;
insert into payments default values;
insert into order_payment_link (
id_order,
id_payment,
part_of_payment
) values
(1, 1, 100),
(2, 1, 100)
;
update payments set
orders_numbers = 'wrong'
`);
await audit();
});
});
describe("cache with mutable order by", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/totals.sql", `
cache totals for companies (
select
orders.doc_number as best_order_number
from orders
where
orders.id_client = companies.id
order by orders.profit desc
limit 1
)
`);
await build();
await db.query(`
update companies set
best_order_number = 'wrong'
`);
await audit();
});
it("log on update sort column", async () => {
await db.query(`
update orders set
profit = 350
where id = 1;
`);
await expectedChangesFor("public.orders", 1, "changed_old_row", {
id: 1,
id_client: 1,
doc_number: "order-1",
profit: 100
});
await expectedChangesFor("public.orders", 1, "changed_new_row", {
id: 1,
id_client: 1,
doc_number: "order-1",
profit: 350
});
});
});
describe("cache same table deps, but other rows", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/parent.sql", `
cache parent for companies as child_company (
select
parent_company.name as parent_company_name
from companies as parent_company
where
parent_company.id = child_company.id_parent
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/children.sql", `
cache children for companies as parent_company (
select
string_agg(child_company.name, ', ') as children_companies_names
from companies as child_company
where
child_company.id_parent = parent_company.id
)
`);
await build();
await db.query(`
update companies set
parent_company_name = 'wrong',
children_companies_names = 'wrong'
`);
await audit();
await db.query(`
update companies set
id_parent = 1
where id = 2;
`);
});
it("log cache values", async () => {
await expectedChangesFor("public.companies", 2, "changed_old_row", {
id: 2,
id_parent: null,
name: "partner",
parent_company_name: null,
children_companies_names: null
});
await expectedChangesFor("public.companies", 2, "changed_new_row", {
id: 2,
id_parent: 1,
name: "partner",
parent_company_name: "client",
children_companies_names: null
});
});
});
describe("recursion cache", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/lvl.sql", `
cache lvl for companies (
select
coalesce(
companies.parent_lvl + 1,
1
)::integer as lvl
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/parent.sql", `
cache parent for companies as child_company (
select
-- parent_lvl служебное поле необходимое для работы обычного lvl
parent_company.lvl as parent_lvl
from companies as parent_company
where
parent_company.id = child_company.id_parent
)
`);
await build();
await db.query(`
update companies set
lvl = -1
`);
await audit();
});
it("log on update reference column", async () => {
await db.query(`
update companies set
id_parent = 1
where id = 2;
`);
await expectedChanges("changed_old_row", {
id: 2,
id_parent: null,
parent_lvl: null,
lvl: 1
});
await expectedChanges("changed_new_row", {
id: 2,
id_parent: 1,
parent_lvl: 1,
lvl: 2
});
});
});
describe("cache dependent on custom before update trigger", () => {
beforeEach(async () => {
fs.writeFileSync(ROOT_TMP_PATH + "/totals.sql", `
cache totals for companies (
select
companies.name || companies.note as name_note
)
`);
fs.writeFileSync(ROOT_TMP_PATH + "/custom.sql", `
create or replace function set_note()
returns trigger as $body$
begin
new.note = new.id_parent::text;
return new;
end
$body$ language plpgsql;
create trigger a_test
before update of id_parent
on companies
for each row
execute procedure set_note();
`);
await build();
await db.query(`
update companies set
name_note = 'wrong'
`);
await audit();
});
it("log update", async () => {
await db.query(`
update companies set
id_parent = 1
where id = 1;
`);
await expectedChanges("changed_old_row", {
id: 1,
name: "client",
note: null,
name_note: null
});
await expectedChanges("changed_new_row", {
id: 1,
name: "client",
note: "1",
name_note: "client1"
});
});
});
describe("remove logger", () => {
beforeEach(async() => {
fs.writeFileSync(ROOT_TMP_PATH + "/cache.sql", `
cache totals for companies (
select
companies.name || 'test' as note
)
`);
await build();
await breakCache();
await audit();
});
it("remove logger if no more cache", async () => {
fs.unlinkSync(ROOT_TMP_PATH + "/cache.sql");
await audit();
await breakCache();
await noColumnLogs();
});
it("remove logger if long time no reports", async () => {
await db.query(`
update ddl_manager_audit_report set
scan_date = now() - interval '3 months 5 days'
`);
await audit();
await breakCache();
await noColumnLogs();
});
it("remove if no reports", async() => {
await audit();
await db.query(`
delete from ddl_manager_audit_report
`);
await audit();
await breakCache();
await noColumnLogs();
});
async function breakCache() {
await db.query(`
update companies set
note = 'wrong ' || now()::text
`);
}
});
it("log 110 fields", async() => {
let columns: string[] = [];
for (let i = 0; i < 110; i++) {
columns.push(`column_${i}`);
}
await db.query(`
alter table orders
${columns.map(column =>
`add column ${column} text`
).join(",")};
`);
fs.writeFileSync(`${ROOT_TMP_PATH}/totals.sql`, `
cache totals for companies (
select
${columns.map(column =>
`string_agg(orders.${column}, ', ') as ${column}`
).join(", ")}
from orders
where
orders.id_client = companies.id
)
`);
await build();
await db.query(`
update companies set
(${columns}) = (${columns.map(() => "'wrong'").join(", ")})
`);
await audit();
await db.query(`
update orders set
column_0 = 'test'
where id = 1;
`);
await expectedChanges("changed_new_row", {
id: 1,
column_0: "test"
});
});
async function build() {
await buildDDL(db);
}
async function audit() {
const fsState = FileReader.read([ROOT_TMP_PATH]);
const dbState = new Database();
const graph = CacheColumnGraph.build(
new Database().aggregators,
fsState.allCache()
);
const postgres = new PostgresDriver(db);
const scanner = new CacheScanner(
postgres,
dbState,
graph
);
const auditor = new CacheAuditor(
postgres,
dbState,
fsState,
graph,
scanner,
);
await auditor.audit();
}
async function expectedChanges(column: string, expected: any) {
const lastChanges = await loadLastColumnChanges();
strict.ok(lastChanges, "changes should be saved");
const actual = lastChanges[column];
if ( isObject(expected) ) {
strict.deepEqual(actual, {
...actual,
...expected
}, `lastChanges[${column}]`);
}
else {
strict.deepEqual(
actual, expected,
`lastChanges["${column}"]`
);
}
}
async function expectedChangesFor(table: string, rowId: number, column: string, expected: any) {
const lastChanges = await loadLastColumnChanges(table, rowId);
strict.ok(lastChanges, "changes should be saved");
const actual = lastChanges[column];
strict.deepEqual({
[column]: actual
}, {
[column]: (
isObject(expected) ?
{...actual, ...expected} :
expected
)
});
}
async function noColumnLogs() {
const lastChanges = await loadLastColumnChanges();
strict.equal(
lastChanges, undefined,
"expected no changes"
);
}
async function loadLastColumnChanges(table?: string, forRowId?: number) {
const conditions: string[] = [];
if ( table ) {
conditions.push(`changed_table = '${table}'`);
}
if ( forRowId ) {
conditions.push(`changed_row_id = ${+forRowId}`);
}
const where = conditions.length ?
`where ${conditions.join(" and ")}` : "";
const {rows} = await db.query(`
select
*,
to_char(
ddl_manager_audit_column_changes.transaction_date at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
) as transaction_date,
to_char(
ddl_manager_audit_column_changes.changes_date at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
) as changes_date
from ddl_manager_audit_column_changes
${where}
order by ddl_manager_audit_column_changes.id desc
limit 1
`);
return rows[0];
}
function isObject(value: any) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value)
)
}
})