@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -2
  3. package/dist/adapter.d.ts +26 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +254 -0
  6. package/dist/aggregate.d.ts +8 -0
  7. package/dist/aggregate.d.ts.map +1 -0
  8. package/dist/aggregate.js +138 -0
  9. package/dist/decode.d.ts +5 -0
  10. package/dist/decode.d.ts.map +1 -0
  11. package/dist/decode.js +30 -0
  12. package/dist/fills.d.ts +16 -0
  13. package/dist/fills.d.ts.map +1 -0
  14. package/dist/fills.js +37 -0
  15. package/dist/filter.d.ts +50 -0
  16. package/dist/filter.d.ts.map +1 -0
  17. package/dist/filter.js +247 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +11 -0
  21. package/dist/json-filter.d.ts +12 -0
  22. package/dist/json-filter.d.ts.map +1 -0
  23. package/dist/json-filter.js +65 -0
  24. package/dist/listen.d.ts +10 -0
  25. package/dist/listen.d.ts.map +1 -0
  26. package/dist/listen.js +92 -0
  27. package/dist/logic.d.ts +7 -0
  28. package/dist/logic.d.ts.map +1 -0
  29. package/dist/logic.js +18 -0
  30. package/dist/migrations.d.ts +23 -0
  31. package/dist/migrations.d.ts.map +1 -0
  32. package/dist/migrations.js +217 -0
  33. package/dist/names.d.ts +66 -0
  34. package/dist/names.d.ts.map +1 -0
  35. package/dist/names.js +175 -0
  36. package/dist/notify.d.ts +19 -0
  37. package/dist/notify.d.ts.map +1 -0
  38. package/dist/notify.js +75 -0
  39. package/dist/numbers.d.ts +13 -0
  40. package/dist/numbers.d.ts.map +1 -0
  41. package/dist/numbers.js +49 -0
  42. package/dist/parts.d.ts +36 -0
  43. package/dist/parts.d.ts.map +1 -0
  44. package/dist/parts.js +99 -0
  45. package/dist/plan-indexes.d.ts +12 -0
  46. package/dist/plan-indexes.d.ts.map +1 -0
  47. package/dist/plan-indexes.js +49 -0
  48. package/dist/plan-parts.d.ts +16 -0
  49. package/dist/plan-parts.d.ts.map +1 -0
  50. package/dist/plan-parts.js +9 -0
  51. package/dist/plan.d.ts +6 -0
  52. package/dist/plan.d.ts.map +1 -0
  53. package/dist/plan.js +278 -0
  54. package/dist/pool.d.ts +5 -0
  55. package/dist/pool.d.ts.map +1 -0
  56. package/dist/pool.js +41 -0
  57. package/dist/read.d.ts +47 -0
  58. package/dist/read.d.ts.map +1 -0
  59. package/dist/read.js +227 -0
  60. package/dist/scratch.d.ts +22 -0
  61. package/dist/scratch.d.ts.map +1 -0
  62. package/dist/scratch.js +77 -0
  63. package/dist/snapshot.d.ts +66 -0
  64. package/dist/snapshot.d.ts.map +1 -0
  65. package/dist/snapshot.js +272 -0
  66. package/dist/write.d.ts +17 -0
  67. package/dist/write.d.ts.map +1 -0
  68. package/dist/write.js +296 -0
  69. package/package.json +45 -3
  70. package/src/adapter.ts +349 -0
  71. package/src/aggregate.ts +168 -0
  72. package/src/decode.ts +46 -0
  73. package/src/fills.ts +52 -0
  74. package/src/filter.ts +306 -0
  75. package/src/index.ts +20 -0
  76. package/src/json-filter.ts +74 -0
  77. package/src/listen.ts +97 -0
  78. package/src/logic.ts +28 -0
  79. package/src/migrations.ts +275 -0
  80. package/src/names.ts +217 -0
  81. package/src/notify.ts +101 -0
  82. package/src/numbers.ts +64 -0
  83. package/src/parts.ts +143 -0
  84. package/src/plan-indexes.ts +100 -0
  85. package/src/plan-parts.ts +35 -0
  86. package/src/plan.ts +389 -0
  87. package/src/pool.ts +63 -0
  88. package/src/read.ts +287 -0
  89. package/src/scratch.ts +92 -0
  90. package/src/snapshot.ts +390 -0
  91. package/src/write.ts +384 -0
@@ -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
+ }