UNPKG

@dataplan/pg

Version:
1,138 lines (1,137 loc) 114 kB
"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(() => {