@dataplan/pg
Version:
PostgreSQL step classes for Grafast
476 lines • 24 kB
TypeScript
import type { ConnectionHandlingResult, ConnectionHandlingStep, ExecutionDetails, ExecutionDetailsStream, GrafastResultsList, Maybe, StepOptimizeOptions } from "grafast";
import { Step } from "grafast";
import type { SQL } from "pg-sql2";
import { $$toSQL } from "pg-sql2";
import type { PgCodecAttributes } from "../codecs.js";
import type { PgResource, PgResourceParameter } from "../datasource.js";
import type { GetPgResourceAttributes, GetPgResourceCodec, GetPgResourceRelations, PgCodec, PgGroupSpec, PgOrderSpec, PgQueryBuilder, PgSelectQueryBuilderCallback, PgSQLCallbackOrDirect, PgTypedStep, ReadonlyArrayOrDirect } from "../interfaces.js";
import { PgLocker } from "../pgLocker.js";
import { PgClassExpressionStep } from "./pgClassExpression.js";
import type { PgHavingConditionSpec, PgWhereConditionSpec } from "./pgCondition.js";
import { PgCondition } from "./pgCondition.js";
import type { PgCursorDetails } from "./pgCursor.js";
import { PgCursorStep } from "./pgCursor.js";
import type { PgSelectSinglePlanOptions } from "./pgSelectSingle.js";
import { PgSelectSingleStep } from "./pgSelectSingle.js";
import type { PgStmtCommonQueryInfo, PgStmtCompileQueryInfo, PgStmtDeferredPlaceholder, PgStmtDeferredSQL } from "./pgStmt.js";
import { PgStmtBaseStep } from "./pgStmt.js";
export type PgSelectParsedCursorStep = Step<null | readonly any[]>;
type PgSelectPlanJoin = {
type: "cross";
from: SQL;
alias: SQL;
attributeNames?: SQL;
lateral?: boolean;
} | {
type: "inner" | "left" | "right" | "full";
from: SQL;
alias: SQL;
attributeNames?: SQL;
conditions: SQL[];
lateral?: boolean;
};
type PgSelectScopedPlanJoin = PgSQLCallbackOrDirect<PgSelectPlanJoin>;
export type PgSelectIdentifierSpec = {
step: Step;
codec: PgCodec;
matches: (alias: SQL) => SQL;
} | {
step: PgTypedStep<any>;
codec?: PgCodec;
matches: (alias: SQL) => SQL;
};
export type PgSelectArgumentSpec = {
step: Step;
pgCodec: PgCodec<any, any, any, any>;
name?: string;
} | {
step: PgTypedStep<any>;
pgCodec?: never;
name?: string;
};
export interface PgSelectArgumentDigest {
position?: number;
name?: string;
placeholder: SQL;
step?: never;
}
interface PgSelectArgumentBasics {
position?: number;
name?: string;
}
interface PgSelectArgumentPlaceholder extends PgSelectArgumentBasics {
placeholder: SQL;
step?: never;
depId?: never;
}
export interface PgSelectArgumentRuntimeValue extends PgSelectArgumentBasics {
placeholder?: never;
value: unknown;
}
export type PgSelectMode = "normal" | "aggregate" | "mutation";
/**
* Something that's placeholder/deferredSQL capable; typically a PgSelectStep
* but not guaranteed.
*/
export type PgRootStep = Step & {
placeholder(step: Step, codec: PgCodec): SQL;
deferredSQL($step: Step<SQL>): SQL;
};
export type PgSelectFromOption = SQL | {
callback: ($select: PgRootStep) => SQL;
} | ((...args: PgSelectArgumentDigest[]) => SQL);
export interface PgSelectOptions<TResource extends PgResource<any, any, any, any, any> = PgResource> {
/**
* Tells us what we're dealing with - data type, columns, where to get it
* from, what it's called, etc. Many of these details can be overridden
* below.
*/
resource: TResource;
/**
* The identifiers to limit the results down to just the row(s) you care
* about.
*
* NOTE: this is required because it's a big footgun to omit it by accident,
* if you truly do not need it (e.g. if you're calling a function with
* limited results or you really want everything) then you can specify it as
* an empty array `[]`.
*/
identifiers: Array<PgSelectIdentifierSpec>;
/**
* Set this true if your query includes any `VOLATILE` function (including
* seemingly innocuous things such as `random()`) otherwise we might only
* call the relevant function once and re-use the result.
*/
forceIdentity?: boolean;
parameters?: readonly PgResourceParameter[];
/**
* If your `from` (or resource.from if omitted) is a function, the arguments
* to pass to the function.
*/
args?: ReadonlyArray<PgSelectArgumentSpec>;
/**
* If you want to build the data in a custom way (e.g. calling a function,
* selecting from a view, building a complex query, etc) then you can
* override the `resource.from` here with your own from code. Defaults to
* `resource.from`.
*/
from?: PgSelectFromOption;
/**
* You should never rely on implicit order - use explicit `ORDER BY` (via
* `$select.orderBy(...)`) instead. However, if you _are_ relying on implicit
* order in your `from` result (e.g. a subquery or function call that has its
* own internal ordering), setting this to `true` will prevent PgSelect from
* inlining some queries (joins) that it thinks might impact the order of
* results. Setting this to `true` does NOT guarantee that you can rely on
* your order being maintained, but it does increase the chances.
*/
hasImplicitOrder?: false;
/**
* If you pass a custom `from` (or otherwise want to aid in debugging),
* passing a custom name can make it easier to follow the SQL/etc that is
* generated.
*/
name?: string;
mode?: PgSelectMode;
/**
* If true and this turns into a join it should be a lateral join.
*/
joinAsLateral?: boolean;
}
interface PgSelectStepResult extends ConnectionHandlingResult<unknown> {
hasNextPage: boolean;
hasPreviousPage: boolean;
/** a tuple based on what is selected at runtime */
items: ReadonlyArray<unknown[]> | AsyncIterable<unknown[]>;
cursorDetails: PgCursorDetails | undefined;
groupDetails: PgGroupDetails | undefined;
m: Record<string, unknown>;
}
export interface PgGroupDetails {
readonly indicies: ReadonlyArray<{
index: number;
codec: PgCodec;
}>;
}
/**
* This represents selecting from a class-like entity (table, view, etc); i.e.
* it represents `SELECT <attributes>, <cursor?> FROM <table>`. You can also add
* `JOIN`, `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`. You cannot add `GROUP BY`
* because that would invalidate the identifiers; and as such you can't use
* `HAVING` or functions that implicitly turn the query into an aggregate. We
* don't allow `UNION`/`INTERSECT`/`EXCEPT`/`FOR UPDATE`/etc at this time,
* purely because it hasn't been sufficiently considered.
*/
export declare class PgSelectStep<TResource extends PgResource<any, any, any, any, any> = PgResource> extends PgStmtBaseStep<any> implements ConnectionHandlingStep<any, PgSelectSingleStep<TResource> | PgClassExpressionStep<any, TResource>, any, null | readonly any[]> {
static $$export: {
moduleName: string;
exportName: string;
};
isSyncAndSafe: boolean;
private readonly from;
private readonly hasImplicitOrder;
/**
* This defaults to the name of the resource but you can override it. Aids
* in debugging.
*/
readonly name: string;
/**
* To be used as the table alias, we always use a symbol unless the calling
* code specifically indicates a string to use.
*/
private readonly symbol;
/**
* When SELECTs get merged, symbols also need to be merged. The keys in this
* map are the symbols of PgSelects that don't exist any more, the values are
* symbols of the PgSelects that they were replaced with (which might also not
* exist in future, but we follow the chain so it's fine).
*/
private readonly _symbolSubstitutes;
/** = sql.identifier(this.symbol) */
readonly alias: SQL;
/**
* The resource from which we are selecting: table, view, etc
*/
readonly resource: TResource;
private relationJoins;
private joins;
private conditions;
private groups;
private havingConditions;
private orders;
private isOrderUnique;
protected firstStepId: number | null;
protected lastStepId: number | null;
protected fetchOneExtra: boolean;
/** When using natural pagination, this index is the lower bound (and should be excluded) */
protected lowerIndexStepId: number | null;
/** When using natural pagination, this index is the upper bound (and should be excluded) */
protected upperIndexStepId: number | null;
protected offsetStepId: number | null;
protected beforeStepId: number | null;
protected afterStepId: number | null;
private connectionDepId;
private applyDepIds;
/**
* Set this true if your query includes any `VOLATILE` function (including
* seemingly innocuous things such as `random()`) otherwise we might only
* call the relevant function once and re-use the result.
*/
forceIdentity: boolean;
protected placeholders: Array<PgStmtDeferredPlaceholder>;
protected deferreds: Array<PgStmtDeferredSQL>;
private fixedPlaceholderValues;
/**
* If true, we don't need to add any of the security checks from the
* resource; otherwise we must do so. Default false.
*/
private isTrusted;
/**
* If true, we know at most one result can be matched for each identifier, so
* it's safe to do a `LEFT JOIN` without risk of returning duplicates. Default false.
*/
private isUnique;
/**
* If true, we will not attempt to inline this into the parent query.
* Default false.
*/
private isInliningForbidden;
/**
* If true and this becomes a join during optimisation then it should become
* a lateral join; e.g. in the following query, the left join must be
* lateral.
*
* ```sql
* select *
* from foo
* left join lateral (
* select (foo.col).*
* ) t
* on true
* ```
*/
private joinAsLateral;
/**
* The list of things we're selecting.
*/
private selects;
/**
* The id for the PostgreSQL context plan.
*/
private contextId;
readonly mode: PgSelectMode;
protected locker: PgLocker<this>;
private _meta;
static clone<TResource extends PgResource<any, any, any, any, any>>(cloneFrom: PgSelectStep<TResource>, mode?: PgSelectMode): PgSelectStep<TResource>;
_fieldMightStream: boolean;
constructor(options: PgSelectOptions<TResource>);
toStringMeta(): string;
lock(): void;
setInliningForbidden(newInliningForbidden?: boolean): this;
inliningForbidden(): boolean;
setTrusted(newIsTrusted?: boolean): this;
trusted(): boolean;
/**
* Set this true ONLY if there can be at most one match for each of the
* identifiers. If you set this true when this is not the case then you may
* get unexpected results during inlining; if in doubt leave it at the
* default.
*/
setUnique(newUnique?: boolean): this;
unique(): boolean;
/**
* Join to a named relationship and return the alias that can be used in
* SELECT, WHERE and ORDER BY.
*/
singleRelation<TRelationName extends keyof GetPgResourceRelations<TResource> & string>(relationIdentifier: TRelationName): SQL;
/**
* @experimental Please use `singleRelation` or `manyRelation` instead.
*/
join(spec: PgSelectScopedPlanJoin): void;
getMeta(key: string): import("grafast").AccessStep<unknown>;
private nullCheckIndex;
/**
* Finalizes this instance and returns a mutable clone; useful for
* connections/etc (e.g. copying `where` conditions but adding more, or
* pagination, or grouping, aggregates, etc)
*/
clone(mode?: PgSelectMode): PgSelectStep<TResource>;
connectionClone(mode?: PgSelectMode): PgSelectStep<TResource>;
where(rawCondition: PgSQLCallbackOrDirect<PgWhereConditionSpec<keyof GetPgResourceAttributes<TResource> & string>>): void;
groupBy(group: PgSQLCallbackOrDirect<PgGroupSpec>): void;
having(rawCondition: PgSQLCallbackOrDirect<PgHavingConditionSpec<keyof GetPgResourceAttributes<TResource> & string>>): void;
orderBy(order: PgSQLCallbackOrDirect<PgOrderSpec>): void;
setOrderIsUnique(): void;
apply($step: Step<ReadonlyArrayOrDirect<Maybe<PgSelectQueryBuilderCallback>>>): void;
protected assertCursorPaginationAllowed(): void;
items(): PgSelectRowsStep<TResource>;
private getCursorDetails;
/**
* When selecting a connection we need to be able to get the cursor. The
* cursor is built from the values of the `ORDER BY` clause so that we can
* find nodes before/after it.
*/
cursorForItem($item: Step<readonly [...(readonly any[])] | null>): PgCursorStep;
private needsGroups;
getGroupDetails(): Step<PgGroupDetails>;
/**
* `execute` will always run as a root-level query. In future we'll implement a
* `toSQL` method that allows embedding this plan within another SQL plan...
* But that's a problem for later.
*
* This runs the query for every entry in the values, and then returns an
* array of results where each entry in the results relates to the entry in
* the incoming values.
*
* NOTE: we don't know what the values being fed in are, we must feed them to
* the plans stored in this.identifiers to get actual values we can use.
*/
execute(executionDetails: ExecutionDetails): Promise<GrafastResultsList<PgSelectStepResult>>;
finalize(): void;
deduplicate(peers: PgSelectStep<any>[]): PgSelectStep<TResource>[];
private getParentForInlining;
private mergeSelectsWith;
/**
* - Merge placeholders
* - Merge fixedPlaceholders
* - Merge deferreds
* - Merge _symbolSubstitutes
*/
private mergePlaceholdersInto;
private streamDetailsDepIds;
addStreamDetails($details: Step<ExecutionDetailsStream | null> | null): void;
mightHaveStream(): boolean;
optimize(options: StepOptimizeOptions): Step;
/**
* Most likely you want `.single()` instead of this method.
*
* If this plan may only return one record, you can use `.singleAsRecord()`
* to return a plan that resolves to that record (rather than a list of
* records as it does currently).
*
* The main reason to use this instead of `.single()` is if you are
* paginating over a scalar and you truly need a PgSelectSingleStep interface
* e.g. so you can get the `count(*)` aggregate.
*
* Beware: if you call this and the database might actually return more than
* one record then you're potentially in for a Bad Time.
*/
singleAsRecord(options?: PgSelectSinglePlanOptions): PgSelectSingleStep<TResource>;
/**
* If this plan may only return one record, you can use `.single()` to return
* a plan that resolves to either that record (in the case of composite
* types) or the underlying scalar (in the case of a resource whose codec has
* no attributes).
*
* Beware: if you call this and the database might actually return more than
* one record then you're potentially in for a Bad Time.
*/
single(options?: PgSelectSinglePlanOptions): TResource extends PgResource<any, PgCodec<any, infer UAttributes, any, any, any, any, any>, any, any, any> ? UAttributes extends PgCodecAttributes ? PgSelectSingleStep<TResource> : PgClassExpressionStep<PgCodec<string, undefined, any, any, any, any, any>, TResource> : never;
private _singleUncached;
row($row: Step, options?: PgSelectSinglePlanOptions): PgSelectSingleStep<TResource>;
/**
* When you return a plan in a situation where GraphQL is expecting a
* GraphQLList, it must implement the `.listItem()` method to return a plan
* for an individual item within this list. Grafast will automatically call
* this (possibly recursively) to pass to the plan resolvers on the children
* of this field.
*
* NOTE: Grafast handles the list indexes for you, so your list item plan
* should process just the single input list item.
*
* IMPORTANT: do not call `.listItem` from user code; it's only intended to
* be called by Grafast.
*/
listItem(itemPlan: Step): TResource extends PgResource<any, PgCodec<any, infer UAttributes, any, any, any, any, any>, any, any, any> ? UAttributes extends PgCodecAttributes ? PgSelectSingleStep<TResource> : PgClassExpressionStep<PgCodec<string, undefined, any, any, any, any, any>, TResource> : never;
[$$toSQL](): SQL;
whereBuilder(): PgCondition<this>;
havingBuilder(): PgCondition<this>;
setMeta(key: string, value: unknown): void;
getMetaRaw(key: string): unknown;
static getStaticInfo<TResource extends PgResource<any, any, any, any, any>>($source: PgSelectStep<TResource>): StaticInfo<TResource>;
}
export declare class PgSelectRowsStep<TResource extends PgResource<any, any, any, any, any> = PgResource> extends Step {
static $$export: {
moduleName: string;
exportName: string;
};
isSyncAndSafe: boolean;
constructor($pgSelect: PgSelectStep<TResource>);
getClassStep(): PgSelectStep<TResource>;
listItem(itemPlan: Step): TResource extends PgResource<any, PgCodec<any, infer UAttributes extends {
[x: string]: import("../codecs.js").PgCodecAttribute<PgCodec<string, /*elided*/ any | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>, boolean>;
} | undefined, any, any, any, any, any>, any, any, any> ? UAttributes extends {
[x: string]: import("../codecs.js").PgCodecAttribute<PgCodec<string, /*elided*/ any | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>, boolean>;
} ? PgSelectSingleStep<TResource> : PgClassExpressionStep<PgCodec<string, undefined, any, any, any, any, any>, TResource> : never;
deduplicate(_peers: readonly Step[]): readonly Step<any>[];
optimize(): this;
execute(executionDetails: ExecutionDetails): readonly any[];
}
export declare function pgSelect<TResource extends PgResource<any, any, any, any, any>>(options: PgSelectOptions<TResource>): PgSelectStep<TResource>;
/**
* Turns a list of records (e.g. from PgSelectSingleStep.record()) back into a PgSelect.
*/
export declare function pgSelectFromRecords<TResource extends PgResource<any, any, any, any, any>>(resource: TResource, records: PgClassExpressionStep<PgCodec<any, undefined, any, any, GetPgResourceCodec<TResource>, any, any>, TResource> | Step<readonly any[]>): PgSelectStep<TResource>;
export declare function sqlFromArgDigests(digests: readonly PgSelectArgumentDigest[]): SQL;
export declare function pgFromExpression($target: {
getPgRoot(): PgRootStep;
}, baseFrom: PgSelectFromOption, inParameters?: readonly PgResourceParameter[] | undefined, specs?: ReadonlyArray<PgSelectArgumentSpec | PgSelectArgumentDigest>): SQL;
declare const $$generationCheck: unique symbol;
export declare function generatePgParameterAnalysis(parameters: readonly PgResourceParameter[]): {
/** DO NOT GENERATE THIS OBJECT YOURSELF! Use generateParameterAnalysis(parameters) */
[$$generationCheck]: readonly PgResourceParameter<string | null, PgCodec<string, {
[x: string]: import("../codecs.js").PgCodecAttribute<PgCodec<string, /*elided*/ any | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>, boolean>;
} | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>>[];
parameterByName: Record<string, PgResourceParameter<string | null, PgCodec<string, {
[x: string]: import("../codecs.js").PgCodecAttribute<PgCodec<string, /*elided*/ any | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>, boolean>;
} | undefined, any, any, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, any, any, any, any, any, any> | undefined, PgCodec<string, undefined, any, any, undefined, any, undefined> | undefined>>>;
indexAfterWhichAllArgsAreNamed: number;
};
export declare function pgFromExpressionRuntime(from: (...args: PgSelectArgumentDigest[]) => SQL, parameters: readonly PgResourceParameter[], digests: ReadonlyArray<PgSelectArgumentPlaceholder | PgSelectArgumentRuntimeValue>, parameterAnalysis?: ReturnType<typeof generatePgParameterAnalysis>): SQL;
export declare function getFragmentAndCodecFromOrder(alias: SQL, order: PgOrderSpec, codecOrCodecs: PgCodec | PgCodec[]): [fragment: SQL, codec: PgCodec, isNullable?: boolean];
interface PgSelectQueryInfo<TResource extends PgResource<any, any, any, any, any> = PgResource> extends PgStmtCommonQueryInfo, PgStmtCompileQueryInfo {
/** For debugging only */
readonly sourceStepDescription: string;
readonly name: string;
readonly resource: TResource;
readonly mode: PgSelectMode;
/** Are we fetching just one record? */
readonly isUnique: boolean;
readonly joinAsLateral: boolean;
/** Is the order that was established at planning time unique? */
readonly isOrderUnique: boolean;
readonly fixedPlaceholderValues: ReadonlyMap<symbol, SQL>;
readonly _symbolSubstitutes: ReadonlyMap<symbol, symbol>;
readonly needsGroups: boolean;
readonly selects: ReadonlyArray<SQL>;
readonly from: SQL;
readonly joins: ReadonlyArray<PgSelectPlanJoin>;
readonly conditions: ReadonlyArray<SQL>;
readonly orders: ReadonlyArray<PgOrderSpec>;
readonly relationJoins: ReadonlyMap<keyof GetPgResourceRelations<TResource>, SQL>;
readonly meta: {
readonly [key: string]: any;
};
readonly streamDetailsDepIds: null | readonly number[];
}
type CoreInfo<TResource extends PgResource<any, any, any, any, any>> = Readonly<Omit<PgSelectQueryInfo<TResource>, "placeholders" | "deferreds">>;
type StaticKeys = "sourceStepDescription" | "forceIdentity" | "havingConditions" | "mode" | "hasSideEffects" | "name" | "alias" | "symbol" | "resource" | "groups" | "orders" | "selects" | "fetchOneExtra" | "isOrderUnique" | "isUnique" | "conditions" | "from" | "joins" | "needsCursor" | "needsGroups" | "relationJoins" | "meta" | "placeholderSymbols" | "deferredSymbols" | "fixedPlaceholderValues" | "_symbolSubstitutes" | "joinAsLateral";
type StaticInfo<TResource extends PgResource<any, any, any, any, any>> = Pick<CoreInfo<TResource>, StaticKeys>;
export interface PgSelectQueryBuilder<TResource extends PgResource<any, any, any, any, any> = PgResource> extends PgQueryBuilder {
/** Instruct to add another order */
orderBy(spec: PgOrderSpec): void;
/** Inform that the resulting order is now unique */
setOrderIsUnique(): void;
/** Returns the SQL alias representing the table related to this relation */
singleRelation<TRelationName extends keyof GetPgResourceRelations<TResource> & string>(relationIdentifier: TRelationName): SQL;
where(condition: PgWhereConditionSpec<keyof GetPgResourceAttributes<TResource> & string>): void;
whereBuilder(): PgCondition<this>;
groupBy(group: PgGroupSpec): void;
having(condition: PgHavingConditionSpec<keyof GetPgResourceAttributes<TResource> & string>): void;
havingBuilder(): PgCondition<this>;
join(spec: PgSelectPlanJoin): void;
selectAndReturnIndex(fragment: SQL): number;
}
export {};
//# sourceMappingURL=pgSelect.d.ts.map