ddl-manager
Version:
store postgres procedures and triggers in files
244 lines (206 loc) • 8.16 kB
text/typescript
import { CacheColumn } from "../Comparator/graph/CacheColumn";
import { CacheColumnGraph } from "../Comparator/graph/CacheColumnGraph";
import { Cache, Expression, SelectColumn } from "../ast";
import { IDatabaseDriver } from "../database/interface";
import { Database } from "../database/schema/Database";
export interface IFindBrokenColumnsParams {
timeout?: number;
concreteTables?: string | string[];
onStartScanColumn?: (column: string) => void | Promise<void>;
onScanColumn?: (result: IColumnScanResult) => void | Promise<void>;
onScanError?: (result: IColumnScanError) => void | Promise<void>;
onFinish?: () => void | Promise<void>;
}
export interface IColumnScanResult {
column: string;
hasWrongValues: boolean;
wrongExample?: {
/** wrong cache value on cache row */
actual: any;
/** correct cache value on cache row */
expected: any;
/** schema.table of cache row */
table: string;
/** how to fetch correct data (cache select) */
selectExpectedForThatRow: string;
/** full state of cache row */
cacheRow: Record<string, any>;
/** full state of sources rows, which used for calculate cache */
sourceRows?: Record<string, any>[];
};
time: TimeRange;
}
export interface IColumnScanError {
column: string;
error: Error;
time: TimeRange;
}
export interface TimeRange {
start: Date;
end: Date;
duration: number;
}
export class CacheScanner {
constructor(
private driver: IDatabaseDriver,
private database: Database,
private graph: CacheColumnGraph
) {}
async scan(params: IFindBrokenColumnsParams = {}) {
let allCacheColumns = this.graph.findCacheColumnsForTablesOrColumns(params.concreteTables);
const brokenColumns: CacheColumn[] = [];
for (const column of allCacheColumns) {
const hasWrongValues = await this.tryScanColumnOnWrongValues(params, column);
if ( hasWrongValues ) {
brokenColumns.push(column);
}
}
if ( params.onFinish ) {
await params.onFinish();
}
return brokenColumns;
}
private async tryScanColumnOnWrongValues(
params: IFindBrokenColumnsParams,
column: CacheColumn
) {
const timeStart = new Date();
if ( params.onStartScanColumn ) {
await params.onStartScanColumn(column.toString());
}
try {
const wrongExample = await this.tryFindWrongRowExampleForColumn(params, column);
if ( params.onScanColumn ) {
const timeEnd = new Date();
await params.onScanColumn({
column: column.getId(),
hasWrongValues: !!wrongExample,
wrongExample,
time: {
start: timeStart,
end: timeEnd,
duration: +timeEnd - +timeStart
}
});
}
return wrongExample;
} catch(error) {
if ( params.onScanError ) {
const timeEnd = new Date();
await params.onScanError({
column: column.toString(),
error: error as any,
time: {
start: timeStart,
end: timeEnd,
duration: +timeEnd - +timeStart
}
});
}
}
}
private async tryFindWrongRowExampleForColumn(
params: IFindBrokenColumnsParams,
column: CacheColumn
) {
const columnRef = `${column.for.getIdentifier()}.${column.name}`;
let details = "null::json";
if ( column.select.from.length === 1 ) {
const selectSql = column.select.toString();
const fromAlias = column.select.getFromTable().getIdentifier();
const sourceRowJson = `row_to_json(${fromAlias}.*)`;
if ( this.database.aggregators.some(aggName =>
selectSql.includes(aggName + "("))
) {
details = `array_agg(${sourceRowJson})`;
}
else if ( column.select.orderBy ) {
details = `(${column.select.clone({
columns: [new SelectColumn({
expression: Expression.unknown(`array_agg(${sourceRowJson})`),
name: "source_row"
})],
orderBy: undefined,
limit: undefined
})})`;
}
else {
details = `ARRAY[${sourceRowJson}]`;
}
}
const selectExpected = column.select.clone({
columns: [
...column.select.columns,
new SelectColumn({
expression: Expression.unknown(details),
name: "__cache_source_row_details__"
})
]
});
let whereBroken = `${columnRef} is distinct from tmp.${column.name}`;
const expression = column.getColumnExpression();
if ( expression.isFuncCall() ) {
const [call] = expression.getFuncCalls();
if ( call.name === "array_agg" ) {
whereBroken = `
${columnRef} is distinct from tmp.${column.name} and
not(${columnRef} @> tmp.${column.name} and
${columnRef} <@ tmp.${column.name})
`
}
if ( call.name === "sum" ) {
whereBroken = `coalesce(${columnRef}, 0) is distinct from coalesce(tmp.${column.name}, 0)`
}
if ( call.name === "string_agg" && !call.orderBy ) {
const actual = `string_to_array(coalesce(${columnRef}, ''), ${call.args[1]})`;
const expected = `string_to_array(coalesce(tmp.${column.name}, ''), ${call.args[1]})`;
whereBroken = `
${actual} is distinct from ${expected} and
not(${actual} @> ${expected} and
${actual} <@ ${expected})
`;
}
}
if ( column.name == Cache.generateJsonHelperColumnName(column.cache.name) ) {
whereBroken = `coalesce(${columnRef}, '{}'::jsonb) is distinct from coalesce(tmp.${column.name}, '{}'::jsonb)`;
}
const selectHasBroken = `
select
${columnRef} as "actual",
tmp.${column.name} as "expected",
'${column.getTableId()}' as "table",
row_to_json(${column.for.getIdentifier()}.*) as "cacheRow",
tmp.__cache_source_row_details__ as "sourceRows"
from ${column.for}
left join lateral (
${selectExpected.toSQL()}
) as tmp on true
where
${whereBroken}
order by ${column.for.getIdentifier()}.id
limit 1
`;
const {rows} = await this.driver.queryWithTimeout(
selectHasBroken,
params.timeout ?? 0
);
const wrongExample = rows[0] as IColumnScanResult["wrongExample"];
if ( wrongExample ) {
wrongExample.table = column.getTableId();
wrongExample.selectExpectedForThatRow = `
select
${column.for.getIdentifier()}.id,
${columnRef} as "actual",
tmp.${column.name} as "expected"
from ${column.for}
left join lateral (
${selectExpected.toSQL()}
) as tmp on true
where
${column.for.getIdentifier()}.id = ${wrongExample.cacheRow.id} and
${columnRef} is distinct from tmp.${column.name}
`;
}
return wrongExample;
}
}