@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/snapshot.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// What PostgreSQL makes of the models (D-98, D-105): every table with its
|
|
2
|
+
// columns, keys, checks, indexes and foreign keys, as JSON that `plan`
|
|
3
|
+
// compares and that a database or `migrations/schema.json` keeps.
|
|
4
|
+
import type { ScalarType, SchemaField } from 'ultimabase'
|
|
5
|
+
import type { Catalog, Table } from './names.ts'
|
|
6
|
+
import { constraint } from './names.ts'
|
|
7
|
+
import { Refused } from 'ultimabase'
|
|
8
|
+
|
|
9
|
+
/** `field` is the model's name for it, which messages use. */
|
|
10
|
+
export type Column = {
|
|
11
|
+
readonly field: string
|
|
12
|
+
readonly type: string
|
|
13
|
+
readonly nullable: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ForeignKey = {
|
|
17
|
+
readonly column: string
|
|
18
|
+
readonly table: string
|
|
19
|
+
readonly cascade: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A column among `values`; each element of a list's (D-127). */
|
|
23
|
+
export type Check = {
|
|
24
|
+
readonly column: string
|
|
25
|
+
readonly values: string[]
|
|
26
|
+
readonly list?: true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `model` is the model's name, or `Book.tags` for a join table. Constraints
|
|
31
|
+
* and indexes are keyed by name, columns in table order.
|
|
32
|
+
*/
|
|
33
|
+
export type TableSnapshot = {
|
|
34
|
+
readonly model: string
|
|
35
|
+
readonly columns: { readonly [name: string]: Column }
|
|
36
|
+
readonly primaryKey: { readonly name: string; readonly columns: string[] }
|
|
37
|
+
readonly uniques: { readonly [name: string]: string[] }
|
|
38
|
+
readonly checks: { readonly [name: string]: Check }
|
|
39
|
+
readonly indexes: { readonly [name: string]: string[] }
|
|
40
|
+
/** A `contains` index's name → its column (D-141). */
|
|
41
|
+
readonly trigrams: { readonly [name: string]: string }
|
|
42
|
+
readonly foreignKeys: { readonly [name: string]: ForeignKey }
|
|
43
|
+
/** The `_search` column, from these columns, and its GIN index (D-148). */
|
|
44
|
+
readonly search: Search | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Search = {
|
|
48
|
+
readonly index: string
|
|
49
|
+
readonly language: string
|
|
50
|
+
readonly columns: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type Snapshot = {
|
|
54
|
+
readonly postgres: 1
|
|
55
|
+
readonly tables: { readonly [name: string]: TableSnapshot }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const types: { readonly [type in ScalarType]: string } = {
|
|
59
|
+
string: 'text collate "C"',
|
|
60
|
+
number: 'double precision',
|
|
61
|
+
decimal: 'numeric',
|
|
62
|
+
boolean: 'boolean',
|
|
63
|
+
dateTime: 'timestamptz',
|
|
64
|
+
date: 'date',
|
|
65
|
+
json: 'jsonb',
|
|
66
|
+
enum: 'text collate "C"',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A list's column: an enum's is text, as its single values (D-98, D-127). */
|
|
70
|
+
const lists: { readonly [type in ScalarType]?: string } = {
|
|
71
|
+
string: 'text[]',
|
|
72
|
+
number: 'double precision[]',
|
|
73
|
+
enum: 'text[]',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Building = {
|
|
77
|
+
model: string
|
|
78
|
+
columns: Record<string, Column>
|
|
79
|
+
primaryKey: { name: string; columns: string[] }
|
|
80
|
+
uniques: Record<string, string[]>
|
|
81
|
+
checks: Record<string, Check>
|
|
82
|
+
indexes: Record<string, string[]>
|
|
83
|
+
trigrams: Record<string, string>
|
|
84
|
+
foreignKeys: Record<string, ForeignKey>
|
|
85
|
+
search: Search | null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type Claim = (name: string, what: string) => string
|
|
89
|
+
|
|
90
|
+
/** Refuses two things stored under one name (D-98). */
|
|
91
|
+
function claimer(): Claim {
|
|
92
|
+
const owners = new Map<string, string>()
|
|
93
|
+
return (name, what) => {
|
|
94
|
+
const prior = owners.get(name)
|
|
95
|
+
if (prior !== undefined) {
|
|
96
|
+
throw new Refused(`${prior} and ${what} are both stored as "${name}".`)
|
|
97
|
+
}
|
|
98
|
+
owners.set(name, what)
|
|
99
|
+
return name
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Tables, keys and indexes share a schema's names; columns, a table's. */
|
|
104
|
+
type Claims = { readonly schema: Claim; readonly columns: Claim }
|
|
105
|
+
|
|
106
|
+
function empty(
|
|
107
|
+
claims: Claims,
|
|
108
|
+
model: string,
|
|
109
|
+
table: string,
|
|
110
|
+
keys: string[],
|
|
111
|
+
): Building {
|
|
112
|
+
const name = claims.schema(constraint(`${table}_pkey`), `the key of ${model}`)
|
|
113
|
+
return {
|
|
114
|
+
model,
|
|
115
|
+
columns: {},
|
|
116
|
+
primaryKey: { name, columns: keys },
|
|
117
|
+
uniques: {},
|
|
118
|
+
checks: {},
|
|
119
|
+
indexes: {},
|
|
120
|
+
trigrams: {},
|
|
121
|
+
foreignKeys: {},
|
|
122
|
+
search: null,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function index(
|
|
127
|
+
claims: Claims,
|
|
128
|
+
building: Building,
|
|
129
|
+
table: string,
|
|
130
|
+
columns: string[],
|
|
131
|
+
what: string,
|
|
132
|
+
): void {
|
|
133
|
+
const name = constraint(`${table}_${columns.join('_')}_idx`)
|
|
134
|
+
building.indexes[claims.schema(name, `the index on ${what}`)] = columns
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function field(
|
|
138
|
+
catalog: Catalog,
|
|
139
|
+
claims: Claims,
|
|
140
|
+
table: Table,
|
|
141
|
+
building: Building,
|
|
142
|
+
given: SchemaField,
|
|
143
|
+
): void {
|
|
144
|
+
if (given.kind === 'many' || (given.kind === 'one' && !given.stored)) return
|
|
145
|
+
const what = `${table.model.name}.${given.name}`
|
|
146
|
+
const column = claims.columns(catalog.column(table, given.name), what)
|
|
147
|
+
const type =
|
|
148
|
+
given.kind === 'one'
|
|
149
|
+
? 'uuid'
|
|
150
|
+
: given.integer === true
|
|
151
|
+
? `integer${given.list ? '[]' : ''}`
|
|
152
|
+
: given.type === 'decimal'
|
|
153
|
+
? `numeric(${given.precision},${given.scale})`
|
|
154
|
+
: ((given.list ? lists[given.type] : undefined) ?? types[given.type])
|
|
155
|
+
building.columns[column] = {
|
|
156
|
+
field: given.name,
|
|
157
|
+
type,
|
|
158
|
+
nullable: given.optional,
|
|
159
|
+
}
|
|
160
|
+
if (given.kind === 'scalar' && given.type === 'enum') {
|
|
161
|
+
const name = constraint(`${table.name}_${column}_check`)
|
|
162
|
+
building.checks[claims.schema(name, `the check on ${what}`)] = {
|
|
163
|
+
column,
|
|
164
|
+
values: [...(given.values ?? [])],
|
|
165
|
+
...(given.list ? { list: true as const } : {}),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (given.kind !== 'one') return
|
|
169
|
+
const key = constraint(`${table.name}_${column}_fkey`)
|
|
170
|
+
building.foreignKeys[claims.schema(key, `the foreign key of ${what}`)] = {
|
|
171
|
+
column,
|
|
172
|
+
table: catalog.table(given.target).name,
|
|
173
|
+
cascade: false,
|
|
174
|
+
}
|
|
175
|
+
if (!given.unique) index(claims, building, table.name, [column], what)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The fields of a key or an index, as `Thing(a, b)`. */
|
|
179
|
+
const on = (model: string, fields: readonly string[]) =>
|
|
180
|
+
`${model}(${fields.join(', ')})`
|
|
181
|
+
|
|
182
|
+
/** The snapshot of the catalog's tables, join tables last. */
|
|
183
|
+
export function snapshotOf(catalog: Catalog): Snapshot {
|
|
184
|
+
const tables: Record<string, Building> = {}
|
|
185
|
+
const schema = claimer()
|
|
186
|
+
for (const table of catalog.tables.values()) {
|
|
187
|
+
const model = table.model.name
|
|
188
|
+
const claims = { schema, columns: claimer() }
|
|
189
|
+
schema(table.name, `the table of ${model}`)
|
|
190
|
+
const building = empty(claims, model, table.name, ['id'])
|
|
191
|
+
for (const [column, name, type] of [
|
|
192
|
+
['id', 'id', 'uuid'],
|
|
193
|
+
['created_at', 'createdAt', 'timestamptz'],
|
|
194
|
+
['updated_at', 'updatedAt', 'timestamptz'],
|
|
195
|
+
] as const) {
|
|
196
|
+
claims.columns(column, `${model}.${name}`)
|
|
197
|
+
building.columns[column] = { field: name, type, nullable: false }
|
|
198
|
+
}
|
|
199
|
+
for (const given of table.model.fields) {
|
|
200
|
+
field(catalog, claims, table, building, given)
|
|
201
|
+
}
|
|
202
|
+
const [, ...uniques] = table.uniques
|
|
203
|
+
for (const { name, fields } of uniques) {
|
|
204
|
+
const what = `the unique key on ${on(model, fields)}`
|
|
205
|
+
building.uniques[schema(name, what)] = fields.map((given) =>
|
|
206
|
+
catalog.column(table, given),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
for (const { fields, unique, contains } of table.model.indexes) {
|
|
210
|
+
if (unique) continue
|
|
211
|
+
if (contains) {
|
|
212
|
+
const [column = ''] = fields.map((name) => catalog.column(table, name))
|
|
213
|
+
const name = constraint(`${table.name}_${column}_trgm`)
|
|
214
|
+
const what = `the contains index on ${on(model, fields)}`
|
|
215
|
+
building.trigrams[schema(name, what)] = column
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const columns = fields.map((name) => catalog.column(table, name))
|
|
219
|
+
index(claims, building, table.name, columns, on(model, fields))
|
|
220
|
+
}
|
|
221
|
+
const searched = table.model.fields.flatMap((given) =>
|
|
222
|
+
given.kind === 'scalar' && given.search === true
|
|
223
|
+
? [catalog.column(table, given.name)]
|
|
224
|
+
: [],
|
|
225
|
+
)
|
|
226
|
+
if (searched.length > 0) {
|
|
227
|
+
claims.columns('_search', `the search of ${model}`)
|
|
228
|
+
const name = constraint(`${table.name}__search_idx`)
|
|
229
|
+
building.search = {
|
|
230
|
+
index: schema(name, `the search index of ${model}`),
|
|
231
|
+
language: catalog.language,
|
|
232
|
+
columns: searched,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
tables[table.name] = building
|
|
236
|
+
}
|
|
237
|
+
for (const [side, join] of catalog.joins) {
|
|
238
|
+
// Both sides of a relation share its table.
|
|
239
|
+
if (side !== join.relation) continue
|
|
240
|
+
const claims = { schema, columns: claimer() }
|
|
241
|
+
schema(join.table, `the join table of ${side}`)
|
|
242
|
+
const building = empty(claims, side, join.table, [join.self, join.other])
|
|
243
|
+
for (const column of [join.self, join.other]) {
|
|
244
|
+
building.columns[column] = {
|
|
245
|
+
field: column,
|
|
246
|
+
type: 'uuid',
|
|
247
|
+
nullable: false,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const [column, table] of [
|
|
251
|
+
[join.self, join.owner],
|
|
252
|
+
[join.other, join.target],
|
|
253
|
+
] as const) {
|
|
254
|
+
const key = constraint(`${join.table}_${column}_fkey`)
|
|
255
|
+
building.foreignKeys[schema(key, `the foreign key of ${side}`)] = {
|
|
256
|
+
column,
|
|
257
|
+
table,
|
|
258
|
+
cascade: true,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
index(claims, building, join.table, [join.other], side)
|
|
262
|
+
tables[join.table] = building
|
|
263
|
+
}
|
|
264
|
+
return { postgres: 1, tables }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
268
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
269
|
+
|
|
270
|
+
const isStrings = (value: unknown): value is string[] =>
|
|
271
|
+
Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
272
|
+
|
|
273
|
+
/** Each entry of a record, checked by `read`, or `undefined`. */
|
|
274
|
+
function entries<T>(
|
|
275
|
+
value: unknown,
|
|
276
|
+
read: (item: unknown) => T | undefined,
|
|
277
|
+
): Record<string, T> | undefined {
|
|
278
|
+
if (!isRecord(value)) return undefined
|
|
279
|
+
const result: Record<string, T> = {}
|
|
280
|
+
for (const [key, item] of Object.entries(value)) {
|
|
281
|
+
const checked = read(item)
|
|
282
|
+
if (checked === undefined) return undefined
|
|
283
|
+
result[key] = checked
|
|
284
|
+
}
|
|
285
|
+
return result
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const columnOf = (value: unknown): Column | undefined =>
|
|
289
|
+
isRecord(value) &&
|
|
290
|
+
typeof value['field'] === 'string' &&
|
|
291
|
+
typeof value['type'] === 'string' &&
|
|
292
|
+
typeof value['nullable'] === 'boolean'
|
|
293
|
+
? {
|
|
294
|
+
field: value['field'],
|
|
295
|
+
type: value['type'],
|
|
296
|
+
nullable: value['nullable'],
|
|
297
|
+
}
|
|
298
|
+
: undefined
|
|
299
|
+
|
|
300
|
+
const checkOf = (value: unknown): Check | undefined =>
|
|
301
|
+
isRecord(value) &&
|
|
302
|
+
typeof value['column'] === 'string' &&
|
|
303
|
+
isStrings(value['values'])
|
|
304
|
+
? {
|
|
305
|
+
column: value['column'],
|
|
306
|
+
values: value['values'],
|
|
307
|
+
...(value['list'] === true ? { list: true as const } : {}),
|
|
308
|
+
}
|
|
309
|
+
: undefined
|
|
310
|
+
|
|
311
|
+
const foreignKeyOf = (value: unknown): ForeignKey | undefined =>
|
|
312
|
+
isRecord(value) &&
|
|
313
|
+
typeof value['column'] === 'string' &&
|
|
314
|
+
typeof value['table'] === 'string' &&
|
|
315
|
+
typeof value['cascade'] === 'boolean'
|
|
316
|
+
? {
|
|
317
|
+
column: value['column'],
|
|
318
|
+
table: value['table'],
|
|
319
|
+
cascade: value['cascade'],
|
|
320
|
+
}
|
|
321
|
+
: undefined
|
|
322
|
+
|
|
323
|
+
const stringsOf = (value: unknown): string[] | undefined =>
|
|
324
|
+
isStrings(value) ? value : undefined
|
|
325
|
+
|
|
326
|
+
/** A search, `null` for none, or `undefined` when it is neither. */
|
|
327
|
+
const searchOf = (value: unknown): Search | null | undefined =>
|
|
328
|
+
value === null
|
|
329
|
+
? null
|
|
330
|
+
: isRecord(value) &&
|
|
331
|
+
typeof value['index'] === 'string' &&
|
|
332
|
+
typeof value['language'] === 'string' &&
|
|
333
|
+
isStrings(value['columns'])
|
|
334
|
+
? {
|
|
335
|
+
index: value['index'],
|
|
336
|
+
language: value['language'],
|
|
337
|
+
columns: value['columns'],
|
|
338
|
+
}
|
|
339
|
+
: undefined
|
|
340
|
+
|
|
341
|
+
function tableOf(value: unknown): TableSnapshot | undefined {
|
|
342
|
+
if (!isRecord(value)) return undefined
|
|
343
|
+
const key = value['primaryKey']
|
|
344
|
+
const columns = entries(value['columns'], columnOf)
|
|
345
|
+
const uniques = entries(value['uniques'], stringsOf)
|
|
346
|
+
const checks = entries(value['checks'], checkOf)
|
|
347
|
+
const indexes = entries(value['indexes'], stringsOf)
|
|
348
|
+
const trigrams = entries(value['trigrams'], (item) =>
|
|
349
|
+
typeof item === 'string' ? item : undefined,
|
|
350
|
+
)
|
|
351
|
+
const foreignKeys = entries(value['foreignKeys'], foreignKeyOf)
|
|
352
|
+
const search = searchOf(value['search'])
|
|
353
|
+
const model = value['model']
|
|
354
|
+
if (
|
|
355
|
+
typeof model !== 'string' ||
|
|
356
|
+
!isRecord(key) ||
|
|
357
|
+
typeof key['name'] !== 'string' ||
|
|
358
|
+
!isStrings(key['columns']) ||
|
|
359
|
+
columns === undefined ||
|
|
360
|
+
uniques === undefined ||
|
|
361
|
+
checks === undefined ||
|
|
362
|
+
indexes === undefined ||
|
|
363
|
+
trigrams === undefined ||
|
|
364
|
+
foreignKeys === undefined ||
|
|
365
|
+
search === undefined
|
|
366
|
+
) {
|
|
367
|
+
return undefined
|
|
368
|
+
}
|
|
369
|
+
const primaryKey = { name: key['name'], columns: key['columns'] }
|
|
370
|
+
return {
|
|
371
|
+
model,
|
|
372
|
+
columns,
|
|
373
|
+
primaryKey,
|
|
374
|
+
uniques,
|
|
375
|
+
checks,
|
|
376
|
+
indexes,
|
|
377
|
+
trigrams,
|
|
378
|
+
foreignKeys,
|
|
379
|
+
search,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A snapshot read back from a database or a file, checked. */
|
|
384
|
+
export function parseSnapshot(value: unknown): Snapshot {
|
|
385
|
+
const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined
|
|
386
|
+
if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
|
|
387
|
+
throw new Error('The schema snapshot is not one postgres() wrote.')
|
|
388
|
+
}
|
|
389
|
+
return { postgres: 1, tables }
|
|
390
|
+
}
|