@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/dist/read.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// A read as one statement (D-46): every list, the root's and each included
|
|
2
|
+
// `many`, is a jsonb array the database builds, so relations at any depth
|
|
3
|
+
// cost no extra round trip.
|
|
4
|
+
import { sql } from 'kysely';
|
|
5
|
+
import { all, Filters } from './filter.js';
|
|
6
|
+
const empty = sql `cast('[]' as jsonb)`;
|
|
7
|
+
/** PostgreSQL takes at most 100 arguments: 50 pairs per `jsonb_build_object`. */
|
|
8
|
+
function object(pairs) {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for (let at = 0; at < pairs.length; at += 50) {
|
|
11
|
+
const items = pairs
|
|
12
|
+
.slice(at, at + 50)
|
|
13
|
+
.flatMap(([key, item]) => [sql.lit(key), item]);
|
|
14
|
+
chunks.push(sql `jsonb_build_object(${sql.join(items)})`);
|
|
15
|
+
}
|
|
16
|
+
return sql.join(chunks, sql ` || `);
|
|
17
|
+
}
|
|
18
|
+
const flip = (order) => ({
|
|
19
|
+
...order,
|
|
20
|
+
direction: order.direction === 'asc' ? 'desc' : 'asc',
|
|
21
|
+
...(order.nulls && { nulls: order.nulls === 'first' ? 'last' : 'first' }),
|
|
22
|
+
});
|
|
23
|
+
/** Whether `null` sorts after every value (D-149). */
|
|
24
|
+
const nullsLast = ({ direction, nulls }) => (nulls ?? (direction === 'asc' ? 'last' : 'first')) === 'last';
|
|
25
|
+
/** A sort key in `ORDER BY`; PostgreSQL's default `null`s are D-94's. */
|
|
26
|
+
export const sorted = (key, { direction, nulls }) => sql `${key} ${sql.raw(direction)}${nulls ? sql.raw(` nulls ${nulls}`) : sql ``}`;
|
|
27
|
+
export class Compiler {
|
|
28
|
+
filters;
|
|
29
|
+
catalog;
|
|
30
|
+
constructor(catalog) {
|
|
31
|
+
this.catalog = catalog;
|
|
32
|
+
this.filters = new Filters(catalog);
|
|
33
|
+
}
|
|
34
|
+
/** The rows `nested` selects among those `reach` gives, as a jsonb array. */
|
|
35
|
+
list(model, nested, reach, alias) {
|
|
36
|
+
const take = nested.take;
|
|
37
|
+
if (take === 0)
|
|
38
|
+
return empty;
|
|
39
|
+
const table = this.catalog.table(model);
|
|
40
|
+
const backward = take !== undefined && take < 0;
|
|
41
|
+
const given = nested.orderBy ?? [{ field: 'id', direction: 'asc' }];
|
|
42
|
+
const order = backward ? given.map(flip) : given;
|
|
43
|
+
const by = sql.join(order.map((each) => sorted(this.key(table, alias, each), each)));
|
|
44
|
+
const conditions = [
|
|
45
|
+
reach.on,
|
|
46
|
+
this.filters.where(model, alias, nested.where),
|
|
47
|
+
];
|
|
48
|
+
let from = reach.from;
|
|
49
|
+
if (nested.cursor !== undefined) {
|
|
50
|
+
this.filters.tables.add(table.name);
|
|
51
|
+
const cursor = this.filters.alias();
|
|
52
|
+
const found = this.filters.alias();
|
|
53
|
+
const keys = order.map((each, at) => sql `${this.key(table, found, each)} as ${sql.id(`k${at}`)}`);
|
|
54
|
+
const where = this.filters.where(model, found, nested.cursor);
|
|
55
|
+
from = sql `${from} cross join (select ${sql.join(keys)} from ${sql.id(table.name)} as ${sql.id(found)} where ${where} limit 1) as ${sql.id(cursor)}`;
|
|
56
|
+
conditions.push(this.keyset(table, alias, cursor, order));
|
|
57
|
+
}
|
|
58
|
+
const limit = take === undefined ? sql `` : sql ` limit ${Math.abs(take)}`;
|
|
59
|
+
const skip = nested.skip ?? 0;
|
|
60
|
+
const offset = skip === 0 ? sql `` : sql ` offset ${skip}`;
|
|
61
|
+
const row = this.row(model, alias, nested);
|
|
62
|
+
const rows = sql `select ${row} as "r", row_number() over (order by ${by}) as "n" from ${from} where ${all(conditions)} order by ${by}${limit}${offset}`;
|
|
63
|
+
const sequence = backward ? sql `"n" desc` : sql `"n"`;
|
|
64
|
+
return sql `(select coalesce(jsonb_agg("q"."r" order by ${sequence}), ${empty}) from (${rows}) as "q")`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The rows at or after the cursor in `order`, `null` where it sorts, as
|
|
68
|
+
* `memory()` compares: `a ≥ c` is `a > c`, or `a = c` and the next key
|
|
69
|
+
* decides.
|
|
70
|
+
*/
|
|
71
|
+
keyset(table, alias, cursor, order) {
|
|
72
|
+
let result = sql `true`;
|
|
73
|
+
for (const [at, each] of [...order.entries()].toReversed()) {
|
|
74
|
+
const a = this.key(table, alias, each);
|
|
75
|
+
const c = sql.id(cursor, `k${at}`);
|
|
76
|
+
const beyond = each.direction === 'asc' ? sql `${a} > ${c}` : sql `${a} < ${c}`;
|
|
77
|
+
const nulls = nullsLast(each)
|
|
78
|
+
? sql `(${a} is null and ${c} is not null)`
|
|
79
|
+
: sql `(${a} is not null and ${c} is null)`;
|
|
80
|
+
const after = sql `(${beyond} or ${nulls})`;
|
|
81
|
+
result = sql `(${after} or (${a} is not distinct from ${c} and ${result}))`;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A sort key: a column, through `one` relations, or a count; a row the
|
|
87
|
+
* relations do not reach gives `null` (D-134). A relevance is `ts_rank`'s
|
|
88
|
+
* (D-148).
|
|
89
|
+
*/
|
|
90
|
+
key(table, alias, order, steps = order.through ?? []) {
|
|
91
|
+
const [step, ...rest] = steps;
|
|
92
|
+
if (order.relevance !== undefined) {
|
|
93
|
+
return sql `ts_rank(${sql.id(alias, '_search')}, ${this.filters.query(order.relevance)})`;
|
|
94
|
+
}
|
|
95
|
+
if (step === undefined) {
|
|
96
|
+
if (order.count === undefined) {
|
|
97
|
+
return this.filters.column(table, alias, order.field);
|
|
98
|
+
}
|
|
99
|
+
return this.count(table, alias, order.field, order.count);
|
|
100
|
+
}
|
|
101
|
+
const field = table.fields.get(step.relation);
|
|
102
|
+
if (field === undefined || field.kind === 'scalar') {
|
|
103
|
+
throw new Error(`${table.model.name}.${step.relation} is not a relation.`);
|
|
104
|
+
}
|
|
105
|
+
const to = this.filters.alias();
|
|
106
|
+
const reach = this.filters.reach(table, alias, field, to);
|
|
107
|
+
const where = this.filters.where(field.target, to, step.where);
|
|
108
|
+
const key = this.key(this.catalog.table(field.target), to, order, rest);
|
|
109
|
+
return sql `(select ${key} from ${reach.from} where ${reach.on} and ${where} limit 1)`;
|
|
110
|
+
}
|
|
111
|
+
/** The rows of a `many` relation of the row that `where` selects. */
|
|
112
|
+
count(table, alias, name, where) {
|
|
113
|
+
const field = table.fields.get(name);
|
|
114
|
+
if (field === undefined || field.kind === 'scalar') {
|
|
115
|
+
throw new Error(`${table.model.name}.${name} is not a relation.`);
|
|
116
|
+
}
|
|
117
|
+
const to = this.filters.alias();
|
|
118
|
+
const reach = this.filters.reach(table, alias, field, to);
|
|
119
|
+
const rows = this.filters.where(field.target, to, where);
|
|
120
|
+
return sql `(select cast(count(*) as integer) from ${reach.from} where ${reach.on} and ${rows})`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A scalar column, a date in UTC whatever the session's time zone, a
|
|
124
|
+
* decimal as text, which jsonb would make a number (D-145).
|
|
125
|
+
*/
|
|
126
|
+
scalar(table, alias, name) {
|
|
127
|
+
const column = this.filters.column(table, alias, name);
|
|
128
|
+
const kind = this.catalog.kind(table, name);
|
|
129
|
+
if (kind === 'decimal')
|
|
130
|
+
return sql `cast(${column} as text)`;
|
|
131
|
+
return kind === 'dateTime' ? sql `(${column} at time zone 'UTC')` : column;
|
|
132
|
+
}
|
|
133
|
+
/** A row as jsonb: its fields, each `one` as `{ id }`, then its includes. */
|
|
134
|
+
row(model, alias, nested) {
|
|
135
|
+
const table = this.catalog.table(model);
|
|
136
|
+
const pairs = new Map();
|
|
137
|
+
const fields = nested.fields && new Set(['id', ...nested.fields]);
|
|
138
|
+
const reads = (name) => fields?.has(name) ?? true;
|
|
139
|
+
for (const name of ['id', 'createdAt', 'updatedAt'].filter(reads)) {
|
|
140
|
+
pairs.set(name, this.scalar(table, alias, name));
|
|
141
|
+
}
|
|
142
|
+
for (const field of table.model.fields) {
|
|
143
|
+
if (!reads(field.name))
|
|
144
|
+
continue;
|
|
145
|
+
if (field.kind === 'scalar') {
|
|
146
|
+
pairs.set(field.name, this.scalar(table, alias, field.name));
|
|
147
|
+
}
|
|
148
|
+
else if (field.kind === 'one' && field.stored) {
|
|
149
|
+
const key = this.filters.column(table, alias, field.name);
|
|
150
|
+
pairs.set(field.name, sql `case when ${key} is null then null else jsonb_build_object('id', ${key}) end`);
|
|
151
|
+
}
|
|
152
|
+
else if (field.kind === 'one') {
|
|
153
|
+
const to = this.filters.alias();
|
|
154
|
+
const reach = this.filters.reach(table, alias, field, to);
|
|
155
|
+
pairs.set(field.name, sql `(select jsonb_build_object('id', ${sql.id(to, 'id')}) from ${reach.from} where ${reach.on} limit 1)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [name, inner] of Object.entries(nested.include ?? {})) {
|
|
159
|
+
const field = table.fields.get(name);
|
|
160
|
+
if (field === undefined || field.kind === 'scalar')
|
|
161
|
+
continue;
|
|
162
|
+
const to = this.filters.alias();
|
|
163
|
+
const reach = this.filters.reach(table, alias, field, to);
|
|
164
|
+
if (field.kind === 'many') {
|
|
165
|
+
pairs.set(name, this.list(field.target, inner, reach, to));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const where = this.filters.where(field.target, to, inner.where);
|
|
169
|
+
const row = this.row(field.target, to, inner);
|
|
170
|
+
pairs.set(name, sql `(select ${row} from ${reach.from} where ${reach.on} and ${where} limit 1)`);
|
|
171
|
+
}
|
|
172
|
+
const counts = Object.entries(nested.count ?? {}).map(([name, where]) => [name, this.count(table, alias, name, where)]);
|
|
173
|
+
if (counts.length > 0)
|
|
174
|
+
pairs.set('_count', object(counts));
|
|
175
|
+
return object([...pairs]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The `id` of the first row of each `distinct` key in `read`'s order
|
|
180
|
+
* (D-147), which the read then sorts and pages.
|
|
181
|
+
*/
|
|
182
|
+
function kept(compiler, read, distinct) {
|
|
183
|
+
const { filters } = compiler;
|
|
184
|
+
const table = compiler.catalog.table(read.model);
|
|
185
|
+
const alias = filters.alias();
|
|
186
|
+
const numbered = filters.alias();
|
|
187
|
+
filters.tables.add(table.name);
|
|
188
|
+
const keys = distinct.map((key) => compiler.key(table, alias, key));
|
|
189
|
+
const order = (read.orderBy ?? [{ field: 'id', direction: 'asc' }]).map((each) => sorted(compiler.key(table, alias, each), each));
|
|
190
|
+
const where = filters.where(read.model, alias, read.where);
|
|
191
|
+
const id = filters.column(table, alias, 'id');
|
|
192
|
+
const rows = sql `select row_number() over (partition by ${sql.join(keys)} order by ${sql.join(order)}) as "n", ${id} as "i" from ${sql.id(table.name)} as ${sql.id(alias)} where ${where}`;
|
|
193
|
+
return sql `select ${sql.id(numbered, 'i')} from (${rows}) as ${sql.id(numbered)} where ${sql.id(numbered, 'n')} = 1`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* `read`'s rows as one jsonb array, its root rows taken from `source`, by
|
|
197
|
+
* default its table, and the tables it reaches besides; a locked read locks
|
|
198
|
+
* the root rows its `where` selects, in `id` order (D-99, D-133).
|
|
199
|
+
*/
|
|
200
|
+
export function compileRows(catalog, read, source) {
|
|
201
|
+
const compiler = new Compiler(catalog);
|
|
202
|
+
const alias = compiler.filters.alias();
|
|
203
|
+
const name = source ?? sql.id(catalog.table(read.model).name);
|
|
204
|
+
const as = sql.id(alias);
|
|
205
|
+
const table = catalog.table(read.model);
|
|
206
|
+
const id = compiler.filters.column(table, alias, 'id');
|
|
207
|
+
// In `id` order, so two reads of the same rows wait instead of deadlocking (D-133).
|
|
208
|
+
let from = read.lock
|
|
209
|
+
? sql `(select * from ${name} as ${as} where ${compiler.filters.where(read.model, alias, read.where)} order by ${id} for update) as ${as}`
|
|
210
|
+
: sql `${name} as ${as}`;
|
|
211
|
+
if (read.distinct !== undefined) {
|
|
212
|
+
from = sql `(select * from ${name} as ${as} where ${id} in (${kept(compiler, read, read.distinct)})) as ${as}`;
|
|
213
|
+
}
|
|
214
|
+
const rows = compiler.list(read.model, read, { from, on: sql `true` }, alias);
|
|
215
|
+
return { rows, tables: compiler.filters.tables };
|
|
216
|
+
}
|
|
217
|
+
/** The statement that returns `read`'s rows as one jsonb array, `rows`. */
|
|
218
|
+
export function compileFind(catalog, read) {
|
|
219
|
+
return sql `select ${compileRows(catalog, read).rows} as "rows"`;
|
|
220
|
+
}
|
|
221
|
+
export function compileCount(catalog, read) {
|
|
222
|
+
const filters = new Filters(catalog);
|
|
223
|
+
const alias = filters.alias();
|
|
224
|
+
const table = catalog.table(read.model);
|
|
225
|
+
const where = filters.where(read.model, alias, read.where);
|
|
226
|
+
return sql `select cast(count(*) as integer) as "count" from ${sql.id(table.name)} as ${sql.id(alias)} where ${where}`;
|
|
227
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type { PoolConfig } from 'pg';
|
|
3
|
+
/** A new test schema's name. */
|
|
4
|
+
export declare const scratchName: () => string;
|
|
5
|
+
/**
|
|
6
|
+
* The connections to `url`, in `schema` when one is given, then in the
|
|
7
|
+
* schemas of the extensions its tables use (D-141).
|
|
8
|
+
*/
|
|
9
|
+
export declare function connecting(url: string, schema: string | undefined, extensions?: readonly string[]): PoolConfig;
|
|
10
|
+
/**
|
|
11
|
+
* Creates `schema`, or empties the tables in it, once the connections are
|
|
12
|
+
* known to use it and nothing else.
|
|
13
|
+
*/
|
|
14
|
+
export declare function emptied(db: Kysely<object>, schema: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Installs pg_trgm where a migration would, in the first schema of the
|
|
17
|
+
* url's own `search_path`, and answers that schema (D-141).
|
|
18
|
+
*/
|
|
19
|
+
export declare function trigrams(url: string): Promise<string>;
|
|
20
|
+
/** Drops `schema` and its tables. */
|
|
21
|
+
export declare function dropped(url: string, schema: string): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=scratch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scratch.d.ts","sourceRoot":"","sources":["../src/scratch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAMpC,gCAAgC;AAChC,eAAO,MAAM,WAAW,QAAO,MACqB,CAAA;AAEpD;;;GAGG;AACH,wBAAgB,UAAU,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,UAAU,GAAE,SAAS,MAAM,EAAO,GAChC,UAAU,CASZ;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC5B,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,EAClB,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB3D;AAED,qCAAqC;AACrC,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxE"}
|
package/dist/scratch.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// A test file's own schema (D-137): the connections' `search_path`, its
|
|
2
|
+
// tables emptied at each start, and dropped after the file's last test.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { sql } from 'kysely';
|
|
5
|
+
import { Client } from 'pg';
|
|
6
|
+
import { parseIntoClientConfig } from 'pg-connection-string';
|
|
7
|
+
import { tables, tablesOf } from './migrations.js';
|
|
8
|
+
import { id } from './plan-parts.js';
|
|
9
|
+
/** A new test schema's name. */
|
|
10
|
+
export const scratchName = () => `ultimabase_test_${randomBytes(4).toString('hex')}`;
|
|
11
|
+
/**
|
|
12
|
+
* The connections to `url`, in `schema` when one is given, then in the
|
|
13
|
+
* schemas of the extensions its tables use (D-141).
|
|
14
|
+
*/
|
|
15
|
+
export function connecting(url, schema, extensions = []) {
|
|
16
|
+
if (schema === undefined)
|
|
17
|
+
return { connectionString: url };
|
|
18
|
+
const config = parseIntoClientConfig(url);
|
|
19
|
+
// After the url's own options: a `search_path` it sets loses.
|
|
20
|
+
const path = [schema, ...extensions].join(',');
|
|
21
|
+
const options = [config.options, `-c search_path=${path}`]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(' ');
|
|
24
|
+
return { ...config, options };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates `schema`, or empties the tables in it, once the connections are
|
|
28
|
+
* known to use it and nothing else.
|
|
29
|
+
*/
|
|
30
|
+
export async function emptied(db, schema) {
|
|
31
|
+
await sql `create schema if not exists ${sql.id(schema)}`.execute(db);
|
|
32
|
+
const { rows } = await sql `select current_schema() as current`.execute(db);
|
|
33
|
+
if (rows[0]?.current !== schema) {
|
|
34
|
+
throw new Error(`The connections did not take ${schema} as their search_path: does a pooler drop their startup options?`);
|
|
35
|
+
}
|
|
36
|
+
const names = (await tablesOf(db)).filter((name) => !tables.includes(name));
|
|
37
|
+
if (names.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
await sql.raw(`truncate ${names.map(id).join(', ')} cascade`).execute(db);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Installs pg_trgm where a migration would, in the first schema of the
|
|
43
|
+
* url's own `search_path`, and answers that schema (D-141).
|
|
44
|
+
*/
|
|
45
|
+
export async function trigrams(url) {
|
|
46
|
+
const client = new Client({ connectionString: url });
|
|
47
|
+
await client.connect();
|
|
48
|
+
try {
|
|
49
|
+
await client
|
|
50
|
+
.query('create extension if not exists pg_trgm')
|
|
51
|
+
.catch((error) => {
|
|
52
|
+
// Another test file created it at the same time.
|
|
53
|
+
const code = Reflect.get(Object(error), 'code');
|
|
54
|
+
if (code !== '23505' && code !== '42710')
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
const { rows } = await client.query(`select n.nspname as schema from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'`);
|
|
58
|
+
const [row] = rows;
|
|
59
|
+
if (row === undefined)
|
|
60
|
+
throw new Error('pg_trgm is not installed.');
|
|
61
|
+
return row.schema;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await client.end();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Drops `schema` and its tables. */
|
|
68
|
+
export async function dropped(url, schema) {
|
|
69
|
+
const client = new Client({ connectionString: url });
|
|
70
|
+
await client.connect();
|
|
71
|
+
try {
|
|
72
|
+
await client.query(`drop schema if exists ${id(schema)} cascade`);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await client.end();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Catalog } from './names.ts';
|
|
2
|
+
/** `field` is the model's name for it, which messages use. */
|
|
3
|
+
export type Column = {
|
|
4
|
+
readonly field: string;
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly nullable: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ForeignKey = {
|
|
9
|
+
readonly column: string;
|
|
10
|
+
readonly table: string;
|
|
11
|
+
readonly cascade: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** A column among `values`; each element of a list's (D-127). */
|
|
14
|
+
export type Check = {
|
|
15
|
+
readonly column: string;
|
|
16
|
+
readonly values: string[];
|
|
17
|
+
readonly list?: true;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* `model` is the model's name, or `Book.tags` for a join table. Constraints
|
|
21
|
+
* and indexes are keyed by name, columns in table order.
|
|
22
|
+
*/
|
|
23
|
+
export type TableSnapshot = {
|
|
24
|
+
readonly model: string;
|
|
25
|
+
readonly columns: {
|
|
26
|
+
readonly [name: string]: Column;
|
|
27
|
+
};
|
|
28
|
+
readonly primaryKey: {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly columns: string[];
|
|
31
|
+
};
|
|
32
|
+
readonly uniques: {
|
|
33
|
+
readonly [name: string]: string[];
|
|
34
|
+
};
|
|
35
|
+
readonly checks: {
|
|
36
|
+
readonly [name: string]: Check;
|
|
37
|
+
};
|
|
38
|
+
readonly indexes: {
|
|
39
|
+
readonly [name: string]: string[];
|
|
40
|
+
};
|
|
41
|
+
/** A `contains` index's name → its column (D-141). */
|
|
42
|
+
readonly trigrams: {
|
|
43
|
+
readonly [name: string]: string;
|
|
44
|
+
};
|
|
45
|
+
readonly foreignKeys: {
|
|
46
|
+
readonly [name: string]: ForeignKey;
|
|
47
|
+
};
|
|
48
|
+
/** The `_search` column, from these columns, and its GIN index (D-148). */
|
|
49
|
+
readonly search: Search | null;
|
|
50
|
+
};
|
|
51
|
+
export type Search = {
|
|
52
|
+
readonly index: string;
|
|
53
|
+
readonly language: string;
|
|
54
|
+
readonly columns: string[];
|
|
55
|
+
};
|
|
56
|
+
export type Snapshot = {
|
|
57
|
+
readonly postgres: 1;
|
|
58
|
+
readonly tables: {
|
|
59
|
+
readonly [name: string]: TableSnapshot;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/** The snapshot of the catalog's tables, join tables last. */
|
|
63
|
+
export declare function snapshotOf(catalog: Catalog): Snapshot;
|
|
64
|
+
/** A snapshot read back from a database or a file, checked. */
|
|
65
|
+
export declare function parseSnapshot(value: unknown): Snapshot;
|
|
66
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,YAAY,CAAA;AAIhD,8DAA8D;AAC9D,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC1E,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;IACnD,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IAC7D,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAA;CAC3D,CAAA;AA8HD,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAkFrD;AAsHD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAMtD"}
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { constraint } from './names.js';
|
|
2
|
+
import { Refused } from 'ultimabase';
|
|
3
|
+
const types = {
|
|
4
|
+
string: 'text collate "C"',
|
|
5
|
+
number: 'double precision',
|
|
6
|
+
decimal: 'numeric',
|
|
7
|
+
boolean: 'boolean',
|
|
8
|
+
dateTime: 'timestamptz',
|
|
9
|
+
date: 'date',
|
|
10
|
+
json: 'jsonb',
|
|
11
|
+
enum: 'text collate "C"',
|
|
12
|
+
};
|
|
13
|
+
/** A list's column: an enum's is text, as its single values (D-98, D-127). */
|
|
14
|
+
const lists = {
|
|
15
|
+
string: 'text[]',
|
|
16
|
+
number: 'double precision[]',
|
|
17
|
+
enum: 'text[]',
|
|
18
|
+
};
|
|
19
|
+
/** Refuses two things stored under one name (D-98). */
|
|
20
|
+
function claimer() {
|
|
21
|
+
const owners = new Map();
|
|
22
|
+
return (name, what) => {
|
|
23
|
+
const prior = owners.get(name);
|
|
24
|
+
if (prior !== undefined) {
|
|
25
|
+
throw new Refused(`${prior} and ${what} are both stored as "${name}".`);
|
|
26
|
+
}
|
|
27
|
+
owners.set(name, what);
|
|
28
|
+
return name;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function empty(claims, model, table, keys) {
|
|
32
|
+
const name = claims.schema(constraint(`${table}_pkey`), `the key of ${model}`);
|
|
33
|
+
return {
|
|
34
|
+
model,
|
|
35
|
+
columns: {},
|
|
36
|
+
primaryKey: { name, columns: keys },
|
|
37
|
+
uniques: {},
|
|
38
|
+
checks: {},
|
|
39
|
+
indexes: {},
|
|
40
|
+
trigrams: {},
|
|
41
|
+
foreignKeys: {},
|
|
42
|
+
search: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function index(claims, building, table, columns, what) {
|
|
46
|
+
const name = constraint(`${table}_${columns.join('_')}_idx`);
|
|
47
|
+
building.indexes[claims.schema(name, `the index on ${what}`)] = columns;
|
|
48
|
+
}
|
|
49
|
+
function field(catalog, claims, table, building, given) {
|
|
50
|
+
if (given.kind === 'many' || (given.kind === 'one' && !given.stored))
|
|
51
|
+
return;
|
|
52
|
+
const what = `${table.model.name}.${given.name}`;
|
|
53
|
+
const column = claims.columns(catalog.column(table, given.name), what);
|
|
54
|
+
const type = given.kind === 'one'
|
|
55
|
+
? 'uuid'
|
|
56
|
+
: given.integer === true
|
|
57
|
+
? `integer${given.list ? '[]' : ''}`
|
|
58
|
+
: given.type === 'decimal'
|
|
59
|
+
? `numeric(${given.precision},${given.scale})`
|
|
60
|
+
: ((given.list ? lists[given.type] : undefined) ?? types[given.type]);
|
|
61
|
+
building.columns[column] = {
|
|
62
|
+
field: given.name,
|
|
63
|
+
type,
|
|
64
|
+
nullable: given.optional,
|
|
65
|
+
};
|
|
66
|
+
if (given.kind === 'scalar' && given.type === 'enum') {
|
|
67
|
+
const name = constraint(`${table.name}_${column}_check`);
|
|
68
|
+
building.checks[claims.schema(name, `the check on ${what}`)] = {
|
|
69
|
+
column,
|
|
70
|
+
values: [...(given.values ?? [])],
|
|
71
|
+
...(given.list ? { list: true } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (given.kind !== 'one')
|
|
75
|
+
return;
|
|
76
|
+
const key = constraint(`${table.name}_${column}_fkey`);
|
|
77
|
+
building.foreignKeys[claims.schema(key, `the foreign key of ${what}`)] = {
|
|
78
|
+
column,
|
|
79
|
+
table: catalog.table(given.target).name,
|
|
80
|
+
cascade: false,
|
|
81
|
+
};
|
|
82
|
+
if (!given.unique)
|
|
83
|
+
index(claims, building, table.name, [column], what);
|
|
84
|
+
}
|
|
85
|
+
/** The fields of a key or an index, as `Thing(a, b)`. */
|
|
86
|
+
const on = (model, fields) => `${model}(${fields.join(', ')})`;
|
|
87
|
+
/** The snapshot of the catalog's tables, join tables last. */
|
|
88
|
+
export function snapshotOf(catalog) {
|
|
89
|
+
const tables = {};
|
|
90
|
+
const schema = claimer();
|
|
91
|
+
for (const table of catalog.tables.values()) {
|
|
92
|
+
const model = table.model.name;
|
|
93
|
+
const claims = { schema, columns: claimer() };
|
|
94
|
+
schema(table.name, `the table of ${model}`);
|
|
95
|
+
const building = empty(claims, model, table.name, ['id']);
|
|
96
|
+
for (const [column, name, type] of [
|
|
97
|
+
['id', 'id', 'uuid'],
|
|
98
|
+
['created_at', 'createdAt', 'timestamptz'],
|
|
99
|
+
['updated_at', 'updatedAt', 'timestamptz'],
|
|
100
|
+
]) {
|
|
101
|
+
claims.columns(column, `${model}.${name}`);
|
|
102
|
+
building.columns[column] = { field: name, type, nullable: false };
|
|
103
|
+
}
|
|
104
|
+
for (const given of table.model.fields) {
|
|
105
|
+
field(catalog, claims, table, building, given);
|
|
106
|
+
}
|
|
107
|
+
const [, ...uniques] = table.uniques;
|
|
108
|
+
for (const { name, fields } of uniques) {
|
|
109
|
+
const what = `the unique key on ${on(model, fields)}`;
|
|
110
|
+
building.uniques[schema(name, what)] = fields.map((given) => catalog.column(table, given));
|
|
111
|
+
}
|
|
112
|
+
for (const { fields, unique, contains } of table.model.indexes) {
|
|
113
|
+
if (unique)
|
|
114
|
+
continue;
|
|
115
|
+
if (contains) {
|
|
116
|
+
const [column = ''] = fields.map((name) => catalog.column(table, name));
|
|
117
|
+
const name = constraint(`${table.name}_${column}_trgm`);
|
|
118
|
+
const what = `the contains index on ${on(model, fields)}`;
|
|
119
|
+
building.trigrams[schema(name, what)] = column;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const columns = fields.map((name) => catalog.column(table, name));
|
|
123
|
+
index(claims, building, table.name, columns, on(model, fields));
|
|
124
|
+
}
|
|
125
|
+
const searched = table.model.fields.flatMap((given) => given.kind === 'scalar' && given.search === true
|
|
126
|
+
? [catalog.column(table, given.name)]
|
|
127
|
+
: []);
|
|
128
|
+
if (searched.length > 0) {
|
|
129
|
+
claims.columns('_search', `the search of ${model}`);
|
|
130
|
+
const name = constraint(`${table.name}__search_idx`);
|
|
131
|
+
building.search = {
|
|
132
|
+
index: schema(name, `the search index of ${model}`),
|
|
133
|
+
language: catalog.language,
|
|
134
|
+
columns: searched,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
tables[table.name] = building;
|
|
138
|
+
}
|
|
139
|
+
for (const [side, join] of catalog.joins) {
|
|
140
|
+
// Both sides of a relation share its table.
|
|
141
|
+
if (side !== join.relation)
|
|
142
|
+
continue;
|
|
143
|
+
const claims = { schema, columns: claimer() };
|
|
144
|
+
schema(join.table, `the join table of ${side}`);
|
|
145
|
+
const building = empty(claims, side, join.table, [join.self, join.other]);
|
|
146
|
+
for (const column of [join.self, join.other]) {
|
|
147
|
+
building.columns[column] = {
|
|
148
|
+
field: column,
|
|
149
|
+
type: 'uuid',
|
|
150
|
+
nullable: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
for (const [column, table] of [
|
|
154
|
+
[join.self, join.owner],
|
|
155
|
+
[join.other, join.target],
|
|
156
|
+
]) {
|
|
157
|
+
const key = constraint(`${join.table}_${column}_fkey`);
|
|
158
|
+
building.foreignKeys[schema(key, `the foreign key of ${side}`)] = {
|
|
159
|
+
column,
|
|
160
|
+
table,
|
|
161
|
+
cascade: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
index(claims, building, join.table, [join.other], side);
|
|
165
|
+
tables[join.table] = building;
|
|
166
|
+
}
|
|
167
|
+
return { postgres: 1, tables };
|
|
168
|
+
}
|
|
169
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
170
|
+
const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
171
|
+
/** Each entry of a record, checked by `read`, or `undefined`. */
|
|
172
|
+
function entries(value, read) {
|
|
173
|
+
if (!isRecord(value))
|
|
174
|
+
return undefined;
|
|
175
|
+
const result = {};
|
|
176
|
+
for (const [key, item] of Object.entries(value)) {
|
|
177
|
+
const checked = read(item);
|
|
178
|
+
if (checked === undefined)
|
|
179
|
+
return undefined;
|
|
180
|
+
result[key] = checked;
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
const columnOf = (value) => isRecord(value) &&
|
|
185
|
+
typeof value['field'] === 'string' &&
|
|
186
|
+
typeof value['type'] === 'string' &&
|
|
187
|
+
typeof value['nullable'] === 'boolean'
|
|
188
|
+
? {
|
|
189
|
+
field: value['field'],
|
|
190
|
+
type: value['type'],
|
|
191
|
+
nullable: value['nullable'],
|
|
192
|
+
}
|
|
193
|
+
: undefined;
|
|
194
|
+
const checkOf = (value) => isRecord(value) &&
|
|
195
|
+
typeof value['column'] === 'string' &&
|
|
196
|
+
isStrings(value['values'])
|
|
197
|
+
? {
|
|
198
|
+
column: value['column'],
|
|
199
|
+
values: value['values'],
|
|
200
|
+
...(value['list'] === true ? { list: true } : {}),
|
|
201
|
+
}
|
|
202
|
+
: undefined;
|
|
203
|
+
const foreignKeyOf = (value) => isRecord(value) &&
|
|
204
|
+
typeof value['column'] === 'string' &&
|
|
205
|
+
typeof value['table'] === 'string' &&
|
|
206
|
+
typeof value['cascade'] === 'boolean'
|
|
207
|
+
? {
|
|
208
|
+
column: value['column'],
|
|
209
|
+
table: value['table'],
|
|
210
|
+
cascade: value['cascade'],
|
|
211
|
+
}
|
|
212
|
+
: undefined;
|
|
213
|
+
const stringsOf = (value) => isStrings(value) ? value : undefined;
|
|
214
|
+
/** A search, `null` for none, or `undefined` when it is neither. */
|
|
215
|
+
const searchOf = (value) => value === null
|
|
216
|
+
? null
|
|
217
|
+
: isRecord(value) &&
|
|
218
|
+
typeof value['index'] === 'string' &&
|
|
219
|
+
typeof value['language'] === 'string' &&
|
|
220
|
+
isStrings(value['columns'])
|
|
221
|
+
? {
|
|
222
|
+
index: value['index'],
|
|
223
|
+
language: value['language'],
|
|
224
|
+
columns: value['columns'],
|
|
225
|
+
}
|
|
226
|
+
: undefined;
|
|
227
|
+
function tableOf(value) {
|
|
228
|
+
if (!isRecord(value))
|
|
229
|
+
return undefined;
|
|
230
|
+
const key = value['primaryKey'];
|
|
231
|
+
const columns = entries(value['columns'], columnOf);
|
|
232
|
+
const uniques = entries(value['uniques'], stringsOf);
|
|
233
|
+
const checks = entries(value['checks'], checkOf);
|
|
234
|
+
const indexes = entries(value['indexes'], stringsOf);
|
|
235
|
+
const trigrams = entries(value['trigrams'], (item) => typeof item === 'string' ? item : undefined);
|
|
236
|
+
const foreignKeys = entries(value['foreignKeys'], foreignKeyOf);
|
|
237
|
+
const search = searchOf(value['search']);
|
|
238
|
+
const model = value['model'];
|
|
239
|
+
if (typeof model !== 'string' ||
|
|
240
|
+
!isRecord(key) ||
|
|
241
|
+
typeof key['name'] !== 'string' ||
|
|
242
|
+
!isStrings(key['columns']) ||
|
|
243
|
+
columns === undefined ||
|
|
244
|
+
uniques === undefined ||
|
|
245
|
+
checks === undefined ||
|
|
246
|
+
indexes === undefined ||
|
|
247
|
+
trigrams === undefined ||
|
|
248
|
+
foreignKeys === undefined ||
|
|
249
|
+
search === undefined) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const primaryKey = { name: key['name'], columns: key['columns'] };
|
|
253
|
+
return {
|
|
254
|
+
model,
|
|
255
|
+
columns,
|
|
256
|
+
primaryKey,
|
|
257
|
+
uniques,
|
|
258
|
+
checks,
|
|
259
|
+
indexes,
|
|
260
|
+
trigrams,
|
|
261
|
+
foreignKeys,
|
|
262
|
+
search,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** A snapshot read back from a database or a file, checked. */
|
|
266
|
+
export function parseSnapshot(value) {
|
|
267
|
+
const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined;
|
|
268
|
+
if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
|
|
269
|
+
throw new Error('The schema snapshot is not one postgres() wrote.');
|
|
270
|
+
}
|
|
271
|
+
return { postgres: 1, tables };
|
|
272
|
+
}
|