@dataplan/pg
Version:
PostgreSQL step classes for Grafast
1,138 lines (1,137 loc) • 114 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PgSelectRowsStep = exports.PgSelectStep = void 0;
exports.pgSelect = pgSelect;
exports.pgSelectFromRecords = pgSelectFromRecords;
exports.sqlFromArgDigests = sqlFromArgDigests;
exports.pgFromExpression = pgFromExpression;
exports.generatePgParameterAnalysis = generatePgParameterAnalysis;
exports.pgFromExpressionRuntime = pgFromExpressionRuntime;
exports.getFragmentAndCodecFromOrder = getFragmentAndCodecFromOrder;
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const debug_1 = tslib_1.__importDefault(require("debug"));
const grafast_1 = require("grafast");
const pg_sql2_1 = tslib_1.__importStar(require("pg-sql2"));
const codecs_js_1 = require("../codecs.js");
const parseArray_js_1 = require("../parseArray.js");
const pgLocker_js_1 = require("../pgLocker.js");
const pgClassExpression_js_1 = require("./pgClassExpression.js");
const pgCondition_js_1 = require("./pgCondition.js");
const pgCursor_js_1 = require("./pgCursor.js");
const pgSelectSingle_js_1 = require("./pgSelectSingle.js");
const pgStmt_js_1 = require("./pgStmt.js");
const pgValidateParsedCursor_js_1 = require("./pgValidateParsedCursor.js");
const ALWAYS_ALLOWED = true;
// Maximum identifier length in Postgres is 63 chars, so trim one off. (We
// could do base64... but meh.)
const hash = (text) => (0, crypto_1.createHash)("sha256").update(text).digest("hex").slice(0, 63);
const debugPlan = (0, debug_1.default)("@dataplan/pg:PgSelectStep:plan");
// const debugExecute = debugFactory("@dataplan/pg:PgSelectStep:execute");
const debugPlanVerbose = debugPlan.extend("verbose");
// const debugExecuteVerbose = debugExecute.extend("verbose");
const EMPTY_ARRAY = Object.freeze([]);
const NO_ROWS = Object.freeze({
hasNextPage: false,
hasPreviousPage: false,
items: [],
cursorDetails: undefined,
groupDetails: undefined,
m: Object.create(null),
});
function assertSensible(step) {
if (step instanceof PgSelectStep) {
throw new Error("You passed a PgSelectStep as an identifier, perhaps you forgot to add `.record()`?");
}
if (step instanceof pgSelectSingle_js_1.PgSelectSingleStep) {
throw new Error("You passed a PgSelectSingleStep as an identifier, perhaps you forgot to add `.record()`?");
}
}
/**
* 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.
*/
class PgSelectStep extends pgStmt_js_1.PgStmtBaseStep {
static { this.$$export = {
moduleName: "@dataplan/pg",
exportName: "PgSelectStep",
}; }
static clone(cloneFrom, mode = cloneFrom.mode) {
const cloneFromMatchingMode = cloneFrom?.mode === mode ? cloneFrom : null;
const $clone = new PgSelectStep({
identifiers: [], //We'll overwrite teh result of this in a moment
args: undefined, // We'll overwrite the result of this in a moment
context: cloneFrom.getDep(cloneFrom.contextId),
resource: cloneFrom.resource,
from: cloneFrom.from,
...(cloneFrom.hasImplicitOrder === false
? { hasImplicitOrder: cloneFrom.hasImplicitOrder }
: {}),
name: cloneFrom.name,
mode,
joinAsLateral: cloneFrom.joinAsLateral,
forceIdentity: cloneFrom.forceIdentity,
_internalCloneSymbol: cloneFrom.symbol,
_internalCloneAlias: cloneFrom.alias,
});
if ($clone.dependencies.length !== 1) {
throw new Error("Should not have any dependencies other than context yet");
}
cloneFrom.dependencies.forEach((planId, idx) => {
if (idx === 0)
return;
const myIdx = $clone.addDependency({
...cloneFrom.getDepOptions(idx),
skipDeduplication: true,
});
if (myIdx !== idx) {
throw new Error(`Failed to clone ${cloneFrom}; dependency indexes did not match: ${myIdx} !== ${idx}`);
}
});
$clone.applyDepIds = [...cloneFrom.applyDepIds];
$clone.isTrusted = cloneFrom.isTrusted;
// TODO: should `isUnique` only be set if mode matches?
$clone.isUnique = cloneFrom.isUnique;
$clone.isInliningForbidden = cloneFrom.isInliningForbidden;
for (const [k, v] of cloneFrom._symbolSubstitutes) {
$clone._symbolSubstitutes.set(k, v);
}
for (const v of cloneFrom.placeholders) {
$clone.placeholders.push(v);
}
for (const v of cloneFrom.deferreds) {
$clone.deferreds.push(v);
}
for (const [k, v] of cloneFrom.fixedPlaceholderValues) {
$clone.fixedPlaceholderValues.set(k, v);
}
for (const [k, v] of cloneFrom.relationJoins) {
$clone.relationJoins.set(k, v);
}
for (const v of cloneFrom.joins) {
$clone.joins.push(v);
}
for (const v of cloneFrom.conditions) {
$clone.conditions.push(v);
}
if (cloneFromMatchingMode) {
for (const v of cloneFromMatchingMode.selects) {
$clone.selects.push(v);
}
for (const v of cloneFromMatchingMode.groups) {
$clone.groups.push(v);
}
for (const v of cloneFromMatchingMode.havingConditions) {
$clone.havingConditions.push(v);
}
for (const v of cloneFromMatchingMode.orders) {
$clone.orders.push(v);
}
$clone.isOrderUnique = cloneFromMatchingMode.isOrderUnique;
$clone.firstStepId = cloneFromMatchingMode.firstStepId;
$clone.lastStepId = cloneFromMatchingMode.lastStepId;
$clone.fetchOneExtra = cloneFromMatchingMode.fetchOneExtra;
$clone.offsetStepId = cloneFromMatchingMode.offsetStepId;
// dependencies were already added, so we can just copy the dependency references
$clone.beforeStepId = cloneFromMatchingMode.beforeStepId;
$clone.afterStepId = cloneFromMatchingMode.afterStepId;
$clone.lowerIndexStepId = cloneFromMatchingMode.lowerIndexStepId;
$clone.upperIndexStepId = cloneFromMatchingMode.upperIndexStepId;
}
return $clone;
}
constructor(options) {
super();
this.isSyncAndSafe = false;
/**
* 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).
*/
this._symbolSubstitutes = new Map();
// JOIN
this.relationJoins = new Map();
this.joins = [];
// WHERE
this.conditions = [];
// GROUP BY
this.groups = [];
// HAVING
this.havingConditions = [];
// ORDER BY
this.orders = [];
this.isOrderUnique = false;
// LIMIT
this.firstStepId = null;
this.lastStepId = null;
this.fetchOneExtra = false;
/** When using natural pagination, this index is the lower bound (and should be excluded) */
this.lowerIndexStepId = null;
/** When using natural pagination, this index is the upper bound (and should be excluded) */
this.upperIndexStepId = null;
// OFFSET
this.offsetStepId = null;
// CURSORS
this.beforeStepId = null;
this.afterStepId = null;
// Connection
this.connectionDepId = null;
this.applyDepIds = [];
this.placeholders = [];
this.deferreds = [];
this.fixedPlaceholderValues = new Map();
/**
* If true, we don't need to add any of the security checks from the
* resource; otherwise we must do so. Default false.
*/
this.isTrusted = false;
/**
* 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.
*/
this.isUnique = false;
/**
* If true, we will not attempt to inline this into the parent query.
* Default false.
*/
this.isInliningForbidden = false;
/**
* The list of things we're selecting.
*/
this.selects = [];
this.locker = new pgLocker_js_1.PgLocker(this);
this._meta = Object.create(null);
/**
* Hints that **ARE NOT COMPARED FOR DEDUPLICATE** and so can be thrown away
* completely. Write stuff here at your own risk.
*
* @internal
* @experimental
*/
this.hints = Object.create(null);
this.nullCheckIndex = undefined;
this.needsGroups = false;
this.streamDetailsDepIds = [];
this._fieldMightStream = (0, grafast_1.currentFieldStreamDetails)() != null;
const { resource, parameters = resource.parameters, identifiers, args: inArgs, from: inFrom = null, hasImplicitOrder: inHasImplicitOrder, name, mode, joinAsLateral: inJoinAsLateral = false, forceIdentity: inForceIdentity = false, context: inContext,
// Clone only details
_internalCloneSymbol, _internalCloneAlias, } = options;
this.mode = mode ?? "normal";
this.resource = resource;
// Since we're applying this to the original it doesn't make sense to
// also apply it to the clones.
if (_internalCloneSymbol === undefined) {
if (this.mode === "aggregate") {
this.locker.beforeLock("orderBy", () => this.locker.lockParameter("groupBy"));
}
}
this.contextId = this.addUnaryDependency(inContext ?? resource.executor.context());
this.name = name ?? resource.name;
this.symbol = _internalCloneSymbol ?? Symbol(this.name);
this.alias = _internalCloneAlias ?? pg_sql2_1.default.identifier(this.symbol);
this.hasImplicitOrder = inHasImplicitOrder ?? resource.hasImplicitOrder;
this.joinAsLateral = inJoinAsLateral ?? !!this.resource.parameters;
this.forceIdentity = inForceIdentity;
{
if (!identifiers) {
throw new Error("Invalid construction of PgSelectStep");
}
identifiers.forEach((identifier) => {
if (grafast_1.isDev) {
assertSensible(identifier.step);
}
const { step, matches } = identifier;
const codec = identifier.codec || identifier.step.pgCodec;
const expression = matches(this.alias);
const placeholder = this.placeholder(step, codec);
this.where((0, pg_sql2_1.default) `${expression} = ${placeholder}`);
});
const ourFrom = inFrom ?? resource.from;
this.from = pgFromExpression(this, ourFrom, parameters, inArgs);
}
this.peerKey = this.resource.name;
// Must be the last thing to happen
this.hasSideEffects = this.mode === "mutation";
debugPlanVerbose(`%s (%s) constructor (%s)`, this, this.name, this.mode);
return this;
}
toStringMeta() {
return (this.name +
(this.fetchOneExtra ? "+1" : "") +
(this.mode === "normal" ? "" : `(${this.mode})`));
}
lock() {
this.locker.lock();
}
setInliningForbidden(newInliningForbidden = true) {
this.isInliningForbidden = newInliningForbidden;
return this;
}
inliningForbidden() {
return this.isInliningForbidden;
}
setTrusted(newIsTrusted = true) {
if (this.locker.locked) {
throw new Error(`${this}: cannot toggle trusted once plan is locked`);
}
this.isTrusted = newIsTrusted;
return this;
}
trusted() {
return this.isTrusted;
}
/**
* 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 = true) {
if (this.locker.locked) {
throw new Error(`${this}: cannot toggle unique once plan is locked`);
}
this.isUnique = newUnique;
return this;
}
unique() {
return this.isUnique;
}
/**
* Join to a named relationship and return the alias that can be used in
* SELECT, WHERE and ORDER BY.
*/
singleRelation(relationIdentifier) {
const relation = this.resource.getRelation(relationIdentifier);
if (!relation) {
throw new Error(`${this.resource} does not have a relation named '${String(relationIdentifier)}'`);
}
if (!relation.isUnique) {
throw new Error(`${this.resource} relation '${String(relationIdentifier)}' is not unique so cannot be used with singleRelation`);
}
const { remoteResource, localAttributes, remoteAttributes } = relation;
// Join to this relation if we haven't already
const cachedAlias = this.relationJoins.get(relationIdentifier);
if (cachedAlias) {
return cachedAlias;
}
const alias = pg_sql2_1.default.identifier(Symbol(relationIdentifier));
if (typeof remoteResource.from === "function") {
throw new Error("Callback sources not currently supported via singleRelation");
}
this.joins.push({
type: "left",
from: remoteResource.from,
alias,
conditions: localAttributes.map((col, i) => (0, pg_sql2_1.default) `${this.alias}.${pg_sql2_1.default.identifier(col)} = ${alias}.${pg_sql2_1.default.identifier(remoteAttributes[i])}`),
});
this.relationJoins.set(relationIdentifier, alias);
return alias;
}
/**
* @experimental Please use `singleRelation` or `manyRelation` instead.
*/
join(spec) {
this.joins.push(this.scopedSQL(spec));
}
getMeta(key) {
return (0, grafast_1.access)(this, ["m", key]);
}
/**
* Select an SQL fragment, returning the index the result will have.
*
* @internal
*/
selectAndReturnIndex(fragmentOrCb) {
const fragment = this.scopedSQL(fragmentOrCb);
if (!this.isArgumentsFinalized) {
throw new Error("Select added before arguments were finalized");
}
// NOTE: it's okay to add selections after the plan is "locked" - lock only
// applies to which rows are being selected, not what is being queried
// about the rows.
// Optimisation: if we're already selecting this fragment, return the existing one.
const options = {
symbolSubstitutes: this._symbolSubstitutes,
};
// PERF: performance of this sucks at planning time
const index = this.selects.findIndex((frag) => pg_sql2_1.default.isEquivalent(frag, fragment, options));
if (index >= 0) {
return index;
}
return this.selects.push(fragment) - 1;
}
/** @internal */
getNullCheckIndex() {
// PERF: if this isn't coming from a function _and_ it's not being inlined
// via a left-join or similar then we shouldn't need this and should be
// able to drop it.
if (this.nullCheckIndex !== undefined) {
return this.nullCheckIndex;
}
const nullCheckExpression = this.resource.getNullCheckExpression(this.alias);
if (nullCheckExpression) {
this.nullCheckIndex = this.selectAndReturnIndex(nullCheckExpression);
}
else {
this.nullCheckIndex = null;
}
return this.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) {
// Prevent any changes to our original to help avoid programming
// errors.
this.lock();
return PgSelectStep.clone(this, mode);
}
connectionClone(mode) {
return this.clone(mode);
}
where(rawCondition) {
if (this.locker.locked) {
throw new Error(`${this}: cannot add conditions once plan is locked ('where')`);
}
const condition = this.scopedSQL(rawCondition);
if (pg_sql2_1.default.isSQL(condition)) {
this.conditions.push(condition);
}
else {
switch (condition.type) {
case "attribute": {
this.conditions.push(this.scopedSQL((sql) => condition.callback(sql `${this.alias}.${sql.identifier(condition.attribute)}`)));
break;
}
default: {
const never = condition.type;
console.error("Unsupported condition: ", never);
throw new Error(`Unsupported condition`);
}
}
}
}
groupBy(group) {
this.locker.assertParameterUnlocked("groupBy");
if (this.mode !== "aggregate") {
throw new grafast_1.SafeError(`Cannot add groupBy to a non-aggregate query`);
}
this.groups.push(this.scopedSQL(group));
}
having(rawCondition) {
if (this.locker.locked) {
throw new Error(`${this}: cannot add having conditions once plan is locked ('having')`);
}
if (this.mode !== "aggregate") {
throw new grafast_1.SafeError(`Cannot add having to a non-aggregate query`);
}
const condition = this.scopedSQL(rawCondition);
if (pg_sql2_1.default.isSQL(condition)) {
this.havingConditions.push(condition);
}
else {
const never = condition;
console.error("Unsupported condition: ", never);
throw new Error(`Unsupported condition`);
}
}
orderBy(order) {
this.locker.assertParameterUnlocked("orderBy");
this.orders.push(this.scopedSQL(order));
}
setOrderIsUnique() {
if (this.locker.locked) {
throw new Error(`${this}: cannot set order unique once plan is locked`);
}
this.isOrderUnique = true;
}
apply($step) {
if ($step instanceof grafast_1.ConstantStep) {
$step.data(this);
}
else {
this.applyDepIds.push(this.addUnaryDependency($step));
}
}
assertCursorPaginationAllowed() {
if (this.mode === "aggregate") {
throw new grafast_1.SafeError("Cannot use cursor pagination on an aggregate PgSelectStep");
}
}
items() {
return this.withMyLayerPlan(() => this.operationPlan.cacheStep(this, "items", "" /* Digest of our arguments */, () => new PgSelectRowsStep(this)));
}
getCursorDetails() {
this.needsCursor = true;
return (0, grafast_1.access)(this, "cursorDetails");
}
/**
* 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) {
return new pgCursor_js_1.PgCursorStep($item, this.getCursorDetails());
}
getGroupDetails() {
this.needsGroups = true;
return (0, grafast_1.access)(this, "groupDetails");
}
/**
* `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.
*/
async execute(executionDetails) {
const { indexMap, count, values, extra: { eventEmitter }, } = executionDetails;
const { meta, text, rawSqlValues, textForDeclare, rawSqlValuesForDeclare, identifierIndex, name, streamInitialCount, queryValues, shouldReverseOrder, first, last, offset, cursorDetails, groupDetails, } = buildTheQuery({
executionDetails,
// Stuff directly referencing dependency IDs
firstStepId: this.firstStepId,
lastStepId: this.lastStepId,
offsetStepId: this.offsetStepId,
afterStepId: this.afterStepId,
beforeStepId: this.beforeStepId,
applyDepIds: this.applyDepIds,
streamDetailsDepIds: this.streamDetailsDepIds,
// Stuff referencing dependency IDs in a nested fashion
placeholders: this.placeholders,
deferreds: this.deferreds,
// Fixed stuff that is local to us (aka "StaticInfo")
...PgSelectStep.getStaticInfo(this),
});
if (first === 0 || last === 0) {
return (0, grafast_1.arrayOfLength)(count, NO_ROWS);
}
const context = values[this.contextId].unaryValue();
if (streamInitialCount == null) {
const specs = indexMap((i) => {
return {
// The context is how we'd handle different connections with different claims
context,
queryValues: identifierIndex != null
? queryValues.map(({ dependencyIndex, codec }) => {
const val = values[dependencyIndex].at(i);
return val == null ? null : codec.toPg(val);
})
: EMPTY_ARRAY,
};
});
const executeMethod = this.operationPlan.operation.operation === "query"
? "executeWithCache"
: "executeWithoutCache";
const executionResult = await this.resource[executeMethod](specs, {
text,
rawSqlValues,
identifierIndex,
name,
eventEmitter,
useTransaction: this.mode === "mutation",
});
// debugExecute("%s; result: %c", this, executionResult);
return executionResult.values.map((allVals) => {
if ((0, grafast_1.isPromiseLike)(allVals)) {
// Must be an error
return allVals;
}
return createSelectResult(allVals, {
first,
last,
offset,
fetchOneExtra: this.fetchOneExtra,
shouldReverseOrder,
meta,
cursorDetails,
groupDetails,
});
});
}
else {
if (shouldReverseOrder !== false) {
throw new Error("shouldReverseOrder must be false for stream");
}
if (!rawSqlValuesForDeclare || !textForDeclare) {
throw new Error("declare query must exist for stream");
}
let specs = null;
if (text) {
specs = indexMap((i) => {
return {
// The context is how we'd handle different connections with different claims
context,
queryValues: identifierIndex != null
? queryValues.map(({ dependencyIndex, codec }) => {
const val = values[dependencyIndex].at(i);
return val == null ? null : codec.toPg(val);
})
: EMPTY_ARRAY,
};
});
}
const initialFetchResult = specs
? (await this.resource.executeWithoutCache(specs, {
text,
rawSqlValues,
identifierIndex,
eventEmitter,
})).values
: null;
const streamSpecs = indexMap((i) => {
return {
// The context is how we'd handle different connections with different claims
context,
queryValues: identifierIndex != null
? queryValues.map(({ dependencyIndex, codec }) => {
const val = values[dependencyIndex].at(i);
return val == null ? val : codec.toPg(val);
})
: EMPTY_ARRAY,
};
});
const streams = (await this.resource.executeStream(streamSpecs, {
text: textForDeclare,
rawSqlValues: rawSqlValuesForDeclare,
identifierIndex,
eventEmitter,
})).streams;
return streams.map((iterable, idx) => {
if (!(0, grafast_1.isAsyncIterable)(iterable)) {
// Must be an error
return iterable;
}
if (!initialFetchResult) {
return {
hasNextPage: false,
hasPreviousPage: false,
items: iterable,
cursorDetails,
groupDetails,
m: meta,
};
}
// Munge the initialCount records into the streams
const innerIterator = iterable[Symbol.asyncIterator]();
let i = 0;
let done = false;
const l = initialFetchResult[idx].length;
const mergedGenerator = {
async [Symbol.asyncDispose]() {
await this.return(undefined);
},
next() {
if (done) {
return Promise.resolve({ value: undefined, done });
}
else if (i < l) {
return Promise.resolve({
value: initialFetchResult[idx][i++],
done,
});
}
else if (streamInitialCount != null && l < streamInitialCount) {
done = true;
innerIterator.return?.();
return Promise.resolve({ value: undefined, done });
}
else {
return innerIterator.next();
}
},
return(value) {
done = true;
return (innerIterator.return?.(value) ??
Promise.resolve({ value: undefined, done }));
},
throw(e) {
done = true;
return (innerIterator.throw?.(e) ??
Promise.resolve({ value: undefined, done }));
},
[Symbol.asyncIterator]() {
return this;
},
};
return {
hasNextPage: false,
hasPreviousPage: false,
items: mergedGenerator,
cursorDetails,
groupDetails,
m: meta,
};
});
}
}
finalize() {
// In case we have any lock actions in future:
this.lock();
// Now we need to be able to mess with ourself, but be sure to lock again
// at the end.
this.locker.locked = false;
this.locker.locked = true;
super.finalize();
}
deduplicate(peers) {
if (!this.isTrusted) {
this.resource.applyAuthorizationChecksToPlan(this);
this.isTrusted = true;
}
this.locker.lockAllParameters();
return peers.filter(($p) => {
if ($p === this) {
return true;
}
const p = $p;
// If SELECT, FROM, JOIN, WHERE, ORDER, GROUP BY, HAVING, LIMIT, OFFSET
// all match with one of our peers then we can replace ourself with one
// of our peers. NOTE: we do _not_ merge SELECTs at this stage because
// that would require mapping, and mapping should not be done during
// deduplicate because it would interfere with optimize. So, instead,
// we try to ensure that as few selects as possible exist in the plan
// at this stage.
// Check FROM matches
if (p.resource !== this.resource) {
return false;
}
// Check mode matches
if (p.mode !== this.mode) {
return false;
}
// Since deduplicate runs before we have children, we do not need to
// check the symbol or alias matches. We do need to factor the different
// symbols into SQL equivalency checks though.
const symbolSubstitutes = new Map();
const options = { symbolSubstitutes };
if (typeof this.symbol === "symbol" && typeof p.symbol === "symbol") {
if (this.symbol !== p.symbol) {
symbolSubstitutes.set(this.symbol, p.symbol);
}
else {
// Fine :)
}
}
else if (this.symbol !== p.symbol) {
return false;
}
// Check PLACEHOLDERS match
if (!(0, pg_sql2_1.arraysMatch)(this.placeholders, p.placeholders, (a, b) => {
const equivalent = a.codec === b.codec && a.dependencyIndex === b.dependencyIndex;
if (equivalent) {
if (a.symbol !== b.symbol) {
// Make symbols appear equivalent
symbolSubstitutes.set(a.symbol, b.symbol);
}
}
return equivalent;
})) {
debugPlanVerbose("Refusing to deduplicate %c with %c because the placeholders don't match", this, p);
return false;
}
// Check DEFERREDs match
if (!(0, pg_sql2_1.arraysMatch)(this.deferreds, p.deferreds, (a, b) => {
const equivalent = a.dependencyIndex === b.dependencyIndex;
if (equivalent) {
if (a.symbol !== b.symbol) {
// Make symbols appear equivalent
symbolSubstitutes.set(a.symbol, b.symbol);
}
}
return equivalent;
})) {
debugPlanVerbose("Refusing to deduplicate %c with %c because the deferreds don't match", this, p);
return false;
}
const sqlIsEquivalent = (a, b) => pg_sql2_1.default.isEquivalent(a, b, options);
// Check trusted matches
if (p.trusted !== this.trusted) {
return false;
}
// Check inliningForbidden matches
if (p.inliningForbidden !== this.inliningForbidden) {
return false;
}
// Check FROM
if (!sqlIsEquivalent(p.from, this.from)) {
return false;
}
// Check SELECT matches
if (!(0, pg_sql2_1.arraysMatch)(this.selects, p.selects, sqlIsEquivalent)) {
return false;
}
// Check GROUPs match
if (!(0, pg_sql2_1.arraysMatch)(this.groups, p.groups, (a, b) => sqlIsEquivalent(a.fragment, b.fragment))) {
return false;
}
// Check HAVINGs match
if (!(0, pg_sql2_1.arraysMatch)(this.havingConditions, p.havingConditions, sqlIsEquivalent)) {
return false;
}
// Check ORDERs match
if (!(0, pg_sql2_1.arraysMatch)(this.orders, p.orders, (a, b) => {
if (a.direction !== b.direction)
return false;
if (a.nulls !== b.nulls)
return false;
if (a.attribute != null) {
if (b.attribute !== a.attribute)
return false;
// ENHANCEMENT: really should compare if the result is equivalent?
return a.callback === b.callback;
}
else {
if (b.attribute != null)
return false;
return sqlIsEquivalent(a.fragment, b.fragment);
}
})) {
return false;
}
const depsMatch = (myDepId, theirDepId) => this.maybeGetDep(myDepId) === p.maybeGetDep(theirDepId);
// Check LIMIT, OFFSET and CURSOR matches
if (!depsMatch(this.beforeStepId, p.beforeStepId) ||
!depsMatch(this.afterStepId, p.afterStepId) ||
!depsMatch(this.firstStepId, p.firstStepId) ||
!depsMatch(this.lastStepId, p.lastStepId) ||
!depsMatch(this.offsetStepId, p.offsetStepId) ||
!depsMatch(this.lowerIndexStepId, p.lowerIndexStepId) ||
!depsMatch(this.upperIndexStepId, p.upperIndexStepId)) {
return false;
}
if (!(0, grafast_1.maybeArraysMatch)(this.streamDetailsDepIds, p.streamDetailsDepIds)) {
return false;
}
// Check JOINs match
if (!(0, pg_sql2_1.arraysMatch)(this.joins, p.joins, (a, b) => joinMatches(a, b, sqlIsEquivalent))) {
debugPlanVerbose("Refusing to deduplicate %c with %c because the joins don't match", this, p);
return false;
}
// Check WHEREs match
if (!(0, pg_sql2_1.arraysMatch)(this.conditions, p.conditions, sqlIsEquivalent)) {
debugPlanVerbose("Refusing to deduplicate %c with %c because the conditions don't match", this, p);
return false;
}
debugPlanVerbose("Found that %c and %c are equivalent!", this, p);
return true;
});
}
/** @internal */
deduplicatedWith(replacement) {
if (typeof this.symbol === "symbol" &&
typeof replacement.symbol === "symbol") {
if (this.symbol !== replacement.symbol) {
replacement._symbolSubstitutes.set(this.symbol, replacement.symbol);
}
else {
// Fine :)
}
}
if (this.fetchOneExtra) {
replacement.fetchOneExtra = true;
}
if (this.needsCursor) {
replacement.needsCursor = true;
}
}
getParentForInlining() {
/**
* These are the dependencies that are not PgClassExpressionSteps, we just
* need them to be at a higher level than $pgSelect
*/
const otherDeps = [];
/**
* This is the PgSelectStep that we would like to try and inline ourself
* into. If `undefined`, this hasn't been found yet. If `null`, this has
* been explicitly forbidden due to a mismatch of some kind.
*/
let $pgSelect = undefined;
/**
* This is the pgSelectSingle representing a single record from $pgSelect,
* it's used when remapping of keys is required after inlining ourself into
* $pgSelect.
*/
let $pgSelectSingle = undefined;
// Scan through the dependencies to find a suitable ancestor step to merge with
for (let dependencyIndex = 0, l = this.dependencies.length; dependencyIndex < l; dependencyIndex++) {
if (dependencyIndex === this.contextId) {
// We check myContext vs tsContext below; so lets assume it's fine
// for now.
continue;
}
const depOptions = this.getDepOptions(dependencyIndex);
let $dep = depOptions.step;
if (depOptions.acceptFlags !== grafast_1.DEFAULT_ACCEPT_FLAGS ||
depOptions.onReject != null) {
console.info(`Forbidding inlining of ${$pgSelect} due to dependency ${dependencyIndex}/${$dep} having custom flags`);
// Forbid inlining
return null;
}
if ($dep instanceof PgFromExpressionStep) {
const digest0 = $dep.getDigest(0);
if (digest0?.step && digest0.step instanceof pgClassExpression_js_1.PgClassExpressionStep) {
$dep = digest0.step;
}
}
if ($dep instanceof pgClassExpression_js_1.PgClassExpressionStep) {
const $depPgSelectSingle = $dep.getParentStep();
if (!($depPgSelectSingle instanceof pgSelectSingle_js_1.PgSelectSingleStep)) {
continue;
}
const $depPgSelect = $depPgSelectSingle.getClassStep();
if ($depPgSelect === this) {
throw new Error(`Recursion error - record plan ${$dep} is dependent on ${$depPgSelect}, and ${this} is dependent on ${$dep}`);
}
if ($depPgSelect.hasSideEffects) {
// It's a mutation; don't merge
continue;
}
// Don't allow merging across a stream/defer/subscription boundary
if (!(0, grafast_1.stepAShouldTryAndInlineIntoStepB)(this, $depPgSelect)) {
continue;
}
// Don't want to make this a join as it can result in the order being
// messed up
if ($depPgSelect.hasImplicitOrder &&
!this.joinAsLateral &&
this.isUnique) {
continue;
}
/*
if (!planGroupsOverlap(this, t2)) {
// We're not in the same group (i.e. there's probably a @defer or
// @stream between us) - do not merge.
continue;
}
*/
if ($pgSelect === undefined && $pgSelectSingle === undefined) {
$pgSelectSingle = $depPgSelectSingle;
$pgSelect = $depPgSelect;
}
else if ($depPgSelect !== $pgSelect) {
debugPlanVerbose("Refusing to optimise %c due to dependency %c depending on different class (%c != %c)", this, $dep, $depPgSelect, $pgSelect);
$pgSelect = null;
break;
}
else if ($depPgSelectSingle !== $pgSelectSingle) {
debugPlanVerbose("Refusing to optimise %c due to parent dependency mismatch: %c != %c", this, $depPgSelectSingle, $pgSelectSingle);
$pgSelect = null;
break;
}
}
else {
otherDeps.push($dep);
}
}
if ($pgSelect != null && $pgSelect.mode === "mutation") {
// ABORT! Unsafe!
$pgSelect = undefined;
$pgSelectSingle = undefined;
}
// Check the contexts are the same
if ($pgSelect != null && $pgSelectSingle != null) {
const myContext = this.getDep(this.contextId);
const tsContext = $pgSelect.getDep($pgSelect.contextId);
if (myContext !== tsContext) {
debugPlanVerbose("Refusing to optimise %c due to own context dependency %c differing from tables context dependency %c (%c, %c)", this, myContext, tsContext, $pgSelect.dependencies[$pgSelect.contextId], $pgSelect);
$pgSelect = null;
}
}
// Check the dependencies can be moved across to `t`
if ($pgSelect != null && $pgSelectSingle != null) {
for (const dep of otherDeps) {
if ($pgSelect.canAddDependency(dep)) {
// All good; just move the dependency over
}
else {
debugPlanVerbose("Refusing to optimise %c due to dependency %c which cannot be added as a dependency of %c", this, dep, $pgSelect);
$pgSelect = null;
break;
}
}
}
if ($pgSelect != null && $pgSelectSingle != null) {
// Looks feasible.
if ($pgSelect.id === this.id) {
throw new Error(`Something's gone catastrophically wrong - ${this} is trying to merge with itself!`);
}
return { $pgSelect, $pgSelectSingle };
}
else {
return null;
}
}
mergeSelectsWith(otherPlan) {
const actualKeyByDesiredKey = Object.create(null);
this.selects.forEach((frag, idx) => {
actualKeyByDesiredKey[idx] = otherPlan.selectAndReturnIndex(frag);
});
return actualKeyByDesiredKey;
}
/**
* - Merge placeholders
* - Merge fixedPlaceholders
* - Merge deferreds
* - Merge _symbolSubstitutes
*/
mergePlaceholdersInto($target) {
for (const placeholder of this.placeholders) {
const { dependencyIndex, symbol, codec, alreadyEncoded } = placeholder;
const depOptions = this.getDepOptions(dependencyIndex);
const $dep = depOptions.step;
/*
* We have dependency `dep`. We're attempting to merge ourself into
* `otherPlan`. We have two situations we need to handle:
*
* 1. `dep` is not dependent on `otherPlan`, in which case we can add
* `dep` as a dependency to `otherPlan` without creating a cycle, or
* 2. `dep` is dependent on `otherPlan` (for example, it might be the
* result of selecting an expression in the `otherPlan`), in which
* case we should turn it into an SQL expression and inline that.
*/
// PERF: we know dep can't depend on otherPlan if
// `isStaticInputStep(dep)` or `dep`'s layerPlan is an ancestor of
// `otherPlan`'s layerPlan.
if ((0, grafast_1.stepAMayDependOnStepB)($target, $dep)) {
// Either dep is a static input plan (which isn't dependent on anything
// else) or otherPlan is deeper than dep; either way we can use the dep
// directly within otherPlan.
const newPlanIndex = $target.addStrongDependency(depOptions);
$target.placeholders.push({
dependencyIndex: newPlanIndex,
codec,
symbol,
alreadyEncoded,
});
}
else if ($dep instanceof pgClassExpression_js_1.PgClassExpressionStep) {
// Replace with a reference.
$target.fixedPlaceholderValues.set(placeholder.symbol, $dep.toSQL());
}
else {
throw new Error(`Could not merge placeholder from unsupported plan type: ${$dep}`);
}
}
for (const [sqlPlaceholder, placeholderValue,] of this.fixedPlaceholderValues.entries()) {
if ($target.fixedPlaceholderValues.has(sqlPlaceholder) &&
$target.fixedPlaceholderValues.get(sqlPlaceholder) !== placeholderValue) {
throw new Error(`${$target} already has an identical placeholder with a different value when trying to mergePlaceholdersInto it from ${this}`);
}
$target.fixedPlaceholderValues.set(sqlPlaceholder, placeholderValue);
}
for (const { symbol, dependencyIndex } of this.deferreds) {
const depOptions = this.getDepOptions(dependencyIndex);
const $dep = depOptions.step;
if ((0, grafast_1.stepAMayDependOnStepB)($target, $dep)) {
const newPlanIndex = $target.addStrongDependency(depOptions);
$target.deferreds.push({
dependencyIndex: newPlanIndex,
symbol,
});
}
else if ($dep instanceof PgFromExpressionStep) {
const $newDep = $target.withLayerPlan(() => $dep.inlineInto($target));
const newPlanIndex = $target.addStrongDependency($newDep);
$target.deferreds.push({
dependencyIndex: newPlanIndex,
symbol,
});
}
else {
throw new Error(`Could not merge placeholder from unsupported plan type: ${$dep}`);
}
}
for (const [a, b] of this._symbolSubstitutes.entries()) {
if (grafast_1.isDev) {
if ($target._symbolSubstitutes.has(a) &&
$target._symbolSubstitutes.get(a) !== b) {
throw new Error(`Conflict when setting a substitute whilst merging ${this} into ${$target}; symbol already has a substitute, and it's different.`);
}
}
$target._symbolSubstitutes.set(a, b);
}
}
addStreamDetails($details) {
if ($details) {
this.streamDetailsDepIds?.push(this.addUnaryDependency($details));
}
else {
// Explicitly disable streaming
this.streamDetailsDepIds = null;
}
}
mightHaveStream() {
if (this._fieldMightStream)
return true;
if (this.streamDetailsDepIds != null && this.streamDetailsDepIds.length > 0)
return true;
return false;
}
optimize(options) {
const mightHaveStream = this.mightHaveStream();
if (options.stream && !mightHaveStream) {
throw new Error(`Inconsistency: we didn't think we might have a stream, but optimize says we might!`);
}
// In case we have any lock actions in future:
this.lock();
// Inline ourself into our parent if we can.
let parentDetails;
if (!this.isInliningForbidden &&
!this.hasSideEffects &&
!mightHaveStream &&
!this.joins.some((j) => j.type !== "left") &&
(parentDetails = this.getParentForInlining()) !== null &&
parentDetails.$pgSelect.mode === "normal") {
const { $pgSelect, $pgSelectSingle } = parentDetails;
if (this.mode === "normal" &&
this.isUnique &&
this.firstStepId == null &&
this.lastStepId == null &&
this.offsetStepId == null &&
// For uniques these should all pass anyway, but pays to be cautious..
this.groups.length === 0 &&
this.havingConditions.length === 0 &&
this.orders.length === 0 &&
!this.fetchOneExtra) {
// Allow, do it via left join
debugPlanVerbose("Merging %c into %c (via %c)", this, $pgSelect, $pgSelectSingle);
const recordOf = this.hints.isPgSelectFromRecordOf;
// TODO: the logic around this should move inside PgSelectInlineApplyStep instead.
let skipJoin = false;
if (typeof this.symbol === "symbol" &&
recordOf &&
recordOf.parentId === $pgSelect.id) {
const symbol = pg_sql2_1.default.getIdentifierSymbol(recordOf.expression);
if (symbol) {
if (pg_sql2_1.default.isEquivalent($pgSelect.alias, recordOf.expression)) {
skipJoin = true;
$pgSelect._symbolSubstitutes.set(this.symbol, symbol);
}
else {
const j = $pgSelect.joins.find((j) => pg_sql2_1.default.isEquivalent(j.alias, recordOf.expression));
if (j) {
const jSymbol = pg_sql2_1.default.getIdentifierSymbol(j.alias);
if (jSymbol) {
skipJoin = true;
$pgSelect._symbolSubstitutes.set(jSymbol, symbol);
}
}
}
}
}
this.mergePlaceholdersInto($pgSelect);
const identifier = `joinDetailsFor${this.id}`;
$pgSelect.withLayerPlan(() => {