ddl-manager
Version:
store postgres procedures and triggers in files
363 lines (305 loc) • 10 kB
text/typescript
import { wrapText } from "../postgres/wrapText";
import { MAX_NAME_LENGTH } from "../postgres/constants";
import { Comment } from "./Comment";
import { uniq } from "lodash";
import { equalType, formatType } from "./Type";
export interface IDatabaseFunctionParams {
schema: string;
name: string;
args: IDatabaseFunctionArgument[];
returns: IDatabaseFunctionReturns;
body: string;
language?: string;
immutable?: boolean;
returnsNullOnNull?: boolean;
stable?: boolean;
strict?: boolean;
parallel?: ("safe" | "unsafe" | "restricted")[];
cost?: number;
comment?: Comment;
}
export interface IDatabaseFunctionReturns {
setof?: boolean;
table?: IDatabaseFunctionArgument[];
type?: string;
}
export interface IDatabaseFunctionArgument {
out?: boolean;
in?: boolean;
name?: string;
type: string;
default?: string;
}
export class DatabaseFunction {
readonly schema!: string;
readonly name!: string;
readonly returns!: IDatabaseFunctionReturns;
readonly args!: IDatabaseFunctionArgument[];
readonly body!: string;
readonly language: string;
readonly comment!: Comment;
readonly cacheSignature?: string;
readonly frozen?: boolean;
readonly immutable?: boolean;
readonly returnsNullOnNull?: boolean;
readonly stable?: boolean;
readonly strict?: boolean;
readonly parallel?: ("safe" | "unsafe" | "restricted")[];
readonly cost?: number;
private assignedColumns?: string[];
constructor(json: IDatabaseFunctionParams) {
Object.assign(this, json);
this.language = json.language || "plpgsql";
this.name = this.name.slice(0, MAX_NAME_LENGTH);
this.comment = json.comment || Comment.fromFs({
objectType: "function"
});
this.cacheSignature = this.comment.cacheSignature;
this.frozen = this.comment.frozen;
}
equalName(schemaName: string): boolean {
if ( !schemaName.includes(".") ) {
schemaName = "public." + schemaName;
}
return this.schema + "." + this.name === schemaName;
}
equal(otherFunc: DatabaseFunction) {
return (
this.schema === otherFunc.schema &&
this.name === otherFunc.name &&
this.body === otherFunc.body &&
this.args.length === otherFunc.args.length &&
this.args.every((myArg, i) =>
equalArgument(myArg, otherFunc.args[i])
)
&&
equalReturns(this.returns, otherFunc.returns) &&
this.language === otherFunc.language &&
!!this.immutable === !!otherFunc.immutable &&
!!this.returnsNullOnNull === !!otherFunc.returnsNullOnNull &&
!!this.stable === !!otherFunc.stable &&
!!this.strict === !!otherFunc.strict &&
// null == undefined
// tslint:disable-next-line: triple-equals
String(this.parallel) == String(otherFunc.parallel) &&
this.comment.equal(otherFunc.comment)
);
}
getSignature() {
const argsTypes = this.args
.filter((arg: any) =>
!arg.out
)
.map((arg: any) =>
arg.type
);
return `${ this.schema }.${ this.name }(${ argsTypes.join(", ") })`;
}
findAssignColumns() {
if ( this.assignedColumns ) {
return this.assignedColumns;
}
const body = this.body
.replace(/--[^\n\r]+/g, "")
.replace(/\/\*.+?\*\//g, "");
const matches = body.match(/(begin|then|loop|;)\s*;?\s*new\.(\w+)\s*=/g) || [];
const assignedColumns = matches.map(str =>
str
.replace(/^((begin|then|loop)\s*;?|;)\s*new\./, "")
.replace(/\s*=$/, "")
.toLowerCase()
);
this.assignedColumns = uniq(assignedColumns).sort();
return this.assignedColumns;
}
toSQL(params: {
body?: string;
immutable?: boolean;
stable?: boolean;
} = {}) {
let additionalParams = "";
if ( coalesce(params.immutable, this.immutable) ) {
additionalParams += " immutable";
}
else if ( coalesce(params.stable, this.stable) ) {
additionalParams += " stable";
}
if ( this.returnsNullOnNull ) {
additionalParams += " returns null on null input";
}
else if ( this.strict ) {
additionalParams += " strict";
}
if ( this.parallel ) {
additionalParams += " parallel ";
additionalParams += this.parallel;
}
if ( this.cost != null ) {
additionalParams += " cost " + this.cost;
}
if ( additionalParams ) {
additionalParams = "\n" + additionalParams + "\n";
}
const returnsSql = returns2sql(this.returns);
let argsSql = this.args.map((arg: any) =>
" " + arg2sql(arg)
).join(",\n");
if ( this.args.length ) {
argsSql = "\n" + argsSql + "\n";
}
// отступов не должно быть!
// иначе DDLManager.dump будет писать некрасивый код
return `
create or replace function ${
this.schema === "public" ?
"" :
this.schema + "."
}${ this.name }(${argsSql})
returns ${ returnsSql }${ additionalParams } as ${ wrapText(coalesce(params.body, this.body), "body") }
language ${this.language}
`.trim();
}
toSQLWithLog() {
const DONT_LOG_FUNCS = [
"log",
"cm_build_array_for",
"get_options"
];
if ( DONT_LOG_FUNCS.includes(this.name) || this.language === "sql" ) {
return this.toSQL();
}
// вставляем в самый первый begin
let body = (" " + this.body).replace(
/(([^\w_])begin[^\w_])/i,
`$2
declare ___call_id integer;
begin
insert into system_calls (
tid,
func_name,
call_time
) values (
txid_current(),
'${ this.schema }.${ this.name }',
extract(epoch from timeofday()::timestamp without time zone)
)
returning id into ___call_id;
`
);
// перед каждым return
body = body.replace(
/([^\w_]return[^\w_]|[^\w_]end\s*;?\s*(\$\w+\$)?\s*$)/ig,
`
insert into system_calls (
tid,
end_time,
end_id
) values (
txid_current(),
extract(epoch from timeofday()::timestamp without time zone),
___call_id
)
on conflict (end_id)
do update set
end_time = excluded.end_time,
id = excluded.id;
$1
`
);
const sql = this.toSQL({
body,
// INSERT is not allowed in a non-volatile function
immutable: false,
stable: false
});
return sql;
}
}
function returns2sql(returns: IDatabaseFunctionReturns) {
let out = "";
if ( returns.setof ) {
out += "setof ";
}
if ( returns.table ) {
out += `table(${
returns.table.map((arg: any) =>
arg2sql(arg)
).join(", ")
})`;
} else {
out += returns.type;
}
return out;
}
function arg2sql(arg: IDatabaseFunctionArgument) {
let out = "";
if ( arg.out ) {
out += "out ";
}
else if ( arg.in ) {
out += "in ";
}
if ( arg.name ) {
out += arg.name;
out += " ";
}
out += arg.type;
if ( arg.default ) {
out += " default ";
out += arg.default;
}
return out;
}
function equalArgument(argA: IDatabaseFunctionArgument, argB: IDatabaseFunctionArgument) {
return (
// null == undefined
// tslint:disable-next-line: triple-equals
argA.name == argB.name &&
equalType(argA.type, argB.type) &&
equalArgumentDefault(argA, argB) &&
!!argA.in === !!argB.in &&
!!argA.out === !!argB.out
);
}
function equalArgumentDefault(argA: IDatabaseFunctionArgument, argB: IDatabaseFunctionArgument) {
if ( !argA.default && !argB.default ) {
return true;
}
// null == undefined
// tslint:disable-next-line: triple-equals
if ( argA.default == argB.default ) {
return true;
}
const defaultA = formatDefault(argA);
const defaultB = formatDefault(argB);
return defaultA === defaultB;
}
function formatDefault(someArg: IDatabaseFunctionArgument) {
let someDefault = ("" + someArg.default).trim().toLowerCase();
someDefault = someDefault.replace(/\s*::\s*([\w\s]+|numeric\([\d\s,]+\))(\[])?$/, "");
const type = formatType(someArg.type);
someDefault += "::" + type;
if ( someDefault.startsWith("{}::") ) {
someDefault = someDefault.replace("{}::", "'{}'::");
}
return someDefault;
}
function equalReturns(returnsA: IDatabaseFunctionReturns, returnsB: IDatabaseFunctionReturns) {
return (
equalType(returnsA.type, returnsB.type) &&
!!returnsA.setof === !!returnsB.setof &&
(
returnsA.table && returnsB.table &&
returnsA.table.every((argA, i) =>
equalArgument(argA, (returnsB.table as any)[i])
)
||
// null == undefined
// tslint:disable-next-line: triple-equals
returnsA.table == returnsB.table
)
);
}
function coalesce<T>(...values: (T | null | undefined)[]): T {
return values.find(value => value != null) as T;
}