@avelonjs/postgres 0.1.0 → 0.3.0
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/README.md +89 -55
- package/package.json +3 -2
- package/src/bun-client.ts +32 -0
- package/src/compile.ts +62 -8
- package/src/driver.ts +33 -223
- package/src/errors.ts +9 -2
- package/src/execute.ts +209 -0
- package/src/fixtures-sql.ts +104 -0
- package/src/fixtures.ts +2 -52
- package/src/index.ts +2 -1
- package/src/migrations-core.ts +142 -0
- package/src/migrations.ts +13 -91
- package/src/schema-core.ts +124 -0
- package/src/schema.ts +5 -117
- package/src/sql-client.ts +28 -0
- package/src/sql.ts +15 -0
- package/src/validate.ts +24 -7
package/README.md
CHANGED
|
@@ -40,20 +40,49 @@ const result = await db.execute<{ id: string; title: string }>(latest)
|
|
|
40
40
|
|
|
41
41
|
## Capabilities
|
|
42
42
|
|
|
43
|
-
| Capability
|
|
44
|
-
|
|
45
|
-
| `transactions`
|
|
46
|
-
| `rowSecurity`
|
|
47
|
-
| `maxRelationDepth` | `8`
|
|
48
|
-
| `fullTextSearch`
|
|
49
|
-
| `upsert`
|
|
50
|
-
| `returning`
|
|
51
|
-
| `windowFunctions`
|
|
52
|
-
| `jsonOperators`
|
|
43
|
+
| Capability | Value | Notes |
|
|
44
|
+
| ------------------ | ------- | --------------------------------------------------------------------- |
|
|
45
|
+
| `transactions` | `true` | Interactive transactions through `db.transaction()` |
|
|
46
|
+
| `rowSecurity` | `false` | Ward-to-RLS compilation belongs to `@avelonjs/supabase` |
|
|
47
|
+
| `maxRelationDepth` | `8` | Application-side nested loads; measured against nested assay fixtures |
|
|
48
|
+
| `fullTextSearch` | `false` | No portable `search()` surface in v1 |
|
|
49
|
+
| `upsert` | `true` | Requires explicit conflict target and update list |
|
|
50
|
+
| `returning` | `true` | Write queries may project rows |
|
|
51
|
+
| `windowFunctions` | `true` | Informational; available through `raw()` |
|
|
52
|
+
| `jsonOperators` | `true` | Informational; available through `raw()` |
|
|
53
|
+
|
|
54
|
+
## Node Without Bun
|
|
55
|
+
|
|
56
|
+
The query IR algorithm, migrations, schema checks, and error mapping live behind the `@avelonjs/postgres/sql` subpath, whose import graph contains no `bun` specifier. The package root stays on Bun's SQL client. A driver on another runtime supplies its own client through `SqlRunner` and `SqlBatchRunner`; `@avelonjs/neon` uses this to run on Node.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { executeQueryIR, loadSchemaCache, type SqlRunner } from '@avelonjs/postgres/sql'
|
|
60
|
+
import type { QueryIR } from '@avelonjs/core'
|
|
61
|
+
|
|
62
|
+
declare const vendor: {
|
|
63
|
+
query(
|
|
64
|
+
text: string,
|
|
65
|
+
params: unknown[],
|
|
66
|
+
): Promise<{ rows: Record<string, unknown>[]; rowCount: number }>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const runner: SqlRunner = {
|
|
70
|
+
unsafe: async (text, parameters) => {
|
|
71
|
+
const result = await vendor.query(text, parameters === undefined ? [] : [...parameters])
|
|
72
|
+
return { rows: result.rows, count: result.rowCount }
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare const query: QueryIR
|
|
77
|
+
const schema = await loadSchemaCache(runner)
|
|
78
|
+
const result = await executeQueryIR(runner, schema, query, 8, () => undefined)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`SqlBatchRunner` adds `batch()`, which applies a fully known statement list atomically. Migrations use it so every statement of one migration, plus its history row, reaches the database in a single transaction.
|
|
53
82
|
|
|
54
83
|
## Query Compilation
|
|
55
84
|
|
|
56
|
-
The driver validates IR shape, rejects unknown public-schema identifiers, normalizes predicates, and compiles positional SQL. Empty AND is true, empty OR is false, empty IN matches nothing unless negated, and a constant-false ward or where short-circuits without a database round trip.
|
|
85
|
+
The driver validates IR shape, rejects unknown public-schema identifiers, normalizes predicates, and compiles positional SQL. Empty AND is true, empty OR is false, empty IN matches nothing unless negated, and a constant-false ward or where short-circuits without a database round trip. On insert and upsert a ward the values row does not satisfy raises `Invalid` instead of returning an empty result.
|
|
57
86
|
|
|
58
87
|
```ts
|
|
59
88
|
import { compilePostgres, combinedPredicate } from '@avelonjs/postgres'
|
|
@@ -104,9 +133,7 @@ const db = createPostgresDatabase({
|
|
|
104
133
|
migrations: [
|
|
105
134
|
{
|
|
106
135
|
id: '20260804_create_posts',
|
|
107
|
-
up: [
|
|
108
|
-
'CREATE TABLE posts (id text PRIMARY KEY, title text NOT NULL)',
|
|
109
|
-
],
|
|
136
|
+
up: ['CREATE TABLE posts (id text PRIMARY KEY, title text NOT NULL)'],
|
|
110
137
|
down: ['DROP TABLE posts'],
|
|
111
138
|
},
|
|
112
139
|
],
|
|
@@ -121,13 +148,13 @@ await db.status()
|
|
|
121
148
|
|
|
122
149
|
Vendor SQLSTATE values never leave the driver. Unique violations become `Conflict`, unknown tables/columns/routines become `Invalid`, and connection failures become `Unavailable`. Every other code becomes `DriverFault`.
|
|
123
150
|
|
|
124
|
-
| SQLSTATE | Framework error | Meaning
|
|
125
|
-
|
|
126
|
-
| `23505`
|
|
127
|
-
| `42P01`
|
|
128
|
-
| `42703`
|
|
129
|
-
| `42883`
|
|
130
|
-
| `08006`
|
|
151
|
+
| SQLSTATE | Framework error | Meaning |
|
|
152
|
+
| -------- | --------------- | ------------------ |
|
|
153
|
+
| `23505` | `Conflict` | unique_violation |
|
|
154
|
+
| `42P01` | `Invalid` | undefined_table |
|
|
155
|
+
| `42703` | `Invalid` | undefined_column |
|
|
156
|
+
| `42883` | `Invalid` | undefined_function |
|
|
157
|
+
| `08006` | `Unavailable` | connection_failure |
|
|
131
158
|
|
|
132
159
|
## Live Conformance
|
|
133
160
|
|
|
@@ -141,40 +168,47 @@ bun test
|
|
|
141
168
|
|
|
142
169
|
## Method Reference
|
|
143
170
|
|
|
144
|
-
| Method
|
|
145
|
-
|
|
146
|
-
| `createPostgresDatabase`
|
|
147
|
-
| `postgresDatabaseCapabilities`
|
|
148
|
-
| `PostgresDatabaseOptions`
|
|
149
|
-
| `PostgresDatabase.execute`
|
|
150
|
-
| `PostgresDatabase.rpc`
|
|
151
|
-
| `PostgresDatabase.transaction`
|
|
152
|
-
| `PostgresDatabase.plan`
|
|
153
|
-
| `PostgresDatabase.apply`
|
|
154
|
-
| `PostgresDatabase.rollback`
|
|
155
|
-
| `PostgresDatabase.status`
|
|
156
|
-
| `PostgresDatabase.raw`
|
|
157
|
-
| `PostgresDatabase.resetFixtures` | `() => Promise<void>`
|
|
158
|
-
| `PostgresDatabase.close`
|
|
159
|
-
| `compilePostgres`
|
|
160
|
-
| `compileSqlPredicate`
|
|
161
|
-
| `CompiledSql`
|
|
162
|
-
| `normalizePredicate`
|
|
163
|
-
| `combinedPredicate`
|
|
164
|
-
| `mapPostgresError`
|
|
165
|
-
| `POSTGRES_ERROR_MAP`
|
|
166
|
-
| `validateQueryIR`
|
|
167
|
-
| `assertIdentifier`
|
|
168
|
-
| `assertQueryAgainstSchema`
|
|
169
|
-
| `loadSchemaCache`
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
171
|
+
| Method | Signature | Description |
|
|
172
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `createPostgresDatabase` | `(options?: PostgresDatabaseOptions) => PostgresDatabase` | Constructs a driver from options or `POSTGRES_URL` / `DATABASE_URL`. |
|
|
174
|
+
| `postgresDatabaseCapabilities` | `{ transactions: true, rowSecurity: false, maxRelationDepth: 8, fullTextSearch: false, upsert: true, returning: true, windowFunctions: true, jsonOperators: true }` | Exact capability declaration for the Postgres driver. |
|
|
175
|
+
| `PostgresDatabaseOptions` | `interface` | Connection URL, instance name, and optional migrations. |
|
|
176
|
+
| `PostgresDatabase.execute` | `(query: QueryIR) => Promise<QueryResult>` | Validates, compiles, and executes one query IR operation. |
|
|
177
|
+
| `PostgresDatabase.rpc` | `(routine: string, args: Readonly<Record<string, unknown>>) => Promise<T>` | Invokes a Postgres routine; missing routines raise `Invalid`. |
|
|
178
|
+
| `PostgresDatabase.transaction` | `(callback: (tx: DatabaseTransaction) => Promise<T>) => Promise<T>` | Runs the callback atomically and returns its result. |
|
|
179
|
+
| `PostgresDatabase.plan` | `() => Promise<MigrationPlan>` | Returns pending migration identifiers and SQL steps. |
|
|
180
|
+
| `PostgresDatabase.apply` | `() => Promise<readonly MigrationStatus[]>` | Applies pending migrations inside transactions. |
|
|
181
|
+
| `PostgresDatabase.rollback` | `(steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
182
|
+
| `PostgresDatabase.status` | `() => Promise<readonly MigrationStatus[]>` | Lists applied and pending migration states. |
|
|
183
|
+
| `PostgresDatabase.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
184
|
+
| `PostgresDatabase.resetFixtures` | `() => Promise<void>` | Recreates assay tables and `assay_echo` for live conformance. |
|
|
185
|
+
| `PostgresDatabase.close` | `() => Promise<void>` | Closes the underlying SQL client pool. |
|
|
186
|
+
| `compilePostgres` | `(ir: QueryIR, predicate?: Predicate) => CompiledSql` | Compiles IR to parameterized SQL without executing it. |
|
|
187
|
+
| `compileSqlPredicate` | `(predicate: Predicate, bind: (value: unknown) => string) => string` | Compiles a normalized predicate to a SQL boolean expression. |
|
|
188
|
+
| `CompiledSql` | `interface` | Parameterized `text` plus positional `parameters`. |
|
|
189
|
+
| `normalizePredicate` | `(predicate: Predicate) => Predicate` | Applies empty-list and constant identities. |
|
|
190
|
+
| `combinedPredicate` | `(ir: Pick<QueryIR, 'where' \| 'ward'>) => Predicate` | ANDs where and ward, then normalizes. |
|
|
191
|
+
| `mapPostgresError` | `(error: unknown, operation: string) => never` | Maps vendor failures into framework errors. |
|
|
192
|
+
| `POSTGRES_ERROR_MAP` | `readonly { sqlstate, framework, meaning }[]` | SQLSTATE values this driver maps into the taxonomy. |
|
|
193
|
+
| `validateQueryIR` | `(ir: QueryIR, maxRelationDepth: number) => void` | Rejects malformed IR before compilation. |
|
|
194
|
+
| `assertIdentifier` | `(value: unknown, path: string) => asserts value is string` | Rejects identifiers that are not simple SQL names. |
|
|
195
|
+
| `assertQueryAgainstSchema` | `(cache: SchemaCache, query: QueryIR) => void` | Rejects unknown public-schema tables and columns. |
|
|
196
|
+
| `loadSchemaCache` | `(sql: SQL) => Promise<SchemaCache>` | Loads public base tables and their columns. |
|
|
197
|
+
| `executeQueryIR` | `(runner: SqlRunner, schema: SchemaCache, ir: QueryIR, maxRelationDepth: number, onRoundTrip: RoundTripCounter) => Promise<QueryResult>` | Runs one query IR operation, relation loads included, against any client. |
|
|
198
|
+
| `executeRpc` | `(runner: SqlRunner, routine: string, args: Readonly<Record<string, unknown>>, onRoundTrip: RoundTripCounter) => Promise<unknown>` | Invokes a routine with a single jsonb argument against any client. |
|
|
199
|
+
| `RoundTripCounter` | `() => void` | Called once per statement so a driver can keep its round-trip counter. |
|
|
200
|
+
| `SqlRunner` | `interface` | `unsafe(text, parameters?)` returning normalized rows; the seam every client implements. |
|
|
201
|
+
| `SqlBatchRunner` | `interface` | A `SqlRunner` that also applies a known statement list atomically through `batch()`. |
|
|
202
|
+
| `SqlRows` | `interface` | Normalized `rows` plus the affected-row `count`. |
|
|
203
|
+
| `SchemaCache` | `type` | `Map` of table name to column set. |
|
|
204
|
+
| `planMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<MigrationPlan>` | Builds a pending plan from registered migrations and history. |
|
|
205
|
+
| `applyMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<readonly MigrationStatus[]>` | Applies pending migrations inside transactions. |
|
|
206
|
+
| `rollbackMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[], steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
207
|
+
| `statusMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<readonly MigrationStatus[]>` | Returns applied/pending status for every registered migration. |
|
|
208
|
+
| `PostgresMigration` | `interface` | Driver-owned `id`, `up`, and `down` SQL pair. |
|
|
209
|
+
| `resetAssayFixtures` | `(sql: SQL) => Promise<void>` | Provisions empty assay fixtures on a SQL client. |
|
|
210
|
+
| `ASSAY_FIXTURE_SQL` | `string` | SQL that drops and recreates the database conformance fixtures. |
|
|
211
|
+
| `ASSAY_FIXTURE_STATEMENTS` | `readonly string[]` | The same fixtures as one statement per entry, for clients that reject multi-statement text. |
|
|
178
212
|
|
|
179
213
|
## Testing
|
|
180
214
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avelonjs/postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Postgres database driver that compiles Avelon QueryIR to SQL.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"LICENSE"
|
|
31
31
|
],
|
|
32
32
|
"exports": {
|
|
33
|
-
".": "./src/index.ts"
|
|
33
|
+
".": "./src/index.ts",
|
|
34
|
+
"./sql": "./src/sql.ts"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
37
|
"test": "bun test",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SQL } from 'bun'
|
|
2
|
+
import type { SqlBatchRunner, SqlRows } from './sql-client'
|
|
3
|
+
|
|
4
|
+
function toSqlRows(result: unknown): SqlRows {
|
|
5
|
+
if (!Array.isArray(result)) return { rows: [], count: 0 }
|
|
6
|
+
const count = Reflect.get(result, 'count')
|
|
7
|
+
return {
|
|
8
|
+
rows: result.map((row) => ({ ...(row as Record<string, unknown>) })),
|
|
9
|
+
count: typeof count === 'number' ? count : result.length,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adapts Bun's SQL client to the vendor-neutral runner.
|
|
15
|
+
*
|
|
16
|
+
* Written as an explicit object because Bun's `begin` resolves to `SQL.ContextCallbackResult<T>`
|
|
17
|
+
* and `unsafe` returns `SQL.Query<T>`, neither of which matches the runner interface structurally.
|
|
18
|
+
*/
|
|
19
|
+
export function bunSqlRunner(sql: SQL): SqlBatchRunner {
|
|
20
|
+
return {
|
|
21
|
+
unsafe: async (text, parameters) =>
|
|
22
|
+
toSqlRows(await sql.unsafe(text, parameters === undefined ? undefined : [...parameters])),
|
|
23
|
+
batch: async (statements) => {
|
|
24
|
+
await sql.begin(async (tx) => {
|
|
25
|
+
for (const statement of statements) await tx.unsafe(statement.text, statement.parameters)
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
close: async () => {
|
|
29
|
+
await sql.close()
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/compile.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Invalid,
|
|
3
|
+
evaluatePredicate,
|
|
4
|
+
rowAllowedByWard,
|
|
5
|
+
type CompareOp,
|
|
6
|
+
type OrderTerm,
|
|
7
|
+
type Predicate,
|
|
8
|
+
type QueryIR,
|
|
9
|
+
} from '@avelonjs/core'
|
|
2
10
|
import { combinedPredicate } from './normalize'
|
|
3
11
|
import { assertIdentifier } from './validate'
|
|
4
12
|
|
|
@@ -46,16 +54,55 @@ function writeRows(ir: QueryIR): { rows: Row[]; columns: string[] } {
|
|
|
46
54
|
return { rows, columns }
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
/** A `where` on an insert or upsert has no row to scope, so only a constant true is documented. */
|
|
58
|
+
function ensureUnscopedWriteWhere(ir: QueryIR, mode: 'insert' | 'upsert'): void {
|
|
59
|
+
const where = combinedPredicate({ where: ir.where })
|
|
60
|
+
if (where.kind !== 'const' || !where.value) {
|
|
51
61
|
invalid(
|
|
52
|
-
`${mode} with a non-constant where
|
|
62
|
+
`${mode} with a non-constant where has no documented row scope.`,
|
|
53
63
|
'where',
|
|
54
64
|
`${mode} predicates are not documented`,
|
|
55
65
|
)
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
function refuseWardedRow(table: string): never {
|
|
70
|
+
invalid(
|
|
71
|
+
`The ward on ${table} refuses this row.`,
|
|
72
|
+
'ward',
|
|
73
|
+
`The ward on ${table} refuses this row.`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Refuses an insert or upsert whose row the ward would hide.
|
|
79
|
+
*
|
|
80
|
+
* The values carry the whole row, so this is the same check `MemoryDatabase` runs and the same
|
|
81
|
+
* answer a Postgres `WITH CHECK` policy would give. A ward column the values omit reads as unknown
|
|
82
|
+
* and is refused, matching row security treating unknown as not visible.
|
|
83
|
+
*/
|
|
84
|
+
function ensureWardAdmitsWrite(ir: QueryIR): void {
|
|
85
|
+
const ward = ir.ward
|
|
86
|
+
if (ward === undefined) return
|
|
87
|
+
for (const row of writeRows(ir).rows) {
|
|
88
|
+
if (!rowAllowedByWard(ward, row)) refuseWardedRow(ir.table)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Refuses an update that hands the row to another ward scope.
|
|
94
|
+
*
|
|
95
|
+
* Only the assigned columns are known here, so the ward is refused when those columns alone make
|
|
96
|
+
* it false. Columns the update leaves alone read as unknown and keep the row eligible; the ward in
|
|
97
|
+
* the `WHERE` clause still decides which rows the statement reaches.
|
|
98
|
+
*/
|
|
99
|
+
function ensureWardAdmitsUpdate(ir: QueryIR): void {
|
|
100
|
+
const ward = ir.ward
|
|
101
|
+
if (ward === undefined) return
|
|
102
|
+
const row = writeRows(ir).rows[0] as Row
|
|
103
|
+
if (evaluatePredicate(ward, row) === false) refuseWardedRow(ir.table)
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
/** Compiles a normalized predicate into a SQL boolean expression. */
|
|
60
107
|
export function compileSqlPredicate(
|
|
61
108
|
predicate: Predicate,
|
|
@@ -103,7 +150,10 @@ export function compilePostgres(ir: QueryIR, predicate = combinedPredicate(ir)):
|
|
|
103
150
|
parameters.push(value)
|
|
104
151
|
return `$${parameters.length}`
|
|
105
152
|
}
|
|
106
|
-
|
|
153
|
+
// Insert and upsert carry no row scope, so their ward is checked against the values instead of
|
|
154
|
+
// compiled into SQL. Binding it here would number the value placeholders past the ward's.
|
|
155
|
+
const rowScoped = ir.mode !== 'insert' && ir.mode !== 'upsert'
|
|
156
|
+
const where = rowScoped ? compileSqlPredicate(predicate, bind) : 'TRUE'
|
|
107
157
|
const whereClause = where === 'TRUE' ? '' : ` WHERE ${where}`
|
|
108
158
|
const order = compileSqlOrder(ir.order)
|
|
109
159
|
const limit = ir.limit === undefined ? '' : ` LIMIT ${bind(ir.limit)}`
|
|
@@ -121,7 +171,8 @@ export function compilePostgres(ir: QueryIR, predicate = combinedPredicate(ir)):
|
|
|
121
171
|
parameters,
|
|
122
172
|
}
|
|
123
173
|
case 'insert': {
|
|
124
|
-
|
|
174
|
+
ensureUnscopedWriteWhere(ir, 'insert')
|
|
175
|
+
ensureWardAdmitsWrite(ir)
|
|
125
176
|
const { rows, columns } = writeRows(ir)
|
|
126
177
|
const values = rows
|
|
127
178
|
.map((row) => `(${columns.map((column) => bind(row[column])).join(', ')})`)
|
|
@@ -132,6 +183,7 @@ export function compilePostgres(ir: QueryIR, predicate = combinedPredicate(ir)):
|
|
|
132
183
|
}
|
|
133
184
|
}
|
|
134
185
|
case 'update': {
|
|
186
|
+
ensureWardAdmitsUpdate(ir)
|
|
135
187
|
const row = writeRows(ir).rows[0] as Row
|
|
136
188
|
const assignments = Object.keys(row).map(
|
|
137
189
|
(column) => `${quoteIdentifier(column)} = ${bind(row[column])}`,
|
|
@@ -147,9 +199,11 @@ export function compilePostgres(ir: QueryIR, predicate = combinedPredicate(ir)):
|
|
|
147
199
|
parameters,
|
|
148
200
|
}
|
|
149
201
|
case 'upsert': {
|
|
150
|
-
|
|
202
|
+
ensureUnscopedWriteWhere(ir, 'upsert')
|
|
203
|
+
ensureWardAdmitsWrite(ir)
|
|
151
204
|
const conflict = ir.conflict
|
|
152
|
-
if (conflict === undefined)
|
|
205
|
+
if (conflict === undefined)
|
|
206
|
+
invalid('upsert requires conflict.', 'conflict', 'Required for upsert')
|
|
153
207
|
const { rows, columns } = writeRows(ir)
|
|
154
208
|
const values = rows
|
|
155
209
|
.map((row) => `(${columns.map((column) => bind(row[column])).join(', ')})`)
|