@ultimabase/postgres 0.0.0-stage → 0.1.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/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +254 -0
- package/dist/aggregate.d.ts +8 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +138 -0
- package/dist/decode.d.ts +5 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +30 -0
- package/dist/fills.d.ts +16 -0
- package/dist/fills.d.ts.map +1 -0
- package/dist/fills.js +37 -0
- package/dist/filter.d.ts +50 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +247 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/json-filter.d.ts +12 -0
- package/dist/json-filter.d.ts.map +1 -0
- package/dist/json-filter.js +65 -0
- package/dist/listen.d.ts +10 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +92 -0
- package/dist/logic.d.ts +7 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +18 -0
- package/dist/migrations.d.ts +23 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +217 -0
- package/dist/names.d.ts +66 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +175 -0
- package/dist/notify.d.ts +19 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +75 -0
- package/dist/numbers.d.ts +13 -0
- package/dist/numbers.d.ts.map +1 -0
- package/dist/numbers.js +49 -0
- package/dist/parts.d.ts +36 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +99 -0
- package/dist/plan-indexes.d.ts +12 -0
- package/dist/plan-indexes.d.ts.map +1 -0
- package/dist/plan-indexes.js +49 -0
- package/dist/plan-parts.d.ts +16 -0
- package/dist/plan-parts.d.ts.map +1 -0
- package/dist/plan-parts.js +9 -0
- package/dist/plan.d.ts +6 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +278 -0
- package/dist/pool.d.ts +5 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +41 -0
- package/dist/read.d.ts +47 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +227 -0
- package/dist/scratch.d.ts +22 -0
- package/dist/scratch.d.ts.map +1 -0
- package/dist/scratch.js +77 -0
- package/dist/snapshot.d.ts +66 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +272 -0
- package/dist/write.d.ts +17 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +296 -0
- package/package.json +45 -3
- package/src/adapter.ts +349 -0
- package/src/aggregate.ts +168 -0
- package/src/decode.ts +46 -0
- package/src/fills.ts +52 -0
- package/src/filter.ts +306 -0
- package/src/index.ts +20 -0
- package/src/json-filter.ts +74 -0
- package/src/listen.ts +97 -0
- package/src/logic.ts +28 -0
- package/src/migrations.ts +275 -0
- package/src/names.ts +217 -0
- package/src/notify.ts +101 -0
- package/src/numbers.ts +64 -0
- package/src/parts.ts +143 -0
- package/src/plan-indexes.ts +100 -0
- package/src/plan-parts.ts +35 -0
- package/src/plan.ts +389 -0
- package/src/pool.ts +63 -0
- package/src/read.ts +287 -0
- package/src/scratch.ts +92 -0
- package/src/snapshot.ts +390 -0
- package/src/write.ts +384 -0
package/src/write.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// A write as the fewest statements (D-99): its checks, then each change as a
|
|
2
|
+
// data-modifying CTE that runs only when they pass, then the read, which sees
|
|
3
|
+
// the changes through CTEs named as the written tables, or, with `before`,
|
|
4
|
+
// takes the rows its model's delete removed from that delete's RETURNING
|
|
5
|
+
// (D-102, D-104), or, with no such delete, those its update changed (D-133). A read of the one row a change writes takes it from that
|
|
6
|
+
// change's RETURNING (D-103). The unique
|
|
7
|
+
// constraint a refused change names is the kernel's `UniqueViolation` (D-98).
|
|
8
|
+
import type { RawBuilder } from 'kysely'
|
|
9
|
+
import { sql } from 'kysely'
|
|
10
|
+
import type { Change, Check, Filter, Read, Row, Write } from 'ultimabase'
|
|
11
|
+
import { RelationViolation, UniqueViolation } from 'ultimabase'
|
|
12
|
+
import type { Sql } from './filter.ts'
|
|
13
|
+
import { Filters, uuid, value } from './filter.ts'
|
|
14
|
+
import type { Catalog, Table } from './names.ts'
|
|
15
|
+
import { computed } from './numbers.ts'
|
|
16
|
+
import type { Part } from './parts.ts'
|
|
17
|
+
import { fieldsOf, grouped, inserted, sizeOf } from './parts.ts'
|
|
18
|
+
import { notifying, pairs as linked, rows as noted } from './notify.ts'
|
|
19
|
+
import { compileRows } from './read.ts'
|
|
20
|
+
|
|
21
|
+
/** A statement of a write: how many checks its result carries, `c0`… */
|
|
22
|
+
export type Statement = {
|
|
23
|
+
readonly sql: RawBuilder<{ readonly [column: string]: unknown }>
|
|
24
|
+
readonly checks: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assignments(
|
|
28
|
+
catalog: Catalog,
|
|
29
|
+
table: Table,
|
|
30
|
+
values: { readonly [field: string]: unknown },
|
|
31
|
+
): (readonly [string, Sql])[] {
|
|
32
|
+
return Object.entries(values)
|
|
33
|
+
.filter(([, item]) => item !== undefined)
|
|
34
|
+
.map(([name, item]) => [
|
|
35
|
+
catalog.column(table, name),
|
|
36
|
+
value(catalog.kind(table, name), item),
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const and = (gate: Sql | undefined): Sql =>
|
|
41
|
+
gate === undefined ? sql`` : sql` and ${gate}`
|
|
42
|
+
|
|
43
|
+
/** The id a filter selects by `{ id: { equals } }` alone, if any. */
|
|
44
|
+
function idOf(where: Filter | undefined): string | undefined {
|
|
45
|
+
if (where === undefined || Object.keys(where).length !== 1) return undefined
|
|
46
|
+
const id = where['id']
|
|
47
|
+
if (typeof id !== 'object' || id === null || Object.keys(id).length !== 1) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
return 'equals' in id && typeof id.equals === 'string' ? id.equals : undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The CTE of a change, `undefined` when it changes nothing. */
|
|
54
|
+
function part(
|
|
55
|
+
catalog: Catalog,
|
|
56
|
+
change: Exclude<Change, { readonly kind: 'insert' }>,
|
|
57
|
+
removed: ReadonlyMap<number, string>,
|
|
58
|
+
): Part | undefined {
|
|
59
|
+
if (change.kind !== 'update' && change.kind !== 'delete') {
|
|
60
|
+
return links(catalog, change)
|
|
61
|
+
}
|
|
62
|
+
const table = catalog.table(change.model)
|
|
63
|
+
const name = sql.id(table.name)
|
|
64
|
+
const base = { table: table.name, key: ['id'] }
|
|
65
|
+
const filters = new Filters(catalog, removed)
|
|
66
|
+
const named = filters.alias()
|
|
67
|
+
const alias = sql.id(named)
|
|
68
|
+
const where = filters.where(change.model, named, change.where)
|
|
69
|
+
const reads = new Set([table.name, ...filters.tables])
|
|
70
|
+
if (change.kind === 'update') {
|
|
71
|
+
const fields = [
|
|
72
|
+
...Object.keys(change.set),
|
|
73
|
+
...Object.keys(change.compute ?? {}),
|
|
74
|
+
]
|
|
75
|
+
const set = sql.join([
|
|
76
|
+
...assignments(catalog, table, change.set).map(
|
|
77
|
+
([column, item]) => sql`${sql.id(column)} = ${item}`,
|
|
78
|
+
),
|
|
79
|
+
...computed(catalog, table, change.compute),
|
|
80
|
+
])
|
|
81
|
+
const row = idOf(change.where)
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
...(row === undefined ? {} : { row }),
|
|
85
|
+
build: (gate) =>
|
|
86
|
+
sql`update ${name} as ${alias} set ${set} where ${where}${and(gate)} returning ${alias}.*`,
|
|
87
|
+
inserts: false,
|
|
88
|
+
removes: true,
|
|
89
|
+
adds: true,
|
|
90
|
+
reads,
|
|
91
|
+
noted: noted('update', change.model, fields),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
build: (gate) =>
|
|
97
|
+
sql`delete from ${name} as ${alias} where ${where}${and(gate)} returning ${alias}.*`,
|
|
98
|
+
inserts: false,
|
|
99
|
+
removes: true,
|
|
100
|
+
adds: false,
|
|
101
|
+
reads,
|
|
102
|
+
noted: noted('delete', change.model),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A link or unlink of nothing, or of ids no row has, changes nothing. */
|
|
107
|
+
function links(
|
|
108
|
+
catalog: Catalog,
|
|
109
|
+
change: Extract<Change, { readonly kind: 'link' | 'unlink' }>,
|
|
110
|
+
): Part | undefined {
|
|
111
|
+
const join = catalog.join(change.model, change.field)
|
|
112
|
+
const name = sql.id(join.table)
|
|
113
|
+
const self = sql.id(join.self)
|
|
114
|
+
const other = sql.id(join.other)
|
|
115
|
+
const key = [join.self, join.other].toSorted()
|
|
116
|
+
const id = sql`cast(${change.id} as uuid)`
|
|
117
|
+
// The same pair, whichever side names it.
|
|
118
|
+
const pairOf = (target: string): string =>
|
|
119
|
+
key[0] === join.self ? `${change.id}:${target}` : `${target}:${change.id}`
|
|
120
|
+
const pairs = (targets: readonly string[]) => ({
|
|
121
|
+
table: join.table,
|
|
122
|
+
key,
|
|
123
|
+
reads: new Set<string>(),
|
|
124
|
+
pairs: new Set(targets.map(pairOf)),
|
|
125
|
+
noted: linked(
|
|
126
|
+
change.kind,
|
|
127
|
+
change.model,
|
|
128
|
+
change.field,
|
|
129
|
+
join.self,
|
|
130
|
+
join.other,
|
|
131
|
+
),
|
|
132
|
+
})
|
|
133
|
+
if (change.kind === 'unlink') {
|
|
134
|
+
const targets = change.targets.filter((target) => uuid.test(target))
|
|
135
|
+
if (targets.length === 0) return undefined
|
|
136
|
+
const list = sql.join(targets.map((target) => sql`cast(${target} as uuid)`))
|
|
137
|
+
return {
|
|
138
|
+
...pairs(targets),
|
|
139
|
+
build: (gate) =>
|
|
140
|
+
sql`delete from ${name} where ${self} = ${id} and ${other} in (${list})${and(gate)} returning *`,
|
|
141
|
+
inserts: false,
|
|
142
|
+
removes: true,
|
|
143
|
+
adds: false,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (change.targets.length === 0) return undefined
|
|
147
|
+
const rows = sql.join(
|
|
148
|
+
change.targets.map((target) => sql`(${id}, cast(${target} as uuid))`),
|
|
149
|
+
)
|
|
150
|
+
return {
|
|
151
|
+
...pairs(change.targets),
|
|
152
|
+
build: (gate) => {
|
|
153
|
+
const source =
|
|
154
|
+
gate === undefined
|
|
155
|
+
? sql`values ${rows}`
|
|
156
|
+
: sql`select * from (values ${rows}) as "v" where ${gate}`
|
|
157
|
+
return sql`insert into ${name} (${self}, ${other}) ${source} on conflict do nothing returning *`
|
|
158
|
+
},
|
|
159
|
+
inserts: true,
|
|
160
|
+
removes: false,
|
|
161
|
+
adds: true,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checking(catalog: Catalog, checks: readonly Check[]): Sql {
|
|
166
|
+
const filters = new Filters(catalog)
|
|
167
|
+
const columns = checks.map((check, at) => {
|
|
168
|
+
const table = sql.id(catalog.table(check.model).name)
|
|
169
|
+
const alias = filters.alias()
|
|
170
|
+
const where = filters.where(check.model, alias, check.where)
|
|
171
|
+
// A locked row is counted as it is after any wait for its lock.
|
|
172
|
+
const rows = sql`${table} as ${sql.id(alias)} where ${where}`
|
|
173
|
+
const from = check.lock ? sql`(select from ${rows} for update) as t` : rows
|
|
174
|
+
return sql`(select count(*) from ${from}) = ${check.count} as ${sql.id(`c${at}`)}`
|
|
175
|
+
})
|
|
176
|
+
return sql`select ${sql.join(columns)}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cteName = (at: number): string => `#w${at}`
|
|
180
|
+
const cte = (at: number): Sql => sql.id(cteName(at))
|
|
181
|
+
|
|
182
|
+
/** Each written table as the read sees it: the changed rows in place. */
|
|
183
|
+
function views(parts: ReadonlyMap<number, Part>): Sql[] {
|
|
184
|
+
const tables = new Map<string, [number, Part][]>()
|
|
185
|
+
for (const [at, each] of parts) {
|
|
186
|
+
tables.set(each.table, [...(tables.get(each.table) ?? []), [at, each]])
|
|
187
|
+
}
|
|
188
|
+
return [...tables].map(([table, written]) => {
|
|
189
|
+
const name = sql.id(table)
|
|
190
|
+
const key = written[0]?.[1].key ?? []
|
|
191
|
+
const keys = sql.join(key.map((column) => sql.id(column)))
|
|
192
|
+
const removed = written
|
|
193
|
+
.filter(([, each]) => each.removes)
|
|
194
|
+
.map(([at]) => sql`select ${keys} from ${cte(at)}`)
|
|
195
|
+
const kept =
|
|
196
|
+
removed.length === 0
|
|
197
|
+
? sql`select * from ${name}`
|
|
198
|
+
: sql`select * from ${name} where (${keys}) not in (${sql.join(removed, sql` union all `)})`
|
|
199
|
+
const added = written
|
|
200
|
+
.filter(([, each]) => each.adds)
|
|
201
|
+
.map(([at]) => sql`select * from ${cte(at)}`)
|
|
202
|
+
const body = sql.join([kept, ...added], sql` union all `)
|
|
203
|
+
return sql`${name} as not materialized (${body})`
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The change of a statement that writes the one row `read` selects, when no
|
|
209
|
+
* other change of it writes that table: the read takes the row from its
|
|
210
|
+
* RETURNING rather than from the table's view (D-103).
|
|
211
|
+
*/
|
|
212
|
+
function sourceOf(
|
|
213
|
+
catalog: Catalog,
|
|
214
|
+
read: Read,
|
|
215
|
+
parts: ReadonlyMap<number, Part>,
|
|
216
|
+
): number | undefined {
|
|
217
|
+
const id = idOf(read.where)
|
|
218
|
+
const table = catalog.table(read.model).name
|
|
219
|
+
const written = [...parts].filter(([, each]) => each.table === table)
|
|
220
|
+
const [only] = written
|
|
221
|
+
return id !== undefined && written.length === 1 && only?.[1].row === id
|
|
222
|
+
? only[0]
|
|
223
|
+
: undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The statements of a write, in the order they run. */
|
|
227
|
+
export function compileWrite(
|
|
228
|
+
catalog: Catalog,
|
|
229
|
+
write: Write,
|
|
230
|
+
/** Where the feed's notifications go, when it has a taker (D-154). */
|
|
231
|
+
channel?: string,
|
|
232
|
+
): Statement[] {
|
|
233
|
+
const checks = write.checks ?? []
|
|
234
|
+
// Each statement stays within PostgreSQL's 65,535 parameters, its checks
|
|
235
|
+
// and its read's aside.
|
|
236
|
+
const room =
|
|
237
|
+
65_535 -
|
|
238
|
+
sizeOf(checking(catalog, checks)) -
|
|
239
|
+
(write.read === undefined
|
|
240
|
+
? 0
|
|
241
|
+
: sizeOf(compileRows(catalog, write.read, undefined).rows))
|
|
242
|
+
const parts: Part[] = []
|
|
243
|
+
const removed = new Map<number, string>()
|
|
244
|
+
// Consecutive rows of a table, setting the same fields, go in one insert.
|
|
245
|
+
let batch: { readonly model: string; readonly rows: Row[] } | undefined
|
|
246
|
+
const flush = (): void => {
|
|
247
|
+
if (batch !== undefined) {
|
|
248
|
+
parts.push(inserted(catalog, batch.model, batch.rows))
|
|
249
|
+
}
|
|
250
|
+
batch = undefined
|
|
251
|
+
}
|
|
252
|
+
for (const [index, change] of write.changes.entries()) {
|
|
253
|
+
if (change.kind === 'insert') {
|
|
254
|
+
const fields = fieldsOf(change.row)
|
|
255
|
+
const joins =
|
|
256
|
+
batch?.model === change.model &&
|
|
257
|
+
fieldsOf(batch.rows[0] ?? {})
|
|
258
|
+
.toSorted()
|
|
259
|
+
.join() === fields.toSorted().join() &&
|
|
260
|
+
(batch.rows.length + 1) * fields.length <= room
|
|
261
|
+
if (!joins) {
|
|
262
|
+
flush()
|
|
263
|
+
batch = { model: change.model, rows: [] }
|
|
264
|
+
}
|
|
265
|
+
batch?.rows.push(change.row)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
flush()
|
|
269
|
+
const each = part(catalog, change, removed)
|
|
270
|
+
if (each === undefined) continue
|
|
271
|
+
if (change.kind === 'delete') removed.set(index, cteName(parts.length))
|
|
272
|
+
parts.push(each)
|
|
273
|
+
}
|
|
274
|
+
flush()
|
|
275
|
+
if (parts.length === 0 && checks.length === 0 && write.read === undefined) {
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
const columns = checks.map((_, at) => sql.id(`c${at}`))
|
|
279
|
+
const gate =
|
|
280
|
+
checks.length === 0
|
|
281
|
+
? undefined
|
|
282
|
+
: sql`(select ${sql.join(columns, sql` and `)} from "#checks")`
|
|
283
|
+
const groups = grouped(parts, room)
|
|
284
|
+
const before = write.before === true
|
|
285
|
+
const table = write.read && catalog.table(write.read.model).name
|
|
286
|
+
// With `before`, the read runs beside its model's delete (D-104), or,
|
|
287
|
+
// when the write deletes nothing of it, its update (D-133).
|
|
288
|
+
const of = (adds: boolean): number =>
|
|
289
|
+
parts.findIndex(
|
|
290
|
+
(each) =>
|
|
291
|
+
before && each.table === table && each.removes && each.adds === adds,
|
|
292
|
+
)
|
|
293
|
+
const deleted = of(false)
|
|
294
|
+
const removal = deleted === -1 ? of(true) : deleted
|
|
295
|
+
let next = 0
|
|
296
|
+
return groups.map((group, index): Statement => {
|
|
297
|
+
const first = index === 0 && checks.length > 0
|
|
298
|
+
const numbered = new Map(group.map((each) => [next++, each]))
|
|
299
|
+
const ctes = [...numbered].map(
|
|
300
|
+
([at, each]) =>
|
|
301
|
+
sql`${cte(at)} as (${each.build(first ? gate : undefined)})`,
|
|
302
|
+
)
|
|
303
|
+
const reads = before ? numbered.has(removal) : index === groups.length - 1
|
|
304
|
+
const read = reads ? write.read : undefined
|
|
305
|
+
const seen = read !== undefined && !before
|
|
306
|
+
const source = seen ? sourceOf(catalog, read, numbered) : removal
|
|
307
|
+
const compiled =
|
|
308
|
+
read === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: compileRows(
|
|
311
|
+
catalog,
|
|
312
|
+
read,
|
|
313
|
+
source === undefined ? undefined : cte(source),
|
|
314
|
+
)
|
|
315
|
+
const fed = channel !== undefined && numbered.size > 0
|
|
316
|
+
const selected = [
|
|
317
|
+
...(first ? columns : []),
|
|
318
|
+
...(compiled === undefined ? [] : [sql`${compiled.rows} as "rows"`]),
|
|
319
|
+
// Referenced, the CTE runs.
|
|
320
|
+
...(fed ? [sql`(select count(*) from "#feed") as "fed"`] : []),
|
|
321
|
+
]
|
|
322
|
+
// The root's table needs no view when only its RETURNING is read.
|
|
323
|
+
const root = read === undefined ? '' : catalog.table(read.model).name
|
|
324
|
+
const viewed = [...numbered].filter(
|
|
325
|
+
([, each]) =>
|
|
326
|
+
source === undefined ||
|
|
327
|
+
each.table !== root ||
|
|
328
|
+
compiled?.tables.has(root) === true,
|
|
329
|
+
)
|
|
330
|
+
const all = [
|
|
331
|
+
...(first ? [sql`"#checks" as (${checking(catalog, checks)})`] : []),
|
|
332
|
+
...ctes,
|
|
333
|
+
...(seen ? views(new Map(viewed)) : []),
|
|
334
|
+
...(fed
|
|
335
|
+
? [
|
|
336
|
+
notifying(
|
|
337
|
+
channel,
|
|
338
|
+
[...numbered].map(([at, each]) => [cte(at), each.noted] as const),
|
|
339
|
+
),
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
342
|
+
]
|
|
343
|
+
const list = selected.length === 0 ? sql`1 as "one"` : sql.join(selected)
|
|
344
|
+
const body = sql`select ${list}${first ? sql` from "#checks"` : sql``}`
|
|
345
|
+
return {
|
|
346
|
+
sql: all.length === 0 ? sql`${body}` : sql`with ${sql.join(all)} ${body}`,
|
|
347
|
+
checks: first ? checks.length : 0,
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
type Refusal = {
|
|
353
|
+
readonly code: string
|
|
354
|
+
readonly constraint: string
|
|
355
|
+
readonly message: string
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isRefusal(error: unknown): error is Refusal {
|
|
359
|
+
return (
|
|
360
|
+
error instanceof Error &&
|
|
361
|
+
'code' in error &&
|
|
362
|
+
typeof error.code === 'string' &&
|
|
363
|
+
'constraint' in error &&
|
|
364
|
+
typeof error.constraint === 'string'
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** A constraint's refusal as the kernel's error; any other as is. */
|
|
369
|
+
export function translate(catalog: Catalog, error: unknown): unknown {
|
|
370
|
+
if (!isRefusal(error)) return error
|
|
371
|
+
if (error.code === '23505') {
|
|
372
|
+
const unique = catalog.uniques.get(error.constraint)
|
|
373
|
+
if (unique !== undefined) {
|
|
374
|
+
return new UniqueViolation(unique.model, unique.fields)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const key = catalog.foreignKeys.get(error.constraint)
|
|
378
|
+
if (error.code === '23503' && key !== undefined) {
|
|
379
|
+
// Only the message tells the deleting side from the linking one.
|
|
380
|
+
const restricted = error.message.startsWith('update or delete')
|
|
381
|
+
return new RelationViolation(key.model, key.field, key.target, restricted)
|
|
382
|
+
}
|
|
383
|
+
return error
|
|
384
|
+
}
|