@avelonjs/postgres 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/driver.ts CHANGED
@@ -5,14 +5,13 @@ import {
5
5
  type DatabaseTransaction,
6
6
  type MigrationPlan,
7
7
  type MigrationStatus,
8
- type Predicate,
9
8
  type QueryIR,
10
9
  type QueryResult,
11
- type RelationLoad,
12
10
  type TransactionSurface,
13
11
  } from '@avelonjs/core'
14
- import { compilePostgres } from './compile'
12
+ import { bunSqlRunner } from './bun-client'
15
13
  import { mapPostgresError } from './errors'
14
+ import { executeQueryIR, executeRpc } from './execute'
16
15
  import { resetAssayFixtures } from './fixtures'
17
16
  import {
18
17
  applyMigrations,
@@ -20,10 +19,9 @@ import {
20
19
  rollbackMigrations,
21
20
  statusMigrations,
22
21
  type PostgresMigration,
23
- } from './migrations'
24
- import { combinedPredicate } from './normalize'
25
- import { assertQueryAgainstSchema, loadSchemaCache, type SchemaCache } from './schema'
26
- import { validateQueryIR } from './validate'
22
+ } from './migrations-core'
23
+ import { loadSchemaCache, type SchemaCache } from './schema-core'
24
+ import type { SqlBatchRunner } from './sql-client'
27
25
 
28
26
  /** Exact capability declaration for the Postgres database driver. */
29
27
  export const postgresDatabaseCapabilities = {
@@ -48,9 +46,6 @@ export interface PostgresDatabaseOptions {
48
46
  migrations?: readonly PostgresMigration[]
49
47
  }
50
48
 
51
- type Row = Record<string, unknown>
52
- type SqlClient = SQL
53
-
54
49
  function defaultUrl(): string {
55
50
  return (
56
51
  process.env.POSTGRES_URL ??
@@ -59,56 +54,14 @@ function defaultUrl(): string {
59
54
  )
60
55
  }
61
56
 
62
- function includeColumns(select: string[] | '*', columns: string[]): string[] | '*' {
63
- if (select === '*') return '*'
64
- return [...new Set([...select, ...columns])]
65
- }
66
-
67
- function projectRow(row: Row, select: string[] | '*', relations: readonly RelationLoad[]): Row {
68
- const output: Row = {}
69
- const columns =
70
- select === '*'
71
- ? Object.keys(row).filter((column) => !relations.some((relation) => relation.relation === column))
72
- : select
73
- for (const column of columns) output[column] = row[column]
74
- for (const relation of relations) output[relation.relation] = row[relation.relation]
75
- return output
76
- }
77
-
78
- function unique(values: unknown[]): unknown[] {
79
- const output: unknown[] = []
80
- for (const value of values) {
81
- if (!output.some((existing) => sqlEqual(existing, value))) output.push(value)
82
- }
83
- return output
84
- }
85
-
86
- function sqlEqual(left: unknown, right: unknown): boolean {
87
- if (left === null || left === undefined || right === null || right === undefined) return false
88
- return left === right
89
- }
90
-
91
- function asRows(result: unknown): Row[] {
92
- if (!Array.isArray(result)) return []
93
- return result.map((row) => ({ ...(row as Row) }))
94
- }
95
-
96
- function resultCount(result: unknown): number {
97
- if (!Array.isArray(result)) return 0
98
- const count = Reflect.get(result, 'count')
99
- return typeof count === 'number' ? count : result.length
100
- }
101
-
102
57
  /**
103
58
  * Postgres implementation of the frozen database contract.
104
59
  *
105
- * Vendor access stays inside this package through Bun's SQL client. Application code reaches the
106
- * driver only through the database facade and never imports `bun` SQL APIs.
60
+ * Vendor access stays inside this package through Bun's SQL client. The query IR algorithm itself
61
+ * lives in `@avelonjs/postgres/sql`, which no Bun API reaches; this class only adapts the client.
107
62
  */
108
63
  export class PostgresDatabase
109
- implements
110
- DatabaseDriver<typeof postgresDatabaseCapabilities, SQL>,
111
- TransactionSurface
64
+ implements DatabaseDriver<typeof postgresDatabaseCapabilities, SQL>, TransactionSurface
112
65
  {
113
66
  /** Driver implementation name. */
114
67
  readonly name = 'postgres'
@@ -120,6 +73,7 @@ export class PostgresDatabase
120
73
  readonly capabilities: typeof postgresDatabaseCapabilities
121
74
 
122
75
  readonly #sql: SQL
76
+ readonly #runner: SqlBatchRunner
123
77
  readonly #migrations: readonly PostgresMigration[]
124
78
  #schema: SchemaCache | undefined
125
79
  #roundTrips = 0
@@ -130,6 +84,7 @@ export class PostgresDatabase
130
84
  this.#migrations = options.migrations ?? []
131
85
  this.capabilities = postgresDatabaseCapabilities
132
86
  this.#sql = new SQL(options.url ?? defaultUrl())
87
+ this.#runner = bunSqlRunner(this.#sql)
133
88
  }
134
89
 
135
90
  /** Number of statements that reached Postgres. Used by conformance short-circuit checks. */
@@ -155,7 +110,7 @@ export class PostgresDatabase
155
110
 
156
111
  /** Compiles and executes one query IR operation. */
157
112
  async execute<TRow = Record<string, unknown>>(query: QueryIR): Promise<QueryResult<TRow>> {
158
- return this.#execute(this.#sql, query) as Promise<QueryResult<TRow>>
113
+ return this.#execute(this.#runner, query) as Promise<QueryResult<TRow>>
159
114
  }
160
115
 
161
116
  /** Invokes a named Postgres routine. Missing routines map to `Invalid`. */
@@ -163,37 +118,27 @@ export class PostgresDatabase
163
118
  routine: string,
164
119
  args: Readonly<Record<string, unknown>>,
165
120
  ): Promise<TResult> {
166
- try {
167
- this.#roundTrips += 1
168
- const rows = asRows(
169
- await this.#sql.unsafe(`SELECT ${quoteRoutine(routine)}($1::jsonb) AS result`, [
170
- JSON.stringify(args),
171
- ]),
172
- )
173
- return decodeJsonResult(rows[0]?.result) as TResult
174
- } catch (error) {
175
- mapPostgresError(error, 'rpc')
176
- }
121
+ return executeRpc(this.#runner, routine, args, this.#countRoundTrip) as Promise<TResult>
177
122
  }
178
123
 
179
124
  /** Produces the pending migration plan. */
180
125
  async plan(): Promise<MigrationPlan> {
181
- return planMigrations(this.#sql, this.#migrations)
126
+ return planMigrations(this.#runner, this.#migrations)
182
127
  }
183
128
 
184
129
  /** Applies pending migrations. */
185
130
  async apply(): Promise<readonly MigrationStatus[]> {
186
- return applyMigrations(this.#sql, this.#migrations)
131
+ return applyMigrations(this.#runner, this.#migrations)
187
132
  }
188
133
 
189
134
  /** Rolls back applied migration batches. */
190
135
  async rollback(steps?: number): Promise<readonly MigrationStatus[]> {
191
- return rollbackMigrations(this.#sql, this.#migrations, steps)
136
+ return rollbackMigrations(this.#runner, this.#migrations, steps)
192
137
  }
193
138
 
194
139
  /** Returns all migration states. */
195
140
  async status(): Promise<readonly MigrationStatus[]> {
196
- return statusMigrations(this.#sql, this.#migrations)
141
+ return statusMigrations(this.#runner, this.#migrations)
197
142
  }
198
143
 
199
144
  /** Runs a callback inside a Postgres transaction. */
@@ -202,25 +147,12 @@ export class PostgresDatabase
202
147
  ): Promise<TResult> {
203
148
  try {
204
149
  return await this.#sql.begin(async (tx) => {
150
+ const runner = bunSqlRunner(tx as SQL)
205
151
  const transaction: DatabaseTransaction = {
206
152
  execute: <TRow = Record<string, unknown>>(query: QueryIR) =>
207
- this.#execute(tx as SqlClient, query) as Promise<QueryResult<TRow>>,
208
- rpc: async <TResultRpc = unknown>(
209
- routine: string,
210
- args: Readonly<Record<string, unknown>>,
211
- ) => {
212
- try {
213
- this.#roundTrips += 1
214
- const rows = asRows(
215
- await (tx as SqlClient).unsafe(`SELECT ${quoteRoutine(routine)}($1::jsonb) AS result`, [
216
- JSON.stringify(args),
217
- ]),
218
- )
219
- return decodeJsonResult(rows[0]?.result) as TResultRpc
220
- } catch (error) {
221
- mapPostgresError(error, 'rpc')
222
- }
223
- },
153
+ this.#execute(runner, query) as Promise<QueryResult<TRow>>,
154
+ rpc: <TResultRpc = unknown>(routine: string, args: Readonly<Record<string, unknown>>) =>
155
+ executeRpc(runner, routine, args, this.#countRoundTrip) as Promise<TResultRpc>,
224
156
  }
225
157
  return callback(transaction)
226
158
  })
@@ -238,145 +170,23 @@ export class PostgresDatabase
238
170
  }
239
171
  }
240
172
 
241
- async #schemaCache(): Promise<SchemaCache> {
242
- if (this.#schema === undefined) this.#schema = await loadSchemaCache(this.#sql)
243
- return this.#schema
244
- }
245
-
246
- async #execute(sql: SqlClient, ir: QueryIR): Promise<QueryResult> {
247
- validateQueryIR(ir, this.capabilities.maxRelationDepth)
248
- assertQueryAgainstSchema(await this.#schemaCache(), ir)
249
- const predicate = combinedPredicate(ir)
250
-
251
- if (predicate.kind === 'const' && !predicate.value) {
252
- return ir.mode === 'count' ? { rows: [], affected: 0, count: 0 } : { rows: [], affected: 0 }
253
- }
254
-
255
- if ((ir.mode === 'insert' || ir.mode === 'upsert') && predicate.kind !== 'const') {
256
- throw new Invalid(`${ir.mode} with a non-constant where or ward has no documented row scope.`, {
257
- metadata: { fields: { where: [`${ir.mode} predicates are not documented`] } },
258
- })
259
- }
260
-
261
- if (ir.mode === 'select') {
262
- const primitive: QueryIR = {
263
- ...ir,
264
- select: includeColumns(
265
- ir.select,
266
- ir.relations.map((relation) => relation.localKey),
267
- ),
268
- relations: [],
269
- }
270
- const rows = await this.#executePrimitive(sql, primitive, predicate)
271
- await this.#loadRelations(sql, rows, ir.relations)
272
- return {
273
- rows: rows.map((row) => projectRow(row, ir.select, ir.relations)),
274
- affected: 0,
275
- }
276
- }
277
-
278
- if (ir.mode === 'count') {
279
- const rows = await this.#executePrimitive(sql, ir, predicate)
280
- const count = Number(rows[0]?.count ?? 0)
281
- return { rows: [], affected: 0, count }
282
- }
283
-
284
- const rows = await this.#executePrimitive(sql, ir, predicate)
285
- const affected = resultCount(rows)
286
- return {
287
- rows: ir.returning === undefined ? [] : rows,
288
- affected,
289
- }
290
- }
291
-
292
- async #executePrimitive(sql: SqlClient, ir: QueryIR, predicate: Predicate): Promise<Row[]> {
293
- const compiled = compilePostgres(ir, predicate)
294
- try {
295
- this.#roundTrips += 1
296
- const result = await sql.unsafe(compiled.text, compiled.parameters)
297
- const rows = asRows(result)
298
- // Preserve Bun's statement count for writes without RETURNING (empty row arrays).
299
- Object.defineProperty(rows, 'count', {
300
- value: resultCount(result),
301
- enumerable: false,
302
- })
303
- return rows
304
- } catch (error) {
305
- mapPostgresError(error, 'execute')
306
- }
173
+ readonly #countRoundTrip = (): void => {
174
+ this.#roundTrips += 1
307
175
  }
308
176
 
309
- async #loadRelations(
310
- sql: SqlClient,
311
- parents: Row[],
312
- relations: readonly RelationLoad[],
313
- ): Promise<void> {
314
- for (const relation of relations) {
315
- const keys = unique(
316
- parents
317
- .map((parent) => parent[relation.localKey])
318
- .filter((value) => value !== null && value !== undefined),
319
- )
320
-
321
- if (keys.length === 0) {
322
- for (const parent of parents) {
323
- parent[relation.relation] = relation.kind === 'hasMany' ? [] : null
324
- }
325
- continue
326
- }
327
-
328
- const childSelect = includeColumns(relation.select, [
329
- relation.foreignKey,
330
- ...relation.relations.map((nested) => nested.localKey),
331
- ])
332
- const childWhere: Predicate[] = [
333
- ...(relation.where === null ? [] : [relation.where]),
334
- { kind: 'in', column: relation.foreignKey, values: keys, negated: false },
335
- ]
336
- const childIR: QueryIR = {
337
- table: relation.table,
338
- mode: 'select',
339
- select: childSelect,
340
- where: childWhere,
341
- relations: [],
342
- order: relation.order,
343
- }
344
- const predicate = combinedPredicate(childIR)
345
- const children =
346
- predicate.kind === 'const' && !predicate.value
347
- ? []
348
- : await this.#executePrimitive(sql, childIR, predicate)
349
- await this.#loadRelations(sql, children, relation.relations)
350
-
351
- for (const parent of parents) {
352
- const matches = children.filter((child) =>
353
- sqlEqual(child[relation.foreignKey], parent[relation.localKey]),
354
- )
355
- const limited = relation.limit === undefined ? matches : matches.slice(0, relation.limit)
356
- const projected = limited.map((child) =>
357
- projectRow(child, relation.select, relation.relations),
358
- )
359
- parent[relation.relation] = relation.kind === 'hasMany' ? projected : (projected[0] ?? null)
360
- }
361
- }
362
- }
363
- }
364
-
365
- function quoteRoutine(routine: string): string {
366
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(routine)) {
367
- throw new Invalid('Routine name must be a simple identifier.', {
368
- metadata: { fields: { routine: [routine] } },
369
- })
177
+ async #schemaCache(): Promise<SchemaCache> {
178
+ if (this.#schema === undefined) this.#schema = await loadSchemaCache(this.#runner)
179
+ return this.#schema
370
180
  }
371
- return `"${routine}"`
372
- }
373
181
 
374
- function decodeJsonResult(value: unknown): unknown {
375
- if (typeof value !== 'string') return value
376
- try {
377
- return JSON.parse(value)
378
- } catch {
379
- return value
182
+ async #execute(runner: SqlBatchRunner, ir: QueryIR): Promise<QueryResult> {
183
+ return executeQueryIR(
184
+ runner,
185
+ await this.#schemaCache(),
186
+ ir,
187
+ this.capabilities.maxRelationDepth,
188
+ this.#countRoundTrip,
189
+ )
380
190
  }
381
191
  }
382
192
 
package/src/errors.ts CHANGED
@@ -23,6 +23,8 @@ function asPostgresError(error: unknown): PostgresErrorLike | undefined {
23
23
  function sqlState(error: PostgresErrorLike): string | undefined {
24
24
  if (typeof error.errno === 'string') return error.errno
25
25
  if (typeof error.errno === 'number') return String(error.errno)
26
+ // NeonDbError and node-postgres carry SQLSTATE on `code`; Bun carries it on `errno`.
27
+ if (typeof error.code === 'string') return error.code
26
28
  return undefined
27
29
  }
28
30
 
@@ -34,7 +36,8 @@ function sqlState(error: PostgresErrorLike): string | undefined {
34
36
  export function mapPostgresError(error: unknown, operation: string): never {
35
37
  const pg = asPostgresError(error)
36
38
  const state = pg === undefined ? undefined : sqlState(pg)
37
- const message = pg?.message ?? (error instanceof Error ? error.message : 'Postgres driver failure')
39
+ const message =
40
+ pg?.message ?? (error instanceof Error ? error.message : 'Postgres driver failure')
38
41
 
39
42
  if (state === '23505') {
40
43
  throw new Conflict(message, {
@@ -96,5 +99,9 @@ export const POSTGRES_ERROR_MAP = [
96
99
  { sqlstate: '57P01', framework: 'Unavailable', meaning: 'admin_shutdown' },
97
100
  { sqlstate: '57P03', framework: 'Unavailable', meaning: 'cannot_connect_now' },
98
101
  { sqlstate: '08006', framework: 'Unavailable', meaning: 'connection_failure' },
99
- { sqlstate: '08001', framework: 'Unavailable', meaning: 'sqlclient_unable_to_establish_sqlconnection' },
102
+ {
103
+ sqlstate: '08001',
104
+ framework: 'Unavailable',
105
+ meaning: 'sqlclient_unable_to_establish_sqlconnection',
106
+ },
100
107
  ] as const
package/src/execute.ts ADDED
@@ -0,0 +1,209 @@
1
+ import {
2
+ Invalid,
3
+ type Predicate,
4
+ type QueryIR,
5
+ type QueryResult,
6
+ type RelationLoad,
7
+ } from '@avelonjs/core'
8
+ import { compilePostgres } from './compile'
9
+ import { mapPostgresError } from './errors'
10
+ import { combinedPredicate } from './normalize'
11
+ import { assertQueryAgainstSchema, type SchemaCache } from './schema-core'
12
+ import type { SqlRows, SqlRunner } from './sql-client'
13
+ import { validateQueryIR } from './validate'
14
+
15
+ type Row = Record<string, unknown>
16
+
17
+ /** Called once per statement so drivers can keep the conformance round-trip counter. */
18
+ export type RoundTripCounter = () => void
19
+
20
+ function includeColumns(select: string[] | '*', columns: string[]): string[] | '*' {
21
+ if (select === '*') return '*'
22
+ return [...new Set([...select, ...columns])]
23
+ }
24
+
25
+ function projectRow(row: Row, select: string[] | '*', relations: readonly RelationLoad[]): Row {
26
+ const output: Row = {}
27
+ const columns =
28
+ select === '*'
29
+ ? Object.keys(row).filter(
30
+ (column) => !relations.some((relation) => relation.relation === column),
31
+ )
32
+ : select
33
+ for (const column of columns) output[column] = row[column]
34
+ for (const relation of relations) output[relation.relation] = row[relation.relation]
35
+ return output
36
+ }
37
+
38
+ function unique(values: unknown[]): unknown[] {
39
+ const output: unknown[] = []
40
+ for (const value of values) {
41
+ if (!output.some((existing) => sqlEqual(existing, value))) output.push(value)
42
+ }
43
+ return output
44
+ }
45
+
46
+ function sqlEqual(left: unknown, right: unknown): boolean {
47
+ if (left === null || left === undefined || right === null || right === undefined) return false
48
+ return left === right
49
+ }
50
+
51
+ function quoteRoutine(routine: string): string {
52
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(routine)) {
53
+ throw new Invalid('Routine name must be a simple identifier.', {
54
+ metadata: { fields: { routine: [routine] } },
55
+ })
56
+ }
57
+ return `"${routine}"`
58
+ }
59
+
60
+ function decodeJsonResult(value: unknown): unknown {
61
+ if (typeof value !== 'string') return value
62
+ try {
63
+ return JSON.parse(value)
64
+ } catch {
65
+ return value
66
+ }
67
+ }
68
+
69
+ async function executePrimitive(
70
+ runner: SqlRunner,
71
+ ir: QueryIR,
72
+ predicate: Predicate,
73
+ onRoundTrip: RoundTripCounter,
74
+ ): Promise<SqlRows> {
75
+ const compiled = compilePostgres(ir, predicate)
76
+ try {
77
+ onRoundTrip()
78
+ return await runner.unsafe(compiled.text, compiled.parameters)
79
+ } catch (error) {
80
+ mapPostgresError(error, 'execute')
81
+ }
82
+ }
83
+
84
+ async function loadRelations(
85
+ runner: SqlRunner,
86
+ parents: Row[],
87
+ relations: readonly RelationLoad[],
88
+ onRoundTrip: RoundTripCounter,
89
+ ): Promise<void> {
90
+ for (const relation of relations) {
91
+ const keys = unique(
92
+ parents
93
+ .map((parent) => parent[relation.localKey])
94
+ .filter((value) => value !== null && value !== undefined),
95
+ )
96
+
97
+ if (keys.length === 0) {
98
+ for (const parent of parents) {
99
+ parent[relation.relation] = relation.kind === 'hasMany' ? [] : null
100
+ }
101
+ continue
102
+ }
103
+
104
+ const childSelect = includeColumns(relation.select, [
105
+ relation.foreignKey,
106
+ ...relation.relations.map((nested) => nested.localKey),
107
+ ])
108
+ const childWhere: Predicate[] = [
109
+ ...(relation.where === null ? [] : [relation.where]),
110
+ { kind: 'in', column: relation.foreignKey, values: keys, negated: false },
111
+ ]
112
+ const childIR: QueryIR = {
113
+ table: relation.table,
114
+ mode: 'select',
115
+ select: childSelect,
116
+ where: childWhere,
117
+ relations: [],
118
+ order: relation.order,
119
+ }
120
+ const predicate = combinedPredicate(childIR)
121
+ const children =
122
+ predicate.kind === 'const' && !predicate.value
123
+ ? []
124
+ : (await executePrimitive(runner, childIR, predicate, onRoundTrip)).rows
125
+ await loadRelations(runner, children, relation.relations, onRoundTrip)
126
+
127
+ for (const parent of parents) {
128
+ const matches = children.filter((child) =>
129
+ sqlEqual(child[relation.foreignKey], parent[relation.localKey]),
130
+ )
131
+ const limited = relation.limit === undefined ? matches : matches.slice(0, relation.limit)
132
+ const projected = limited.map((child) =>
133
+ projectRow(child, relation.select, relation.relations),
134
+ )
135
+ parent[relation.relation] = relation.kind === 'hasMany' ? projected : (projected[0] ?? null)
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Validates, compiles, and executes one query IR operation against any SQL runner.
142
+ *
143
+ * Relations load application-side: each level reads its parent rows before the child statement is
144
+ * built, so this needs an interactive runner and cannot be flattened into one batch.
145
+ */
146
+ export async function executeQueryIR(
147
+ runner: SqlRunner,
148
+ schema: SchemaCache,
149
+ ir: QueryIR,
150
+ maxRelationDepth: number,
151
+ onRoundTrip: RoundTripCounter,
152
+ ): Promise<QueryResult> {
153
+ validateQueryIR(ir, maxRelationDepth)
154
+ assertQueryAgainstSchema(schema, ir)
155
+ const predicate = combinedPredicate(ir)
156
+
157
+ // An insert or upsert has no rows to scope: a ward that refuses its values is an error from
158
+ // `compilePostgres`, not an empty result, so it must not short-circuit here.
159
+ const rowScoped = ir.mode !== 'insert' && ir.mode !== 'upsert'
160
+ if (rowScoped && predicate.kind === 'const' && !predicate.value) {
161
+ return ir.mode === 'count' ? { rows: [], affected: 0, count: 0 } : { rows: [], affected: 0 }
162
+ }
163
+
164
+ if (ir.mode === 'select') {
165
+ const primitive: QueryIR = {
166
+ ...ir,
167
+ select: includeColumns(
168
+ ir.select,
169
+ ir.relations.map((relation) => relation.localKey),
170
+ ),
171
+ relations: [],
172
+ }
173
+ const result = await executePrimitive(runner, primitive, predicate, onRoundTrip)
174
+ await loadRelations(runner, result.rows, ir.relations, onRoundTrip)
175
+ return {
176
+ rows: result.rows.map((row) => projectRow(row, ir.select, ir.relations)),
177
+ affected: 0,
178
+ }
179
+ }
180
+
181
+ if (ir.mode === 'count') {
182
+ const result = await executePrimitive(runner, ir, predicate, onRoundTrip)
183
+ return { rows: [], affected: 0, count: Number(result.rows[0]?.count ?? 0) }
184
+ }
185
+
186
+ const result = await executePrimitive(runner, ir, predicate, onRoundTrip)
187
+ return {
188
+ rows: ir.returning === undefined ? [] : result.rows,
189
+ affected: result.count,
190
+ }
191
+ }
192
+
193
+ /** Invokes a named Postgres routine with a single jsonb argument. Missing routines map to `Invalid`. */
194
+ export async function executeRpc(
195
+ runner: SqlRunner,
196
+ routine: string,
197
+ args: Readonly<Record<string, unknown>>,
198
+ onRoundTrip: RoundTripCounter,
199
+ ): Promise<unknown> {
200
+ try {
201
+ onRoundTrip()
202
+ const result = await runner.unsafe(`SELECT ${quoteRoutine(routine)}($1::jsonb) AS result`, [
203
+ JSON.stringify(args),
204
+ ])
205
+ return decodeJsonResult(result.rows[0]?.result)
206
+ } catch (error) {
207
+ mapPostgresError(error, 'rpc')
208
+ }
209
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Statements that drop and recreate the database conformance fixtures.
3
+ *
4
+ * Authored as an array because Neon's HTTP endpoint rejects multi-statement query text, and the
5
+ * `assay_echo` body is dollar-quoted, so splitting the blob on `;` at runtime would corrupt it.
6
+ */
7
+ export const ASSAY_FIXTURE_STATEMENTS: readonly string[] = [
8
+ 'DROP TABLE IF EXISTS assay_reactions CASCADE',
9
+ 'DROP TABLE IF EXISTS assay_comments CASCADE',
10
+ 'DROP TABLE IF EXISTS assay_posts CASCADE',
11
+ 'DROP TABLE IF EXISTS assay_profiles CASCADE',
12
+ 'DROP TABLE IF EXISTS assay_users CASCADE',
13
+ 'DROP FUNCTION IF EXISTS assay_echo(jsonb)',
14
+ `CREATE TABLE assay_users (
15
+ id text PRIMARY KEY,
16
+ email text NOT NULL UNIQUE,
17
+ name text NOT NULL,
18
+ age integer NOT NULL,
19
+ nickname text
20
+ )`,
21
+ `CREATE TABLE assay_profiles (
22
+ id text PRIMARY KEY,
23
+ user_id text NOT NULL UNIQUE REFERENCES assay_users(id) ON DELETE CASCADE,
24
+ bio text NOT NULL
25
+ )`,
26
+ `CREATE TABLE assay_posts (
27
+ id text PRIMARY KEY,
28
+ user_id text NOT NULL REFERENCES assay_users(id) ON DELETE CASCADE,
29
+ title text NOT NULL,
30
+ score integer NOT NULL,
31
+ published_at timestamptz
32
+ )`,
33
+ `CREATE TABLE assay_comments (
34
+ id text PRIMARY KEY,
35
+ post_id text NOT NULL REFERENCES assay_posts(id) ON DELETE CASCADE,
36
+ body text NOT NULL,
37
+ position integer NOT NULL
38
+ )`,
39
+ `CREATE TABLE assay_reactions (
40
+ id text PRIMARY KEY,
41
+ comment_id text NOT NULL REFERENCES assay_comments(id) ON DELETE CASCADE,
42
+ kind text NOT NULL
43
+ )`,
44
+ `CREATE OR REPLACE FUNCTION assay_echo(args jsonb)
45
+ RETURNS jsonb
46
+ LANGUAGE sql
47
+ IMMUTABLE
48
+ AS $$
49
+ SELECT args;
50
+ $$`,
51
+ ]
52
+
53
+ /** SQL that drops and recreates the database conformance fixtures. */
54
+ export const ASSAY_FIXTURE_SQL = `
55
+ DROP TABLE IF EXISTS assay_reactions CASCADE;
56
+ DROP TABLE IF EXISTS assay_comments CASCADE;
57
+ DROP TABLE IF EXISTS assay_posts CASCADE;
58
+ DROP TABLE IF EXISTS assay_profiles CASCADE;
59
+ DROP TABLE IF EXISTS assay_users CASCADE;
60
+ DROP FUNCTION IF EXISTS assay_echo(jsonb);
61
+
62
+ CREATE TABLE assay_users (
63
+ id text PRIMARY KEY,
64
+ email text NOT NULL UNIQUE,
65
+ name text NOT NULL,
66
+ age integer NOT NULL,
67
+ nickname text
68
+ );
69
+
70
+ CREATE TABLE assay_profiles (
71
+ id text PRIMARY KEY,
72
+ user_id text NOT NULL UNIQUE REFERENCES assay_users(id) ON DELETE CASCADE,
73
+ bio text NOT NULL
74
+ );
75
+
76
+ CREATE TABLE assay_posts (
77
+ id text PRIMARY KEY,
78
+ user_id text NOT NULL REFERENCES assay_users(id) ON DELETE CASCADE,
79
+ title text NOT NULL,
80
+ score integer NOT NULL,
81
+ published_at timestamptz
82
+ );
83
+
84
+ CREATE TABLE assay_comments (
85
+ id text PRIMARY KEY,
86
+ post_id text NOT NULL REFERENCES assay_posts(id) ON DELETE CASCADE,
87
+ body text NOT NULL,
88
+ position integer NOT NULL
89
+ );
90
+
91
+ CREATE TABLE assay_reactions (
92
+ id text PRIMARY KEY,
93
+ comment_id text NOT NULL REFERENCES assay_comments(id) ON DELETE CASCADE,
94
+ kind text NOT NULL
95
+ );
96
+
97
+ CREATE OR REPLACE FUNCTION assay_echo(args jsonb)
98
+ RETURNS jsonb
99
+ LANGUAGE sql
100
+ IMMUTABLE
101
+ AS $$
102
+ SELECT args;
103
+ $$;
104
+ `