ddl-manager
Version:
store postgres procedures and triggers in files
242 lines • 8.89 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheLinter = void 0;
const psql_lang_1 = require("psql-lang");
const assert_1 = require("assert");
class CacheLinter {
constructor(cursor, cache, select = cache.row.cache, cacheForItem = new psql_lang_1.FromTable({ row: {
table: cache.row.for,
as: cache.row.as
} })) {
this.cursor = cursor;
this.cache = cache;
this.select = select;
this.cacheForItem = cacheForItem;
}
static lint(cursor, cache) {
const linter = new CacheLinter(cursor, cache);
linter.lint();
}
lint() {
this.validateCte();
this.validateUnion();
this.validateGroupBy();
this.validateFrom();
this.validateJoins();
this.validateSubQueries();
this.validateColumns();
this.validateColumnsLinks();
this.validateWhere();
this.validateStringAggregations();
this.validateOrderByAndLimit();
}
validateCte() {
const cte = this.select.row.with;
if (cte) {
this.throwError("CTE (with queries) are not supported", cte);
}
}
validateUnion() {
const union = this.select.row.union;
if (union) {
this.throwError("UNION are not supported", union.select);
}
}
validateGroupBy() {
const groupBy = this.select.row.groupBy;
if (groupBy === null || groupBy === void 0 ? void 0 : groupBy.length) {
this.throwError("GROUP BY are not supported", groupBy[0]);
}
}
validateSubQueries() {
const subQuery = this.select.filterChildrenByInstance(psql_lang_1.Select)[0];
if (subQuery) {
this.throwError("SUB QUERIES are not supported", subQuery);
}
}
validateColumns() {
var _a, _b;
const columns = (_a = this.select.row.select) !== null && _a !== void 0 ? _a : [];
if (!columns.length) {
this.throwError("required select any columns or expressions", this.select);
}
const columnsMap = {};
for (const columnNode of columns) {
const columnAlias = (_b = columnNode.row.as) === null || _b === void 0 ? void 0 : _b.toValue();
if (!columnAlias) {
this.throwError("required alias for every cache column", columnNode);
}
if (columnAlias in columnsMap) {
this.throwError("duplicated cache column", columnNode.row.as);
}
columnsMap[columnAlias] = true;
}
}
validateColumnsLinks() {
extract(this.select, psql_lang_1.ColumnReference)
.forEach(this.validateColumnLink.bind(this));
}
validateColumnLink(columnLink) {
const isStar = (columnLink.row.allColumns &&
columnLink.row.column.length === 0);
if (isStar) {
return;
}
const sourceFromItem = columnLink.findDeclaration();
const missingSource = (!sourceFromItem &&
!columnLink.isDependentOn(this.cacheForItem));
if (missingSource) {
if (columnLink.row.column.length === 1) {
const fromItems = this.select.filterChildrenByInstance(psql_lang_1.AbstractFromItem);
if (fromItems.length != 1) {
this.throwError(`implicit table reference: ${columnLink}`, columnLink);
}
return;
}
this.throwError(`source for column ${columnLink} not found`, columnLink);
}
}
validateStringAggregations() {
this.select.filterChildrenByInstance(psql_lang_1.FunctionCall)
.forEach(this.validateStringAgg.bind(this));
}
validateStringAgg(funcCall) {
const name = String(funcCall.row.call);
if (name != "string_agg") {
return;
}
const args = funcCall.row.arguments || [];
if (args.length === 1) {
this.throwError("required delimiter for string_agg", funcCall);
}
}
validateFrom() {
if (this.select.row.from.length > 1) {
this.throwError("multiple FROM are not supported", this.select.row.from[1]);
}
const allFromItems = extract(this.select, psql_lang_1.AbstractFromItem);
for (const fromItem of allFromItems) {
if (!(fromItem instanceof psql_lang_1.FromTable)) {
this.throwError("supported only from table", fromItem);
}
}
}
validateJoins() {
this.select.filterChildrenByInstance(psql_lang_1.Join)
.forEach(this.validateJoin.bind(this));
}
validateJoin(join) {
if (!("on" in join.row)) {
this.throwError("required ON condition for join", join);
}
}
validateWhere() {
const { from, where } = this.select.row;
if (!where || !(from[0] instanceof psql_lang_1.FromTable)) {
return;
}
extract(where, psql_lang_1.EqualAny)
.forEach(this.validateWhereAny.bind(this));
extract(where, psql_lang_1.BinaryOperator)
.forEach(this.validateWhereArrayIntersection.bind(this));
}
validateWhereAny(node) {
const isWrongSide = (!this.containsFromTable(node.row.operand) // for_table.column = ...
&&
!(node.row.equalAny instanceof psql_lang_1.Select) &&
this.containsFromTable(node.row.equalAny) // .. =any( from_table.column )
);
if (isWrongSide) {
this.throwError([
"your condition is slow SeqScan, condition should be:",
`${node.row.equalAny} && ARRAY[ ${node.row.operand} ]`
].join("\n"), node);
}
}
validateWhereArrayIntersection(node) {
if (node.row.operator !== "&&") {
return;
}
const arrayLiteral = getArray(node.row.left, node.row.right);
if (!arrayLiteral) {
return;
}
const operand = getOther(node.row.left, node.row.right, arrayLiteral);
const isWrongSide = (this.containsFromTable(arrayLiteral) && // ... && ARRAY[ from_table.column ]
!this.containsFromTable(operand) // for_table.column && ...
);
if (isWrongSide) {
this.throwError([
"your condition is slow SeqScan, condition should be:",
`${arrayLiteral.row.array} = any(${operand})`
].join("\n"), node);
}
}
validateOrderByAndLimit() {
const orderBy = this.select.row.orderBy;
const limit = this.select.row.limit;
if (!(orderBy === null || orderBy === void 0 ? void 0 : orderBy.length)) {
if (limit) {
this.throwError("required ORDER BY", limit);
}
return;
}
const fromItems = this.select.row.from;
if (fromItems.length === 0) {
this.throwError("required FROM ITEM", orderBy[0]);
}
const join = extract(this.select, psql_lang_1.Join)[0];
if (join) {
this.throwError("joins is not supported for order by/limit 1 trigger", join);
}
if (!limit) {
this.throwError("required LIMIT 1", orderBy[0]);
}
if (String(limit) !== "1") {
this.throwError("supported only limit 1", limit);
}
}
containsFromTable(operand) {
const fromItem = this.getFromTable();
const schemaName = fromItem.row.table.row.schema;
const tableName = fromItem.row.table.row.name;
const tableAlias = fromItem.row.as;
return extract(operand, psql_lang_1.ColumnReference).some(column => {
const path = column.row.column;
if (path.length === 0 || path.length === 1) {
return true;
}
if (path.length === 2) {
return path[0].equal(tableAlias || tableName);
}
return (schemaName &&
path[0].equal(schemaName) &&
path[1].equal(tableName));
});
}
getFromTable() {
const fromItem = this.select.row.from[0];
assert_1.strict.ok(fromItem instanceof psql_lang_1.FromTable, "required from table");
return fromItem;
}
throwError(error, atNode) {
this.cursor.throwError(error, atNode);
}
}
exports.CacheLinter = CacheLinter;
function extract(node, NodeClass) {
if (node instanceof NodeClass) {
return [node];
}
return node.filterChildrenByInstance(NodeClass);
}
function getArray(...nodes) {
return nodes.find(node => node instanceof psql_lang_1.ArrayLiteral);
}
function getOther(left, right, other) {
if (left === other) {
return right;
}
return left;
}
//# sourceMappingURL=CacheLinter.js.map