@dataplan/pg
Version:
PostgreSQL step classes for Grafast
378 lines • 16.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PgSelectSingleStep = void 0;
exports.pgSelectFromRecord = pgSelectFromRecord;
exports.pgSelectSingleFromRecord = pgSelectSingleFromRecord;
const tslib_1 = require("tslib");
const grafast_1 = require("grafast");
const pg_sql2_1 = tslib_1.__importStar(require("pg-sql2"));
const codecs_js_1 = require("../codecs.js");
const utils_js_1 = require("../utils.js");
const pgClassExpression_js_1 = require("./pgClassExpression.js");
const pgSelect_js_1 = require("./pgSelect.js");
const EMPTY_TUPLE = Object.freeze([]);
// Types that only take a few bytes so adding them to the selection would be
// cheap to do.
const CHEAP_ATTRIBUTE_TYPES = new Set([
codecs_js_1.TYPES.int2,
codecs_js_1.TYPES.int,
codecs_js_1.TYPES.bigint,
codecs_js_1.TYPES.float,
codecs_js_1.TYPES.float4,
codecs_js_1.TYPES.uuid,
codecs_js_1.TYPES.boolean,
codecs_js_1.TYPES.date,
codecs_js_1.TYPES.timestamp,
codecs_js_1.TYPES.timestamptz,
]);
/**
* Represents the single result of a unique PgSelectStep. This might be
* retrieved explicitly by PgSelectStep.single(), or implicitly (via Grafast)
* by PgSelectStep.item(). Since this is the result of a fetch it does not make
* sense to support changing `.where` or similar; however we now add methods
* such as `.get` and `.cursor` which can receive specific properties by
* telling the PgSelectStep to select the relevant expressions.
*/
class PgSelectSingleStep extends grafast_1.UnbatchedStep {
static { this.$$export = {
moduleName: "@dataplan/pg",
exportName: "PgSelectSingleStep",
}; }
constructor($class, $item, options = Object.create(null)) {
super();
this.isSyncAndSafe = true;
this.nullCheckId = null;
this._coalesceToEmptyObject = false;
this.fromRelation = null;
this.scopedSQL = (0, utils_js_1.makeScopedSQL)(this);
this.nonNullAttribute = null;
this.nullCheckAttributeIndex = null;
this.itemStepId = this.addDependency($item);
this.resource = $class.resource;
this.pgCodec = this.resource.codec;
this.mode = $class.mode;
this.classStepId = $class.id;
this.peerKey = this.resource.name;
if (options.fromRelation) {
const [$pgSelectSingle, relationName] = options.fromRelation;
this.fromRelation = {
refId: this.addRef($pgSelectSingle, "Indirect reference allowed due to relational field potentially pulling from a parent relation"),
relationName,
};
}
}
coalesceToEmptyObject() {
this._coalesceToEmptyObject = true;
}
toStringMeta() {
return this.resource.name;
}
getClassStep() {
if (this.isOptimized) {
throw new Error(`Cannot ${this}.getClassStep() after we're optimized.`);
}
const plan = this.getStep(this.classStepId);
if (!(plan instanceof pgSelect_js_1.PgSelectStep)) {
throw new Error(`Expected ${this.classStepId} (${plan}) to be a PgSelectStep`);
}
return plan;
}
/** @internal */
getItemStep() {
const plan = this.getDep(this.itemStepId);
return plan;
}
/**
* Do not rely on this, we're going to refactor it to work a different way at some point.
*
* @internal
*/
getSelfNamed() {
if (this.mode === "aggregate") {
throw new Error("Invalid call to getSelfNamed on aggregate plan");
}
// Hack because I don't want to duplicate the code.
return this.get("");
}
/**
* Returns a plan representing a named attribute (e.g. column) from the class
* (e.g. table).
*/
get(attr) {
return this.cacheStep("get", attr, () => this._getInternal(attr));
}
_getInternal(attr) {
if (this.mode === "aggregate") {
throw new Error("Invalid call to .get() on aggregate plan");
}
if (!this.resource.codec.attributes && attr !== "") {
throw new Error(`Cannot call ${this}.get() when the resource codec (${this.resource.codec.name}) has no attributes to get.`);
}
const classPlan = this.getClassStep();
const resourceAttribute = this.resource.codec.attributes?.[attr];
if (!resourceAttribute && attr !== "") {
throw new Error(`${this.resource} does not define an attribute named '${String(attr)}'`);
}
if (resourceAttribute?.via) {
const { relationName, attributeName } = this.resource.resolveVia(resourceAttribute.via, attr);
return this.singleRelation(relationName).get(attributeName);
}
if (resourceAttribute?.identicalVia) {
const { relationName, attributeName } = this.resource.resolveVia(resourceAttribute.identicalVia, attr);
const $existingPlan = this.existingSingleRelation(relationName);
if ($existingPlan) {
// Relation exists already; load it from there for efficiency
return $existingPlan.get(attributeName);
}
else {
// Load it from ourself instead
}
}
if (this.fromRelation) {
const { refId, relationName: fromRelationName } = this.fromRelation;
const $fromPlan = this.getRef(refId);
if ($fromPlan instanceof PgSelectSingleStep) {
const matchingAttribute = Object.entries($fromPlan.resource.codec.attributes).find(([name, col]) => {
if (col.identicalVia) {
const { relationName, attributeName } = $fromPlan.resource.resolveVia(col.identicalVia, name);
if (attributeName === attr && relationName === fromRelationName) {
return true;
}
}
return false;
});
if (matchingAttribute) {
return $fromPlan.get(matchingAttribute[0]);
}
}
}
/*
* Only cast to `::text` during select; we want to use it uncasted in
* conditions/etc. The reasons we cast to ::text include:
*
* - to make return values consistent whether they're direct or in nested
* arrays
* - to make sure that that various PostgreSQL clients we support do not
* mangle the data in unexpected ways - we take responsibility for
* decoding these string values.
*/
const sqlExpr = (0, pgClassExpression_js_1.pgClassExpression)(this, attr === ""
? this.resource.codec
: this.resource.codec.attributes[attr].codec, resourceAttribute?.notNull);
const colPlan = resourceAttribute
? resourceAttribute.expression
? sqlExpr `${pg_sql2_1.default.parens(resourceAttribute.expression(classPlan.alias))}`
: sqlExpr `${classPlan.alias}.${pg_sql2_1.default.identifier(String(attr))}`
: sqlExpr `${classPlan.alias}.v`; /* single attribute */
if (this.nonNullAttribute == null &&
typeof attr === "string" &&
attr.length > 0 &&
resourceAttribute &&
!resourceAttribute.expression &&
resourceAttribute.notNull) {
// We know the row is null iff this attribute is null
this.nonNullAttribute = { attribute: resourceAttribute, attr };
}
return colPlan;
}
getMeta(key) {
return this.getClassStep().getMeta(key);
}
/**
* Returns a plan representing the result of an expression.
*/
select(fragment, codec, guaranteedNotNull) {
const sqlExpr = (0, pgClassExpression_js_1.pgClassExpression)(this, codec, guaranteedNotNull);
return sqlExpr `${this.scopedSQL(fragment)}`;
}
/**
* Advanced method; rather than returning a plan it returns an index.
* Generally useful for PgClassExpressionStep.
*
* @internal
*/
selectAndReturnIndex(fragment) {
return this.getClassStep().selectAndReturnIndex(this.scopedSQL(fragment));
}
getPgRoot() {
return this.getClassStep();
}
placeholder($step, overrideCodec) {
return overrideCodec
? this.getClassStep().placeholder($step, overrideCodec)
: this.getClassStep().placeholder($step);
}
deferredSQL($step) {
return this.getClassStep().deferredSQL($step);
}
existingSingleRelation(relationIdentifier) {
if (this.fromRelation) {
const { refId, relationName } = this.fromRelation;
const $fromPlan = this.getRef(refId);
if ($fromPlan instanceof PgSelectSingleStep) {
// check to see if we already came via this relationship
const reciprocal = this.resource.getReciprocal($fromPlan.resource.codec, relationName);
if (reciprocal) {
const reciprocalRelationName = reciprocal[0];
if (reciprocalRelationName === relationIdentifier) {
const reciprocalRelation = reciprocal[1];
if (reciprocalRelation.isUnique) {
return $fromPlan;
}
}
}
}
}
return null;
}
singleRelation(relationIdentifier) {
const $existingPlan = this.existingSingleRelation(relationIdentifier);
if ($existingPlan) {
return $existingPlan;
}
const relation = this.resource.getRelation(relationIdentifier);
if (!relation || !relation.isUnique) {
throw new Error(`${String(relationIdentifier)} is not a unique relation on ${this.resource}`);
}
const { remoteResource, remoteAttributes, localAttributes } = relation;
const options = {
fromRelation: [
this,
relationIdentifier,
],
};
return remoteResource.get(remoteAttributes.reduce((memo, remoteAttribute, attributeIndex) => {
memo[remoteAttribute] = this.get(localAttributes[attributeIndex]);
return memo;
}, Object.create(null)), options);
}
manyRelation(relationIdentifier) {
const relation = this.resource.getRelation(relationIdentifier);
if (!relation) {
throw new Error(`${String(relationIdentifier)} is not a relation on ${this.resource}`);
}
const { remoteResource, remoteAttributes, localAttributes } = relation;
return remoteResource.find(remoteAttributes.reduce((memo, remoteAttribute, attributeIndex) => {
memo[remoteAttribute] = this.get(localAttributes[attributeIndex]);
return memo;
}, Object.create(null)));
}
record() {
return this.cacheStep("record", "", () => (0, pgClassExpression_js_1.pgClassExpression)(this, this.resource.codec, undefined) `${this.getClassStep().alias}`);
}
toRecord() {
return this.record();
}
deduplicate(peers) {
// We've been careful to not store anything locally so we shouldn't
// need to move anything across to the peer.
return peers.filter((peer) => {
if (peer.resource !== this.resource) {
return false;
}
if (peer.getClassStep() !== this.getClassStep()) {
return false;
}
if (peer.getItemStep() !== this.getItemStep()) {
return false;
}
return true;
});
}
optimize() {
const attributes = this.resource.codec.attributes;
if (attributes && this.getClassStep().mode !== "aggregate") {
// We need to see if this row is null. The cheapest way is to select a
// non-null column, but failing that we invoke the codec's
// nonNullExpression (indirectly).
const getSuitableAttribute = () => {
// We want to find a _cheap_ not-null attribute to select to prove that
// the row is not null. Critically this must be an attribute that we can
// always select (i.e. is not prevented by any column-level select
// privileges).
for (const attr of Object.keys(attributes)) {
const attribute = attributes[attr];
if (attribute.notNull &&
CHEAP_ATTRIBUTE_TYPES.has(attribute.codec) &&
!attribute.restrictedAccess) {
return {
attribute,
attr,
};
}
}
return null;
};
const nonNullAttribute = this.nonNullAttribute ?? getSuitableAttribute();
if (nonNullAttribute != null) {
const { attribute: { codec }, attr, } = nonNullAttribute;
const expression = (0, pg_sql2_1.default) `${this}.${pg_sql2_1.default.identifier(attr)}`;
this.nullCheckAttributeIndex = this.getClassStep().selectAndReturnIndex(codec.castFromPg
? codec.castFromPg(expression)
: (0, pg_sql2_1.default) `${pg_sql2_1.default.parens(expression)}::text`);
}
else {
this.nullCheckId = this.getClassStep().getNullCheckIndex();
}
}
return this;
}
unbatchedExecute(_extra, result) {
if (result == null) {
return this._coalesceToEmptyObject ? EMPTY_TUPLE : null;
}
else if (this.nullCheckAttributeIndex != null) {
const nullIfAttributeNull = result[this.nullCheckAttributeIndex];
if (nullIfAttributeNull == null) {
return this._coalesceToEmptyObject ? EMPTY_TUPLE : null;
}
}
else if (this.nullCheckId != null) {
const nullIfExpressionNotTrue = result[this.nullCheckId];
if (nullIfExpressionNotTrue == null ||
codecs_js_1.TYPES.boolean.fromPg(nullIfExpressionNotTrue) != true) {
return this._coalesceToEmptyObject ? EMPTY_TUPLE : null;
}
}
return result;
}
[pg_sql2_1.$$toSQL]() {
return this.getClassStep().alias;
}
}
exports.PgSelectSingleStep = PgSelectSingleStep;
/**
* Given a plan that represents a single record (via
* PgSelectSingleStep.record()) this turns it back into a PgSelectSingleStep
*/
function pgSelectFromRecord(resource, $record) {
const $select = new pgSelect_js_1.PgSelectStep({
resource: resource,
identifiers: [],
from: {
callback: ($select) => (0, pg_sql2_1.default) `(select (${$select.placeholder($record, resource.codec)}).*)`,
},
joinAsLateral: true,
});
if ($record instanceof pgClassExpression_js_1.PgClassExpressionStep) {
const $parent = $record.getParentStep();
if ($parent instanceof PgSelectSingleStep) {
$select.hints.isPgSelectFromRecordOf = {
parentId: $parent.getClassStep().id,
expression: $record.expression,
};
}
}
return $select;
}
/**
* Given a plan that represents a single record (via
* PgSelectSingleStep.record()) this turns it back into a PgSelectSingleStep
*/
function pgSelectSingleFromRecord(resource, $record) {
// OPTIMIZE: we should be able to optimise this so that `plan.record()` returns the original record again.
return (0, grafast_1.operationPlan)().cacheStep($record, "pgSelectSingleFromRecord", null, () => pgSelectFromRecord(resource, $record).single());
}
(0, grafast_1.exportAs)("@dataplan/pg", pgSelectFromRecord, "pgSelectFromRecord");
(0, grafast_1.exportAs)("@dataplan/pg", pgSelectSingleFromRecord, "pgSelectSingleFromRecord");
//# sourceMappingURL=pgSelectSingle.js.map