Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/tame-jokes-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@dataplan/pg": minor
"graphile-build-pg": minor
"postgraphile": minor
---

Add support for PostgreSQL PROCEDUREs (introduced in Postgres 11). Procedures
are invoked via `call proc(...)` rather than being embedded in a `select`, so
they're now exposed as mutation fields backed by a new `PgCallStep`/`pgCall()`
step in `@dataplan/pg`, rather than reusing the `select`-based mechanism used
for functions. Procedures with `OUT`/`INOUT` parameters return their values as a
nested record on the mutation payload, matching the existing behaviour for
functions that return multiple columns.
1 change: 1 addition & 0 deletions .github/styles/config/vocabularies/Graphile/accept.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
tamedevil
performant
[Vv]ariadic
substring
autofix
ESLint
Expand Down
9 changes: 8 additions & 1 deletion grafast/dataplan-pg/src/adaptors/pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,17 @@ function newNodePostgresPgClient(
return doIt();
}
function doIt() {
const { text, name, values, arrayMode } = opts;
const { text, name, values, arrayMode, rawText } = opts;
const queryObj: QueryConfig | QueryArrayConfig = arrayMode
? { text, values, rowMode: "array" }
: { text, values };
if (rawText) {
// Some statements (e.g. `CALL`) return columns whose types we
// cannot wrap in an explicit `::text` cast; force the driver to
// hand back raw strings for every column instead of applying its
// own (potentially codec-incompatible) type parsing.
queryObj.types = { getTypeParser: () => (raw: string) => raw };
}

if (PREPARED_STATEMENT_CACHE_SIZE > 0 && name != null) {
// Hacking into pgClient internals - this is dangerous, but it's the only way I know to get a prepared statement LRU
Expand Down
42 changes: 41 additions & 1 deletion grafast/dataplan-pg/src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
PgRegistryConfig,
PlanByUniques,
} from "./interfaces.ts";
import { pgCall } from "./steps/pgCall.ts";
import type { PgClassExpressionStep } from "./steps/pgClassExpression.ts";
import type {
PgSelectArgumentDigest,
Expand Down Expand Up @@ -221,6 +222,25 @@ export interface PgResourceOptions<
isUnique?: boolean;
sqlPartitionByIndex?: SQL;
isMutation?: boolean;
/**
* If true, this resource represents a PostgreSQL PROCEDURE (as opposed to
* a FUNCTION) and must be invoked via a `CALL` statement rather than being
* embedded in a `SELECT ... FROM`. Implies `isMutation`.
*/
isProcedure?: boolean;
/**
* For procedure resources only: describes *every* positional argument the
* underlying PostgreSQL procedure accepts, including OUT-only arguments -
* in declaration order. PostgreSQL's `CALL` statement (unlike a function
* call) requires a value to be supplied for every positional argument,
* including those that are OUT-only (where the value is ignored), so we
* need this to build a valid `CALL` statement even though OUT-only
* arguments aren't exposed to GraphQL via `parameters`.
*/
procedureArguments?: ReadonlyArray<{
mode: "i" | "o" | "b";
codec: PgCodec;
}>;
hasImplicitOrder?: boolean;
/**
* If true, this indicates that this was originally a list (array) and thus
Expand Down Expand Up @@ -255,6 +275,11 @@ export interface PgFunctionResourceOptions<
uniques?: TUniques;
extensions?: DataplanPg.PgResourceExtensions;
isMutation?: boolean;
isProcedure?: boolean;
procedureArguments?: ReadonlyArray<{
mode: "i" | "o" | "b";
codec: PgCodec;
}>;
hasImplicitOrder?: boolean;
selectAuth?:
| (($step: PgSelectStep<PgResource<any, any, any, any, any>>) => void)
Expand Down Expand Up @@ -307,6 +332,10 @@ export class PgResource<
public readonly description: string | undefined;
public readonly isUnique: boolean;
public readonly isMutation: boolean;
public readonly isProcedure: boolean;
public readonly procedureArguments:
| ReadonlyArray<{ mode: "i" | "o" | "b"; codec: PgCodec }>
| undefined;
public readonly hasImplicitOrder: boolean;
/**
* If true, this indicates that this was originally a list (array) and thus
Expand Down Expand Up @@ -345,6 +374,8 @@ export class PgResource<
isUnique,
sqlPartitionByIndex,
isMutation,
isProcedure,
procedureArguments,
hasImplicitOrder,
selectAuth,
isList,
Expand All @@ -362,7 +393,9 @@ export class PgResource<
this.description = description;
this.isUnique = !!isUnique;
this.sqlPartitionByIndex = sqlPartitionByIndex ?? null;
this.isMutation = !!isMutation;
this.isProcedure = !!isProcedure;
this.isMutation = !!isMutation || this.isProcedure;
this.procedureArguments = procedureArguments;
this.hasImplicitOrder = hasImplicitOrder ?? false;
this.isList = !!isList;
this.isVirtual = isVirtual ?? false;
Expand Down Expand Up @@ -466,6 +499,8 @@ export class PgResource<
uniques,
extensions,
isMutation,
isProcedure,
procedureArguments,
hasImplicitOrder,
selectAuth: overrideSelectAuth,
description,
Expand All @@ -487,6 +522,8 @@ export class PgResource<
extensions,
isUnique: !returnsSetof,
isMutation: Boolean(isMutation),
isProcedure: Boolean(isProcedure),
procedureArguments,
hasImplicitOrder,
selectAuth,
description,
Expand Down Expand Up @@ -756,6 +793,9 @@ export class PgResource<
args: ReadonlyArray<PgSelectArgumentSpec> = [],
mode: PgSelectMode = this.isMutation ? "mutation" : "normal",
): ExecutableStep<unknown> {
if (this.isProcedure) {
return pgCall({ resource: this, args });
}
const $select = pgSelect({
resource: this,
identifiers: [],
Expand Down
20 changes: 19 additions & 1 deletion grafast/dataplan-pg/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export interface PgClientQuery {
arrayMode?: boolean;
/** For prepared statements */
name?: string;
/**
* Forces every column of the result to be returned as a raw (undecoded)
* string, bypassing the client library's usual type-aware parsing.
* Required for statements (e.g. `CALL`) whose result columns cannot be
* wrapped in an explicit `::text` cast.
*/
rawText?: boolean;
}

export type PgRaiseSeverity = "DEBUG" | "LOG" | "INFO" | "NOTICE" | "WARNING";
Expand Down Expand Up @@ -164,6 +171,14 @@ export type PgExecutorMutationOptions = {
context: PgExecutorContext;
text: string;
values: ReadonlyArray<SQLRawValue>;
/**
* Statements such as `CALL` return their columns using Postgres' native
* wire-protocol type parsing rather than the `::text` casts we normally
* use to get consistent string representations for our codecs to decode.
* Setting this forces every column of the result to be returned as a raw
* string so it can be fed through the relevant `PgCodec`'s `fromPg`.
*/
rawText?: boolean;
};

export type PgExecutorSubscribeOptions = {
Expand Down Expand Up @@ -209,6 +224,7 @@ export class PgExecutor<const TName extends string = string, TSettings = any> {
name?: string,
publish?: PublishFunction,
isMutation = false,
rawText = false,
): Promise<PgClientResult<TData>> {
let queryResult: PgClientResult<TData> | null = null,
error: any = null;
Expand All @@ -219,6 +235,7 @@ export class PgExecutor<const TName extends string = string, TSettings = any> {
values: values as SQLRawValue[],
arrayMode: true,
name,
...(rawText ? { rawText: true } : null),
});
} catch (e) {
error = e;
Expand Down Expand Up @@ -938,7 +955,7 @@ ${duration}
public async executeMutation<TData>(
options: PgExecutorMutationOptions,
): Promise<PgClientResult<TData>> {
const { context, text, values } = options;
const { context, text, values, rawText } = options;
const { withPgClient, pgSettings } = context;

// We don't explicitly need a transaction for mutations
Expand All @@ -950,6 +967,7 @@ ${duration}
undefined,
undefined,
true,
rawText,
),
);
// PERF: we could probably make this more efficient rather than blowing away the entire cache!
Expand Down
7 changes: 7 additions & 0 deletions grafast/dataplan-pg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ import {
withSuperuserPgClientFromPgService,
} from "./pgServices.ts";
import { PgContextPlugin } from "./plugins/PgContextPlugin.ts";
import type { PgCallQueryBuilder } from "./steps/pgCall.ts";
import { pgCall, PgCallStep } from "./steps/pgCall.ts";
import {
pgClassExpression,
PgClassExpressionStep,
Expand Down Expand Up @@ -220,6 +222,7 @@ export type {
ObjectFromPgCodecAttributes,
PgAdaptor,
PgBox,
PgCallQueryBuilder,
PgCircle,
PgClassSingleStep,
PgClient,
Expand Down Expand Up @@ -332,6 +335,8 @@ export {
makeRegistry,
makeRegistryBuilder,
PgBooleanFilter,
pgCall,
PgCallStep,
pgClassExpression,
PgClassExpressionStep,
PgClassFilter,
Expand Down Expand Up @@ -406,6 +411,8 @@ exportAsMany("@dataplan/pg", {
PgOrFilter,
pgClassExpression,
PgClassExpressionStep,
pgCall,
PgCallStep,
PgCondition,
pgWhereConditionSpecListToSQL,
PgCursorStep,
Expand Down
4 changes: 3 additions & 1 deletion grafast/dataplan-pg/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PgResourceUnique,
} from "./datasource.ts";
import type { PgExecutor } from "./executor.ts";
import type { PgCallStep } from "./steps/pgCall.ts";
import type { PgDeleteSingleStep } from "./steps/pgDeleteSingle.ts";
import type { PgInsertSingleStep } from "./steps/pgInsertSingle.ts";
import type { PgSelectQueryBuilder } from "./steps/pgSelect.ts";
Expand All @@ -29,7 +30,8 @@ export type PgClassSingleStep<
| PgSelectSingleStep<TResource>
| PgInsertSingleStep<TResource>
| PgUpdateSingleStep<TResource>
| PgDeleteSingleStep<TResource>;
| PgDeleteSingleStep<TResource>
| PgCallStep<TResource>;

/**
* Given a value of type TInput, returns an `SQL` value to insert into an SQL
Expand Down
Loading
Loading