@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
package/src/pool.ts ADDED
@@ -0,0 +1,63 @@
1
+ // The pool's connections, whose failures say why (D-105).
2
+ import type { PostgresPool } from 'kysely'
3
+ import type { Pool } from 'pg'
4
+ import { parse } from 'pg-connection-string'
5
+
6
+ /** The codes of a server the network does not reach, or does not know. */
7
+ const unreachable =
8
+ /^(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH)$/
9
+
10
+ /** What pg-pool throws when no connection frees, or opens, in time. */
11
+ const busy = 'timeout exceeded when trying to connect'
12
+ const silent = 'Connection terminated due to connection timeout'
13
+
14
+ /**
15
+ * `error`, or one naming the server `url` cannot reach, not its password
16
+ * (D-105), or the connections that stayed busy.
17
+ */
18
+ function failed(
19
+ url: string,
20
+ error: unknown,
21
+ connections: number,
22
+ wait: number,
23
+ ): unknown {
24
+ const message = error instanceof Error ? error.message : ''
25
+ const seconds = `${wait / 1000}s`
26
+ if (message === busy) {
27
+ return new Error(
28
+ `Every one of the ${connections} PostgreSQL connections stayed busy for ${seconds}: a transaction may wait on something slow or never end.`,
29
+ { cause: error },
30
+ )
31
+ }
32
+ const code: unknown = Reflect.get(Object(error), 'code')
33
+ const reachable = typeof code !== 'string' || !unreachable.test(code)
34
+ if (reachable && message !== silent) return error
35
+ const { host, port } = parse(url)
36
+ const address = `${host || process.env['PGHOST'] || 'localhost'}:${port || 5432}`
37
+ const why = reachable ? `no answer in ${seconds}` : code
38
+ const reached = new Error(
39
+ `Cannot reach PostgreSQL at ${address} (${why}): is it running, and is its url right?`,
40
+ { cause: error },
41
+ )
42
+ return reachable ? reached : Object.assign(reached, { code })
43
+ }
44
+
45
+ /** `pool`, whose failures to give a connection say why. */
46
+ export function pooled(
47
+ url: string,
48
+ pool: Pool,
49
+ connections: number,
50
+ wait: number,
51
+ ): PostgresPool {
52
+ return {
53
+ options: pool.options,
54
+ connect: async () => {
55
+ try {
56
+ return await pool.connect()
57
+ } catch (error) {
58
+ throw failed(url, error, connections, wait)
59
+ }
60
+ },
61
+ end: async () => pool.end(),
62
+ }
63
+ }
package/src/read.ts ADDED
@@ -0,0 +1,287 @@
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 type {
6
+ CountRead,
7
+ Filter,
8
+ GroupKey,
9
+ Nested,
10
+ Order,
11
+ Read,
12
+ Sort,
13
+ } from 'ultimabase'
14
+ import type { Reach, Sql } from './filter.ts'
15
+ import { all, Filters } from './filter.ts'
16
+ import type { Catalog, Table } from './names.ts'
17
+
18
+ const empty = sql`cast('[]' as jsonb)`
19
+
20
+ /** PostgreSQL takes at most 100 arguments: 50 pairs per `jsonb_build_object`. */
21
+ function object(pairs: readonly (readonly [string, Sql])[]): Sql {
22
+ const chunks: Sql[] = []
23
+ for (let at = 0; at < pairs.length; at += 50) {
24
+ const items = pairs
25
+ .slice(at, at + 50)
26
+ .flatMap(([key, item]) => [sql.lit(key), item])
27
+ chunks.push(sql`jsonb_build_object(${sql.join(items)})`)
28
+ }
29
+ return sql.join(chunks, sql` || `)
30
+ }
31
+
32
+ const flip = (order: Order): Order => ({
33
+ ...order,
34
+ direction: order.direction === 'asc' ? 'desc' : 'asc',
35
+ ...(order.nulls && { nulls: order.nulls === 'first' ? 'last' : 'first' }),
36
+ })
37
+
38
+ /** Whether `null` sorts after every value (D-149). */
39
+ const nullsLast = ({ direction, nulls }: Sort) =>
40
+ (nulls ?? (direction === 'asc' ? 'last' : 'first')) === 'last'
41
+
42
+ /** A sort key in `ORDER BY`; PostgreSQL's default `null`s are D-94's. */
43
+ export const sorted = (key: Sql, { direction, nulls }: Sort): Sql =>
44
+ sql`${key} ${sql.raw(direction)}${nulls ? sql.raw(` nulls ${nulls}`) : sql``}`
45
+
46
+ export class Compiler {
47
+ readonly filters: Filters
48
+ readonly catalog: Catalog
49
+
50
+ constructor(catalog: Catalog) {
51
+ this.catalog = catalog
52
+ this.filters = new Filters(catalog)
53
+ }
54
+
55
+ /** The rows `nested` selects among those `reach` gives, as a jsonb array. */
56
+ list(model: string, nested: Nested, reach: Reach, alias: string): Sql {
57
+ const take = nested.take
58
+ if (take === 0) return empty
59
+ const table = this.catalog.table(model)
60
+ const backward = take !== undefined && take < 0
61
+ const given = nested.orderBy ?? [{ field: 'id', direction: 'asc' }]
62
+ const order = backward ? given.map(flip) : given
63
+ const by = sql.join(
64
+ order.map((each) => sorted(this.key(table, alias, each), each)),
65
+ )
66
+ const conditions = [
67
+ reach.on,
68
+ this.filters.where(model, alias, nested.where),
69
+ ]
70
+ let from = reach.from
71
+ if (nested.cursor !== undefined) {
72
+ this.filters.tables.add(table.name)
73
+ const cursor = this.filters.alias()
74
+ const found = this.filters.alias()
75
+ const keys = order.map(
76
+ (each, at) =>
77
+ sql`${this.key(table, found, each)} as ${sql.id(`k${at}`)}`,
78
+ )
79
+ const where = this.filters.where(model, found, nested.cursor)
80
+ 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)}`
81
+ conditions.push(this.keyset(table, alias, cursor, order))
82
+ }
83
+ const limit = take === undefined ? sql`` : sql` limit ${Math.abs(take)}`
84
+ const skip = nested.skip ?? 0
85
+ const offset = skip === 0 ? sql`` : sql` offset ${skip}`
86
+ const row = this.row(model, alias, nested)
87
+ 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}`
88
+ const sequence = backward ? sql`"n" desc` : sql`"n"`
89
+ return sql`(select coalesce(jsonb_agg("q"."r" order by ${sequence}), ${empty}) from (${rows}) as "q")`
90
+ }
91
+
92
+ /**
93
+ * The rows at or after the cursor in `order`, `null` where it sorts, as
94
+ * `memory()` compares: `a ≥ c` is `a > c`, or `a = c` and the next key
95
+ * decides.
96
+ */
97
+ private keyset(
98
+ table: Table,
99
+ alias: string,
100
+ cursor: string,
101
+ order: readonly Order[],
102
+ ): Sql {
103
+ let result: Sql = sql`true`
104
+ for (const [at, each] of [...order.entries()].toReversed()) {
105
+ const a = this.key(table, alias, each)
106
+ const c = sql.id(cursor, `k${at}`)
107
+ const beyond =
108
+ each.direction === 'asc' ? sql`${a} > ${c}` : sql`${a} < ${c}`
109
+ const nulls = nullsLast(each)
110
+ ? sql`(${a} is null and ${c} is not null)`
111
+ : sql`(${a} is not null and ${c} is null)`
112
+ const after = sql`(${beyond} or ${nulls})`
113
+ result = sql`(${after} or (${a} is not distinct from ${c} and ${result}))`
114
+ }
115
+ return result
116
+ }
117
+
118
+ /**
119
+ * A sort key: a column, through `one` relations, or a count; a row the
120
+ * relations do not reach gives `null` (D-134). A relevance is `ts_rank`'s
121
+ * (D-148).
122
+ */
123
+ key(
124
+ table: Table,
125
+ alias: string,
126
+ order: Pick<Order, 'field' | 'through' | 'count' | 'relevance'>,
127
+ steps: NonNullable<Order['through']> = order.through ?? [],
128
+ ): Sql {
129
+ const [step, ...rest] = steps
130
+ if (order.relevance !== undefined) {
131
+ return sql`ts_rank(${sql.id(alias, '_search')}, ${this.filters.query(order.relevance)})`
132
+ }
133
+ if (step === undefined) {
134
+ if (order.count === undefined) {
135
+ return this.filters.column(table, alias, order.field)
136
+ }
137
+ return this.count(table, alias, order.field, order.count)
138
+ }
139
+ const field = table.fields.get(step.relation)
140
+ if (field === undefined || field.kind === 'scalar') {
141
+ throw new Error(`${table.model.name}.${step.relation} is not a relation.`)
142
+ }
143
+ const to = this.filters.alias()
144
+ const reach = this.filters.reach(table, alias, field, to)
145
+ const where = this.filters.where(field.target, to, step.where)
146
+ const key = this.key(this.catalog.table(field.target), to, order, rest)
147
+ return sql`(select ${key} from ${reach.from} where ${reach.on} and ${where} limit 1)`
148
+ }
149
+
150
+ /** The rows of a `many` relation of the row that `where` selects. */
151
+ private count(table: Table, alias: string, name: string, where: Filter): Sql {
152
+ const field = table.fields.get(name)
153
+ if (field === undefined || field.kind === 'scalar') {
154
+ throw new Error(`${table.model.name}.${name} is not a relation.`)
155
+ }
156
+ const to = this.filters.alias()
157
+ const reach = this.filters.reach(table, alias, field, to)
158
+ const rows = this.filters.where(field.target, to, where)
159
+ return sql`(select cast(count(*) as integer) from ${reach.from} where ${reach.on} and ${rows})`
160
+ }
161
+
162
+ /**
163
+ * A scalar column, a date in UTC whatever the session's time zone, a
164
+ * decimal as text, which jsonb would make a number (D-145).
165
+ */
166
+ private scalar(table: Table, alias: string, name: string): Sql {
167
+ const column = this.filters.column(table, alias, name)
168
+ const kind = this.catalog.kind(table, name)
169
+ if (kind === 'decimal') return sql`cast(${column} as text)`
170
+ return kind === 'dateTime' ? sql`(${column} at time zone 'UTC')` : column
171
+ }
172
+
173
+ /** A row as jsonb: its fields, each `one` as `{ id }`, then its includes. */
174
+ row(model: string, alias: string, nested: Nested): Sql {
175
+ const table = this.catalog.table(model)
176
+ const pairs = new Map<string, Sql>()
177
+ const fields = nested.fields && new Set(['id', ...nested.fields])
178
+ const reads = (name: string) => fields?.has(name) ?? true
179
+ for (const name of ['id', 'createdAt', 'updatedAt'].filter(reads)) {
180
+ pairs.set(name, this.scalar(table, alias, name))
181
+ }
182
+ for (const field of table.model.fields) {
183
+ if (!reads(field.name)) continue
184
+ if (field.kind === 'scalar') {
185
+ pairs.set(field.name, this.scalar(table, alias, field.name))
186
+ } else if (field.kind === 'one' && field.stored) {
187
+ const key = this.filters.column(table, alias, field.name)
188
+ pairs.set(
189
+ field.name,
190
+ sql`case when ${key} is null then null else jsonb_build_object('id', ${key}) end`,
191
+ )
192
+ } else if (field.kind === 'one') {
193
+ const to = this.filters.alias()
194
+ const reach = this.filters.reach(table, alias, field, to)
195
+ pairs.set(
196
+ field.name,
197
+ sql`(select jsonb_build_object('id', ${sql.id(to, 'id')}) from ${reach.from} where ${reach.on} limit 1)`,
198
+ )
199
+ }
200
+ }
201
+ for (const [name, inner] of Object.entries(nested.include ?? {})) {
202
+ const field = table.fields.get(name)
203
+ if (field === undefined || field.kind === 'scalar') continue
204
+ const to = this.filters.alias()
205
+ const reach = this.filters.reach(table, alias, field, to)
206
+ if (field.kind === 'many') {
207
+ pairs.set(name, this.list(field.target, inner, reach, to))
208
+ continue
209
+ }
210
+ const where = this.filters.where(field.target, to, inner.where)
211
+ const row = this.row(field.target, to, inner)
212
+ pairs.set(
213
+ name,
214
+ sql`(select ${row} from ${reach.from} where ${reach.on} and ${where} limit 1)`,
215
+ )
216
+ }
217
+ const counts = Object.entries(nested.count ?? {}).map(
218
+ ([name, where]) => [name, this.count(table, alias, name, where)] as const,
219
+ )
220
+ if (counts.length > 0) pairs.set('_count', object(counts))
221
+ return object([...pairs])
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The `id` of the first row of each `distinct` key in `read`'s order
227
+ * (D-147), which the read then sorts and pages.
228
+ */
229
+ function kept(
230
+ compiler: Compiler,
231
+ read: Read,
232
+ distinct: readonly GroupKey[],
233
+ ): Sql {
234
+ const { filters } = compiler
235
+ const table = compiler.catalog.table(read.model)
236
+ const alias = filters.alias()
237
+ const numbered = filters.alias()
238
+ filters.tables.add(table.name)
239
+ const keys = distinct.map((key) => compiler.key(table, alias, key))
240
+ const order = (read.orderBy ?? [{ field: 'id', direction: 'asc' }]).map(
241
+ (each) => sorted(compiler.key(table, alias, each), each),
242
+ )
243
+ const where = filters.where(read.model, alias, read.where)
244
+ const id = filters.column(table, alias, 'id')
245
+ 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}`
246
+ return sql`select ${sql.id(numbered, 'i')} from (${rows}) as ${sql.id(numbered)} where ${sql.id(numbered, 'n')} = 1`
247
+ }
248
+
249
+ /**
250
+ * `read`'s rows as one jsonb array, its root rows taken from `source`, by
251
+ * default its table, and the tables it reaches besides; a locked read locks
252
+ * the root rows its `where` selects, in `id` order (D-99, D-133).
253
+ */
254
+ export function compileRows(
255
+ catalog: Catalog,
256
+ read: Read,
257
+ source?: Sql,
258
+ ): { readonly rows: Sql; readonly tables: ReadonlySet<string> } {
259
+ const compiler = new Compiler(catalog)
260
+ const alias = compiler.filters.alias()
261
+ const name = source ?? sql.id(catalog.table(read.model).name)
262
+ const as = sql.id(alias)
263
+ const table = catalog.table(read.model)
264
+ const id = compiler.filters.column(table, alias, 'id')
265
+ // In `id` order, so two reads of the same rows wait instead of deadlocking (D-133).
266
+ let from = read.lock
267
+ ? sql`(select * from ${name} as ${as} where ${compiler.filters.where(read.model, alias, read.where)} order by ${id} for update) as ${as}`
268
+ : sql`${name} as ${as}`
269
+ if (read.distinct !== undefined) {
270
+ from = sql`(select * from ${name} as ${as} where ${id} in (${kept(compiler, read, read.distinct)})) as ${as}`
271
+ }
272
+ const rows = compiler.list(read.model, read, { from, on: sql`true` }, alias)
273
+ return { rows, tables: compiler.filters.tables }
274
+ }
275
+
276
+ /** The statement that returns `read`'s rows as one jsonb array, `rows`. */
277
+ export function compileFind(catalog: Catalog, read: Read): Sql {
278
+ return sql`select ${compileRows(catalog, read).rows} as "rows"`
279
+ }
280
+
281
+ export function compileCount(catalog: Catalog, read: CountRead): Sql {
282
+ const filters = new Filters(catalog)
283
+ const alias = filters.alias()
284
+ const table = catalog.table(read.model)
285
+ const where = filters.where(read.model, alias, read.where)
286
+ return sql`select cast(count(*) as integer) as "count" from ${sql.id(table.name)} as ${sql.id(alias)} where ${where}`
287
+ }
package/src/scratch.ts ADDED
@@ -0,0 +1,92 @@
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 type { Kysely } from 'kysely'
5
+ import { sql } from 'kysely'
6
+ import type { PoolConfig } from 'pg'
7
+ import { Client } from 'pg'
8
+ import { parseIntoClientConfig } from 'pg-connection-string'
9
+ import { tables, tablesOf } from './migrations.ts'
10
+ import { id } from './plan-parts.ts'
11
+
12
+ /** A new test schema's name. */
13
+ export const scratchName = (): string =>
14
+ `ultimabase_test_${randomBytes(4).toString('hex')}`
15
+
16
+ /**
17
+ * The connections to `url`, in `schema` when one is given, then in the
18
+ * schemas of the extensions its tables use (D-141).
19
+ */
20
+ export function connecting(
21
+ url: string,
22
+ schema: string | undefined,
23
+ extensions: readonly string[] = [],
24
+ ): PoolConfig {
25
+ if (schema === undefined) return { connectionString: url }
26
+ const config = parseIntoClientConfig(url)
27
+ // After the url's own options: a `search_path` it sets loses.
28
+ const path = [schema, ...extensions].join(',')
29
+ const options = [config.options, `-c search_path=${path}`]
30
+ .filter(Boolean)
31
+ .join(' ')
32
+ return { ...config, options }
33
+ }
34
+
35
+ /**
36
+ * Creates `schema`, or empties the tables in it, once the connections are
37
+ * known to use it and nothing else.
38
+ */
39
+ export async function emptied(
40
+ db: Kysely<object>,
41
+ schema: string,
42
+ ): Promise<void> {
43
+ await sql`create schema if not exists ${sql.id(schema)}`.execute(db)
44
+ const { rows } = await sql<{
45
+ current: string | null
46
+ }>`select current_schema() as current`.execute(db)
47
+ if (rows[0]?.current !== schema) {
48
+ throw new Error(
49
+ `The connections did not take ${schema} as their search_path: does a pooler drop their startup options?`,
50
+ )
51
+ }
52
+ const names = (await tablesOf(db)).filter((name) => !tables.includes(name))
53
+ if (names.length === 0) return
54
+ await sql.raw(`truncate ${names.map(id).join(', ')} cascade`).execute(db)
55
+ }
56
+
57
+ /**
58
+ * Installs pg_trgm where a migration would, in the first schema of the
59
+ * url's own `search_path`, and answers that schema (D-141).
60
+ */
61
+ export async function trigrams(url: string): Promise<string> {
62
+ const client = new Client({ connectionString: url })
63
+ await client.connect()
64
+ try {
65
+ await client
66
+ .query('create extension if not exists pg_trgm')
67
+ .catch((error: unknown) => {
68
+ // Another test file created it at the same time.
69
+ const code: unknown = Reflect.get(Object(error), 'code')
70
+ if (code !== '23505' && code !== '42710') throw error
71
+ })
72
+ const { rows } = await client.query<{ schema: string }>(
73
+ `select n.nspname as schema from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'`,
74
+ )
75
+ const [row] = rows
76
+ if (row === undefined) throw new Error('pg_trgm is not installed.')
77
+ return row.schema
78
+ } finally {
79
+ await client.end()
80
+ }
81
+ }
82
+
83
+ /** Drops `schema` and its tables. */
84
+ export async function dropped(url: string, schema: string): Promise<void> {
85
+ const client = new Client({ connectionString: url })
86
+ await client.connect()
87
+ try {
88
+ await client.query(`drop schema if exists ${id(schema)} cascade`)
89
+ } finally {
90
+ await client.end()
91
+ }
92
+ }