UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

341 lines 13.6 kB
import type { QueryAnnotation } from "../abstract/QueryAnnotation"; import { Runner } from "../abstract/Runner"; import type { Schema } from "../abstract/Schema"; import type { Field, FieldAliased, Hints, Literal, Table, Where } from "../types"; import type { PgClient } from "./PgClient"; /** * A convenient pile of helper methods usable by most of PgQuery* classes. In * some sense it's an anti-pattern, but still reduces the boilerplate. * * PgRunner is also responsible for stringifying the values passed to the * queries and parsing values returned from the DB according to the field types * specs. */ export declare abstract class PgRunner<TTable extends Table, TInput, TOutput> extends Runner<TInput, TOutput> { readonly schema: Schema<TTable>; private client; private escapers; private oneOfBuilders; private dbValueToJs; private stringify; ["constructor"]: typeof PgRunner; protected clientQuery<TOutput extends object>(sql: string, annotations: QueryAnnotation[], batchFactor: number, hints?: Hints): Promise<TOutput[]>; /** * Formats prefixes/suffixes of various compound SQL clauses. Don't use on * performance-critical path! */ protected fmt(template: string, args?: { fields?: Array<FieldAliased<TTable>>; normalize?: boolean; }): string; /** * Escapes a value at runtime using the codegen functions created above. We * use escapers table and the codegen for the following reasons: * 1. We want to be sure that we know in advance, how to escape all table * fields (and not fail at runtime). * 2. We want to make createEscapeCode() the single source of truth about * fields escaping, even at runtime. */ protected escapeValue(field: Field<TTable>, value: unknown): string; /** * Escapes field name identifier. * - In case it's a composite primary key, returns its `ROW(f1,f2,...)` * representation. * - A field may be aliased, e.g. if `{ field: "abc", alias: "$cas.abc" }` is * passed, then the returned value will be `"$cas.abc"`. Basically, `field` * name is used only to verify that such field is presented in the schema. */ protected escapeField(info: FieldAliased<TTable>, { withTable, normalize }?: { withTable?: string; normalize?: boolean; }): string; /** * Returns a newly created JS function which, when called with a row set, * returns the following SQL clause: * * ``` * WITH rows(id, a, b, _key) AS (VALUES * ((NULL::tbl).id, (NULL::tbl).a, (NULL::tbl).b, 'k0'), * ('123', 'xyz', 'nn', 'kSome'), * ('456', 'abc', 'nn', 'kOther'), * ... * ) * {suffix} * ``` * * For composite primary key, its parts (fields) are always prepended. The set * of columns is passed in specs. */ protected createWithBuilder({ fields, suffix, }: { fields: ReadonlyArray<FieldAliased<TTable>>; suffix: string; }): { prefix: string; func: (entries: Iterable<[key: string, input: object]>) => string; suffix: string; }; /** * Returns a newly created JS function which, when called with a row set, * returns the following SQL clause (when called with withKey=true): * * ``` * ('123', 'xyz', 'nn', 'kSome'), * ('456', 'abc', 'nn', 'kOther'), * ... * ) * ``` * * or (when called without withKey): * * ``` * ('123', 'xyz', 'nn'), * ('456', 'abc', 'nn'), * ... * ``` * * The set of columns is passed in fields. * * When the builder func is called, the actual values for some field in a row * is extracted from the same-named prop of the row, but if a `{ field, * rowPath }` object is passed in `fields` array, then the value is extracted * from the `rowPath` sub-prop of the row. This is used to e.g. access * `row.$cas.blah` value for a field named blah (in this case, * `rowPath="$cas"`). * * Notice that either a simple primary key or a composite primary key columns * are always prepended to the list of values since it makes no sense to * generate VALUES clause without exact identification of the destination. */ protected createValuesBuilder<TInput extends object>({ prefix, indent, fields, withKey, skipSorting, suffix, }: { prefix: string; indent: string; fields: ReadonlyArray<FieldAliased<TTable>>; withKey?: boolean; skipSorting?: boolean; suffix: string; }): { prefix: string; func: (entries: Iterable<[key: string, input: TInput]>) => string; suffix: string; }; /** * Returns a newly created JS function which, when called with an object, * returns the following SQL clause: * * id='123', a='xyz', b='nnn' [, {literal}] * * The set of columns is passed in specs, all other columns are ignored. */ protected createUpdateKVsBuilder(fields: Array<Field<TTable>>): (input: object, literal?: Literal) => string; /** * Prefers to do utilize createAnyBuilder() if it can (i.e. build * a=ANY('{...}') clause). Otherwise, builds an IN(...) clause. */ protected createOneOfBuilder(field: Field<TTable>, fieldValCode?: string): (values: Iterable<unknown>) => string; /** * Given a list of fields, returns two builders: * * 1. "Optimized": a newly created JS function which, when called with a row * set, returns one the following SQL clauses: * * ``` * WHERE (field1, field2) IN(VALUES * ((NULL::tbl).field1, (NULL::tbl).field2), * ('aa', 'bb'), * ('cc', 'dd')) * * or * * WHERE (field1='a' AND field2='b' AND field3 IN('a', 'b', 'c', ...)) OR (...) * ^^^^^^^^^^prefix^^^^^^^^^ ^^^^^^^^ins^^^^^^^ * ``` * * 2. "Plain": the last one builder mentioned above (good to always use for * non-batched queries for instance). */ protected createWhereBuildersFieldsEq<TInput extends object>(args: { prefix: string; fields: ReadonlyArray<Field<TTable>>; suffix: string; }): { plain: { prefix: string; func: (inputs: Iterable<[key: string, input: TInput]>) => string; suffix: string; }; optimized: { prefix: string; func: (inputs: Iterable<[key: string, input: TInput]>) => string; suffix: string; }; }; /** * Returns a newly created JS function which, when called with a Where object, * returns the generated SQL WHERE clause. * * - The building is relatively expensive, since it traverses the Where object * at run-time and doesn't know the shape beforehand. * - If the Where object is undefined, skips the entire WHERE clause. */ protected createWhereBuilder({ prefix, suffix, }: { prefix: string; suffix: string; }): { prefix: string; func: (where: Where<TTable>) => string; suffix: string; }; /** * Prepends or appends a primary key to the list of fields. In case the * primary key is plain (i.e. "id" field), it's just added as a field; * otherwise, the unique key fields are added. * * For INSERT/UPSERT operations, we want to append the primary key, since it's * often types pre-generated as a random-looking value. In many places, we * sort batched lists of rows before e.g. inserting them, so we order them by * their natural data order which prevents deadlocks on unique key conflict * when multiple concurrent transactions try to insert the same set of rows in * different order ("while inserting index tuple"). * * For UPDATE operations though, we want to prepend the primary key, to make * sure we run batched updates in the same order in multiple concurrent * transactions. This lowers the chances of deadlocks too. */ protected addPK(fields: ReadonlyArray<Field<TTable>>, mode: "prepend" | "append"): string[]; constructor(schema: Schema<TTable>, client: PgClient); delayForSingleQueryRetryOnError(e: unknown): number | "immediate_retry" | "no_retry"; shouldDebatchOnError(e: unknown): boolean; /** * Given a list of fields, returns a newly created JS function which, when * called with a row set, returns the following SQL clause: * * ``` * WHERE (field1='a' AND field2='b' AND field3 IN('a', 'b', 'c', ...)) OR (...) * ^^^^^^^^^^prefix^^^^^^^^^ ^^^^^^^^ins^^^^^^^ * ``` * * The assumption is that the last field in the list is the most variable, * whilst all previous fields compose a more or less static prefix * * - ATTENTION: if at least one OR is produced, it will likely result in a * slower Bitmap Index Scan. * - Used in runSingle() (no ORs there) or when optimized builder is not * available (e.g. when unique key contains nullable fields). */ private createWhereBuilderFieldsEqOrBased; /** * Given a list of fields, returns a newly created JS function which, when * called with a row set, returns the following SQL clause: * * ``` * WHERE (field1, field2) IN(VALUES * ((NULL::tbl).field1, (NULL::tbl).field2), * ('aa', 'bb'), * ('cc', 'dd')) * ``` * * The assumption is that all fields are non-nullable. * * - This clause always produces an Index Scan (not Bitmap Index Scan). * - Used in most of the cases in runBatch(), e.g. when unique key has >1 * fields, and they are all non-nullable. */ private createWhereBuilderFieldsEqTuplesBased; private buildWhere; private buildFieldBinOp; private buildFieldIsDistinctFrom; private buildFieldEq; private buildLogical; private buildNot; private buildFieldNe; /** * Returns a newly created JS function which, when called with an array of * values, returns one of following SQL clauses: * * - $field=ANY('{aaa,bbb,ccc}') * - ($field=ANY('{aaa,bbb}') OR $field IS NULL) * - $field='aaa' (see below, why) * - ($field='aaa' OR $field IS NULL) * - $field IS NULL * - false */ private createAnyBuilder; /** * Returns a newly created JS function which, when called with an array of * values, returns one of following SQL clauses: * * - $field IN('aaa', 'bbb', 'ccc') * - ($field IN('aaa', 'bbb') OR $field IS NULL) * - $field IS NULL * - false * * This only works for primitive types. */ private createInBuilder; /** * For codegen, returns the following piece of JS code: * * '($fieldValCode !== undefined ? this.escapeXyz($fieldValCode) : "$defSQL")' * * It's expected that, while running the generated code, `this` points to an * object with a) `escapeXyz()` functions, b) `stringify` object containing * the table fields custom to-string converters. */ private createEscapeCode; /** * Compiles a function body with `this` bound to some well-known properties * which are available in the body. * * For each table, we compile frequently accessible pieces of code which * serialize data in SQL format. This allows to remove lots of logic and "ifs" * from runtime and speed up hot code paths. */ private newFunction; /** * The problem: PG is not working fine with queries like: * * ``` * WITH rows(composite_id, c) AS ( * VALUES * ( ROW((NULL::tbl).x, (NULL::tbl).y), (NULL::tbl).c ), * ( ROW(1,2), 3 ), * ( ROW(3,4), 5 ) * ) * UPDATE tbl SET c=rows.c * FROM rows WHERE ROW(tbl.x, tbl.y)=composite_id * ``` * * It cannot match the type of composite_id with the row, and even the trick * with NULLs doesn't help it to infer types. It's a limitation of WITH clause * (because in INSERT ... VALUES, there is no such problem). * * So the only solution is to parse/decompose the row string into individual * unique key columns at runtime for batched UPDATEs. And yes, it's slow. * * ``` * WITH rows(x, y, c) AS ( * VALUES * ( (NULL::tbl).x, (NULL::tbl).y, (NULL::tbl).c ), * ( 1, 2, 3 ), * ( 3, 4, 5 ) * ) * UPDATE tbl SET c=rows.c * FROM rows WHERE ROW(tbl.x, tbl.y)=ROW(rows.x, ROW.y) * ``` */ private unfoldCompositePK; /** * Some data types are different between PG and JS. Here we have a chance to * "normalize" them. E.g. in JS, Date is truncated to milliseconds (3 digits), * whilst in PG, it's by default of 6 digits precision (so if we didn't * normalize, then JS Date would've been never equal to a PG timestamp). */ private normalizeSQLExpr; /** * Throws an exception about some field being not mentioned in the table * schema if the passed data is undefined. Notice that ID is treated as always * available in this message. */ private nullThrowsUnknownField; } //# sourceMappingURL=PgRunner.d.ts.map