@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
341 lines • 13.6 kB
TypeScript
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