@dataplan/pg
Version:
PostgreSQL step classes for Grafast
273 lines • 12.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PgDeleteSingleStep = void 0;
exports.pgDeleteSingle = pgDeleteSingle;
const tslib_1 = require("tslib");
const grafast_1 = require("grafast");
const pg_sql2_1 = tslib_1.__importStar(require("pg-sql2"));
const inspect_js_1 = require("../inspect.js");
const pgClassExpression_js_1 = require("./pgClassExpression.js");
/**
* Deletes a row in the database, can return columns from the deleted row.
*/
class PgDeleteSingleStep extends grafast_1.Step {
static { this.$$export = {
moduleName: "@dataplan/pg",
exportName: "PgDeleteSingleStep",
}; }
constructor(resource, getBy) {
super();
this.isSyncAndSafe = false;
/**
* The attributes and their dependency ids for us to find the record by.
*/
this.getBys = [];
/**
* When locked, no more selects can be added
*/
this.locked = false;
/**
* When finalized, we build the SQL query, queryValues, and note where to feed in
* the relevant queryValues. This saves repeating this work at execution time.
*
* @internal
*/
this.finalizeResults = null;
/**
* The list of things we're selecting.
*/
this.selects = [];
this.applyDepIds = [];
this.resource = resource;
this.name = resource.name;
this.symbol = Symbol(this.name);
this.alias = pg_sql2_1.default.identifier(this.symbol);
this.contextId = this.addDependency(this.resource.executor.context());
const keys = getBy
? Object.keys(getBy)
: [];
if (!this.resource.uniques.some((uniq) => uniq.attributes.every((key) => keys.includes(key)))) {
throw new Error(`Attempted to build 'PgDeleteSingleStep' with a non-unique getBy keys ('${keys.join("', '")}') - please ensure your 'getBy' spec uniquely identifiers a row (resource = ${this.resource}; supported uniques = ${(0, inspect_js_1.inspect)(this.resource.uniques)}).`);
}
keys.forEach((name) => {
if (grafast_1.isDev) {
if (this.getBys.some((col) => col.name === name)) {
throw new Error(`Attribute '${String(name)}' was specified more than once in ${this}'s getBy spec`);
}
}
const value = getBy[name];
const depId = this.addDependency(value);
const attribute = this.resource.codec.attributes[name];
const pgCodec = attribute.codec;
this.getBys.push({ name, depId, pgCodec });
});
// Must be the last action
this.hasSideEffects = true;
}
toStringMeta() {
return `${this.resource.name}(${this.getBys.map((g) => g.name)})`;
}
/**
* Returns a plan representing a named attribute (e.g. column) from the newly
* deleteed row.
*/
get(attr) {
const resourceAttribute = this.resource.codec.attributes[attr];
if (!resourceAttribute) {
throw new Error(`${this.resource} does not define an attribute named '${String(attr)}'`);
}
if (resourceAttribute?.via) {
throw new Error(`Cannot select a 'via' attribute from PgDeleteSingleStep`);
}
/*
* 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, resourceAttribute.codec, resourceAttribute.notNull);
const colPlan = resourceAttribute.expression
? sqlExpr `${pg_sql2_1.default.parens(resourceAttribute.expression(this.alias))}`
: sqlExpr `${this.alias}.${pg_sql2_1.default.identifier(String(attr))}`;
return colPlan;
}
getMeta(key) {
return (0, grafast_1.access)(this, ["m", key]);
}
getNotices() {
return (0, grafast_1.access)(this, "n");
}
record() {
return (0, pgClassExpression_js_1.pgClassExpression)(this, this.resource.codec, false) `${this.alias}`;
}
/**
* Advanced method; rather than returning a plan it returns an index.
* Generally useful for PgClassExpressionStep.
*
* @internal
*/
selectAndReturnIndex(fragment) {
if (this.locked) {
throw new Error("Step is finalized, no more selects may be added");
}
// Optimisation: if we're already selecting this fragment, return the existing one.
const index = this.selects.findIndex((frag) => pg_sql2_1.default.isEquivalent(frag, fragment));
if (index >= 0) {
return index;
}
return this.selects.push(fragment) - 1;
}
apply($step) {
this.applyDepIds.push(this.addUnaryDependency($step));
}
/**
* `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({ indexMap, values, }) {
const { alias, contextId, finalizeResults } = this;
if (!finalizeResults) {
throw new Error("Cannot execute PgSelectStep before finalizing it.");
}
const { text, rawSqlValues, queryValueDetailsBySymbol } = finalizeResults;
// We must execute each mutation on its own, but we can at least do so in
// parallel. Note we return a list of promises, each may reject or resolve
// without causing the others to reject.
const contextDep = values[contextId];
return indexMap(async (i) => {
const context = contextDep.at(i);
const meta = Object.create(null);
const queryBuilder = {
alias,
[pg_sql2_1.$$toSQL]() {
return alias;
},
setMeta(key, value) {
meta[key] = value;
},
getMetaRaw(key) {
return meta[key];
},
};
for (const applyDepId of this.applyDepIds) {
const val = values[applyDepId].unaryValue();
if (Array.isArray(val)) {
val.forEach((v) => v?.(queryBuilder));
}
else {
val?.(queryBuilder);
}
}
const sqlValues = queryValueDetailsBySymbol.size
? rawSqlValues.map((v) => {
if (typeof v === "symbol") {
const details = queryValueDetailsBySymbol.get(v);
if (!details) {
throw new Error(`Saw unexpected symbol '${(0, inspect_js_1.inspect)(v)}'`);
}
const val = values[details.depId].at(i);
return val == null ? null : details.processor(val);
}
else {
return v;
}
})
: rawSqlValues;
const { rows, rowCount, notices } = await this.resource.executeMutation({
context,
text,
values: sqlValues,
});
if (rowCount === 0) {
return Promise.reject(new Error(`No values were deleted in collection '${this.resource.name}' because no values you can delete were found matching these criteria.`));
}
return {
__proto__: null,
m: meta,
t: rows[0] ?? [],
c: rowCount,
n: notices,
};
});
}
finalize() {
if (!this.isFinalized) {
this.locked = true;
const resourceSource = this.resource.from;
if (!pg_sql2_1.default.isSQL(resourceSource)) {
throw new Error(`Error in ${this}: can only delete into resources defined as SQL, however ${this.resource} has ${(0, inspect_js_1.inspect)(this.resource.from)}`);
}
const table = (0, pg_sql2_1.default) `${resourceSource} as ${this.alias}`;
const fragmentsWithAliases = this.selects.map((frag, idx) => (0, pg_sql2_1.default) `${frag} as ${pg_sql2_1.default.identifier(String(idx))}`);
const returning = fragmentsWithAliases.length > 0
? (0, pg_sql2_1.default) ` returning\n${pg_sql2_1.default.indent(pg_sql2_1.default.join(fragmentsWithAliases, ",\n"))}`
: pg_sql2_1.default.blank;
/*
* NOTE: Though we'd like to do bulk deletes, it's challenging to link it
* back together again.
*
* Currently it seems that the order returned from `delete ...
* using (select ... order by ...) returning ...` is the same order as the
* `order by` was, however this is not guaranteed in the documentation
* and as such cannot be relied upon. Further the pgsql-hackers list
* explicitly declined guaranteeing this behavior:
*
* https://www.postgresql.org/message-id/CAKFQuwbgdJ_xNn0YHWGR0D%2Bv%2B3mHGVqJpG_Ejt96KHoJjs6DkA%40mail.gmail.com
*
* So we have to make do with single deletes, alas.
*/
const getByAttributesCount = this.getBys.length;
if (getByAttributesCount === 0) {
// No attributes specified to find the row?! This is forbidden.
throw new grafast_1.SafeError("Attempted to delete a record, but no information on uniquely determining the record was specified.");
}
else {
// This is our common path
const sqlWhereClauses = [];
const queryValueDetailsBySymbol = new Map();
for (let i = 0; i < getByAttributesCount; i++) {
const { name, depId, pgCodec } = this.getBys[i];
const symbol = Symbol(name);
sqlWhereClauses[i] = pg_sql2_1.default.parens((0, pg_sql2_1.default) `${pg_sql2_1.default.identifier(this.symbol, name)} = ${pg_sql2_1.default.value(
// THIS IS A DELIBERATE HACK - we will be replacing this symbol with
// a value before executing the query.
symbol)}::${pgCodec.sqlType}`);
queryValueDetailsBySymbol.set(symbol, {
depId,
processor: pgCodec.toPg,
});
}
const where = (0, pg_sql2_1.default) ` where ${pg_sql2_1.default.parens(pg_sql2_1.default.join(sqlWhereClauses, " and "))}`;
const query = (0, pg_sql2_1.default) `delete from ${table}${where}${returning};`;
const { text, values: rawSqlValues } = pg_sql2_1.default.compile(query);
this.finalizeResults = {
text,
rawSqlValues,
queryValueDetailsBySymbol,
};
}
}
super.finalize();
}
}
exports.PgDeleteSingleStep = PgDeleteSingleStep;
/**
* Delete a row in `resource` identified by the `getBy` unique condition.
*/
function pgDeleteSingle(resource, getBy) {
return new PgDeleteSingleStep(resource, getBy);
}
(0, grafast_1.exportAs)("@dataplan/pg", pgDeleteSingle, "pgDeleteSingle");
//# sourceMappingURL=pgDeleteSingle.js.map