@rwillians/exto 0.1.1 → 0.1.2

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.
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dialect = exports.connect = void 0;
4
+ const bun_1 = require("bun");
5
+ const utils_1 = require("./utils");
6
+ const TYPES = {
7
+ BIGINT: () => 'BIGINT',
8
+ BINARY: () => 'BYTEA',
9
+ BOOLEAN: () => 'BOOLEAN',
10
+ CITEXT: () => 'CITEXT',
11
+ DATE: () => 'DATE',
12
+ DATETIME: () => 'TIMESTAMPTZ',
13
+ DECIMAL: ({ precision, scale }) => [precision, scale].every(Number.isInteger) ? `NUMERIC(${precision}, ${scale})`
14
+ : [precision, scale].some(Number.isInteger) ? (0, utils_1.raise)('precision and scale are mutually inclusive, either you provide both or neither')
15
+ : 'NUMERIC',
16
+ FLOAT: () => 'DOUBLE PRECISION',
17
+ INTEGER: () => 'BIGINT',
18
+ JSON: () => 'JSONB',
19
+ SEQUENCE: () => 'BIGINT',
20
+ STRING: ({ length = 255 }) => `VARCHAR(${length})`,
21
+ TEXT: () => 'TEXT',
22
+ UUID: () => 'UUID',
23
+ };
24
+ const identity = () => ({
25
+ encode: (value) => value,
26
+ decode: (value) => value,
27
+ });
28
+ const dialect = {
29
+ name: 'postgres',
30
+ placeholder: (index) => `$${index}`,
31
+ type: (col) => TYPES[col.type](col),
32
+ generated: (col) => col.type === 'SEQUENCE'
33
+ ? 'GENERATED ALWAYS AS IDENTITY'
34
+ : undefined,
35
+ codecs: {
36
+ BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
37
+ BINARY: identity(),
38
+ BOOLEAN: identity(),
39
+ CITEXT: identity(),
40
+ DATE: { encode: (value) => value, decode: (value) => value instanceof Date ? value.toISOString().slice(0, 10) : value },
41
+ DATETIME: identity(),
42
+ DECIMAL: { encode: (value) => value, decode: (value) => Number(value) },
43
+ FLOAT: identity(),
44
+ INTEGER: { encode: (value) => value, decode: (value) => Number(value) },
45
+ JSON: identity(),
46
+ SEQUENCE: { encode: (value) => value, decode: (value) => Number(value) },
47
+ STRING: identity(),
48
+ TEXT: identity(),
49
+ UUID: identity(),
50
+ },
51
+ };
52
+ exports.dialect = dialect;
53
+ class BunPostgres {
54
+ sql;
55
+ loggers;
56
+ inTransaction;
57
+ dialect = dialect;
58
+ constructor(sql, loggers = [], inTransaction = false) {
59
+ this.sql = sql;
60
+ this.loggers = loggers;
61
+ this.inTransaction = inTransaction;
62
+ }
63
+ async execute(sql, params) {
64
+ return await this.sql.unsafe(sql, params);
65
+ }
66
+ async transaction(fn) {
67
+ const callback = (tx) => fn(new BunPostgres(tx, this.loggers, true));
68
+ const result = this.inTransaction
69
+ ? this.sql.savepoint(callback)
70
+ : this.sql.begin(callback);
71
+ return await result;
72
+ }
73
+ }
74
+ const connect = (url, { loggers = [] } = {}) => new BunPostgres(new bun_1.SQL(url), loggers);
75
+ exports.connect = connect;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dialect = exports.connect = void 0;
4
+ const bun_sqlite_1 = require("bun:sqlite");
5
+ const TYPES = {
6
+ BIGINT: 'INTEGER',
7
+ BINARY: 'BLOB',
8
+ BOOLEAN: 'INTEGER',
9
+ CITEXT: 'TEXT COLLATE NOCASE',
10
+ DATE: 'TEXT',
11
+ DATETIME: 'INTEGER',
12
+ DECIMAL: 'NUMERIC',
13
+ FLOAT: 'REAL',
14
+ INTEGER: 'INTEGER',
15
+ JSON: 'TEXT',
16
+ SEQUENCE: 'INTEGER',
17
+ STRING: 'TEXT',
18
+ TEXT: 'TEXT',
19
+ UUID: 'TEXT',
20
+ };
21
+ const identity = () => ({
22
+ encode: (value) => value,
23
+ decode: (value) => value,
24
+ });
25
+ // integers are read as bigints (see `execute`), so those that aren't BIGINT
26
+ // columns go back to being numbers
27
+ const numeric = {
28
+ encode: (value) => value,
29
+ decode: (value) => Number(value),
30
+ };
31
+ const dialect = {
32
+ name: 'sqlite',
33
+ placeholder: () => '?',
34
+ type: (col) => TYPES[col.type],
35
+ generated: (col) => {
36
+ if (col.type === 'SEQUENCE' && col.primaryKey)
37
+ return 'AUTOINCREMENT';
38
+ if (col.type === 'SEQUENCE')
39
+ throw new Error(`SQLite can only generate sequences for primary keys, column "${col.name}" isn't one`);
40
+ return undefined;
41
+ },
42
+ codecs: {
43
+ BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
44
+ BINARY: identity(),
45
+ BOOLEAN: { encode: (value) => (value ? 1 : 0), decode: (value) => Number(value) === 1 },
46
+ CITEXT: identity(),
47
+ DATE: identity(),
48
+ DATETIME: { encode: (value) => value.getTime(), decode: (value) => new Date(Number(value)) },
49
+ DECIMAL: numeric,
50
+ FLOAT: numeric,
51
+ INTEGER: numeric,
52
+ JSON: { encode: (value) => JSON.stringify(value), decode: (value) => JSON.parse(value) },
53
+ SEQUENCE: numeric,
54
+ STRING: identity(),
55
+ TEXT: identity(),
56
+ UUID: identity(),
57
+ },
58
+ };
59
+ exports.dialect = dialect;
60
+ class BunSQLite {
61
+ conn;
62
+ loggers;
63
+ depth;
64
+ dialect = dialect;
65
+ constructor(conn, loggers = [], depth = 0) {
66
+ this.conn = conn;
67
+ this.loggers = loggers;
68
+ this.depth = depth;
69
+ }
70
+ async execute(sql, params) {
71
+ // without safe integers, bun:sqlite rounds anything beyond 2^53
72
+ return this.conn.prepare(sql).safeIntegers(true).all(...params);
73
+ }
74
+ // bun:sqlite's own `transaction()` is synchronous and would commit before an
75
+ // async callback settles, hence the manual BEGIN/COMMIT (SAVEPOINT if nested)
76
+ async transaction(fn) {
77
+ const savepoint = `"sp_${this.depth}"`;
78
+ const [begin, commit, rollback] = this.depth === 0
79
+ ? ['BEGIN', 'COMMIT', 'ROLLBACK']
80
+ : [`SAVEPOINT ${savepoint}`, `RELEASE ${savepoint}`, `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`];
81
+ this.conn.run(begin);
82
+ try {
83
+ const result = await fn(new BunSQLite(this.conn, this.loggers, this.depth + 1));
84
+ this.conn.run(commit);
85
+ return result;
86
+ }
87
+ catch (error) {
88
+ this.conn.run(rollback);
89
+ throw error;
90
+ }
91
+ }
92
+ }
93
+ const connect = (filename = ':memory:', { loggers = [] } = {}) => new BunSQLite(new bun_sqlite_1.Database(filename), loggers);
94
+ exports.connect = connect;
@@ -0,0 +1,453 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.t = exports.into = exports.from = exports.expr = exports.exec = exports.table = exports.defineMigrations = exports.create = exports.column = exports.ValidationError = void 0;
37
+ const std = __importStar(require("./standard-schema"));
38
+ const standard_schema_1 = require("./standard-schema");
39
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return standard_schema_1.ValidationError; } });
40
+ const utils_1 = require("./utils");
41
+ class ColumnBuilder {
42
+ props;
43
+ constructor(props) {
44
+ this.props = props;
45
+ }
46
+ default(value) {
47
+ return new ColumnBuilder({ ...this.props, default: value });
48
+ }
49
+ generated() {
50
+ if (this.props.type !== 'UUID')
51
+ throw new Error('Only UUID columns can be generated, use t.sequence() for integers');
52
+ return new ColumnBuilder({ ...this.props, generated: true });
53
+ }
54
+ nullable() {
55
+ if (this.props.primaryKey)
56
+ throw new Error('Cannot make a primary key column nullable');
57
+ return new ColumnBuilder({ ...this.props, nullable: true, schema: std.nullable(this.props.schema) });
58
+ }
59
+ primaryKey() {
60
+ if (this.props.nullable)
61
+ throw new Error('Cannot make a nullable column a primary key');
62
+ return new ColumnBuilder({ ...this.props, primaryKey: true });
63
+ }
64
+ unique() {
65
+ if (this.props.primaryKey)
66
+ throw new Error('Primary key columns are already unique');
67
+ return new ColumnBuilder({ ...this.props, unique: true });
68
+ }
69
+ }
70
+ const column = (props) => new ColumnBuilder(props);
71
+ exports.column = column;
72
+ const types = {
73
+ bigint: () => column({ type: 'BIGINT', schema: std.bigint() }),
74
+ binary: () => column({ type: 'BINARY', schema: std.instanceOf(Uint8Array) }),
75
+ boolean: () => column({ type: 'BOOLEAN', schema: std.boolean() }),
76
+ // case-insensitive text; postgres needs `CREATE EXTENSION citext` for it
77
+ citext: () => column({ type: 'CITEXT', schema: std.string() }),
78
+ date: () => column({ type: 'DATE', schema: std.isoDate() }),
79
+ datetime: () => column({ type: 'DATETIME', schema: std.date() }),
80
+ // `precision` and `scale` only shape postgres' NUMERIC; values are still
81
+ // numbers, so they're as exact as a double
82
+ decimal: (options) => column({ type: 'DECIMAL', schema: std.number(), ...options }),
83
+ enum: (values) => column({
84
+ type: 'STRING',
85
+ schema: std.enumOf(values),
86
+ length: Math.max(...values.map(value => value.length)),
87
+ }),
88
+ float: () => column({ type: 'FLOAT', schema: std.number() }),
89
+ integer: () => column({ type: 'INTEGER', schema: std.integer() }),
90
+ json: (schema) => column({ type: 'JSON', schema }),
91
+ sequence: () => column({ type: 'SEQUENCE', schema: std.integer(), generated: true }),
92
+ string: ({ size = 255 } = {}) => column({ type: 'STRING', schema: std.string({ max: size }), length: size }),
93
+ text: () => column({ type: 'TEXT', schema: std.string() }),
94
+ uuid: () => column({ type: 'UUID', schema: std.uuid() }),
95
+ };
96
+ exports.t = types;
97
+ const defineTable = (name, shapeFn) => {
98
+ const columns = Object.fromEntries(Object
99
+ .entries(shapeFn(types))
100
+ .map(([key, { props }]) => [(0, utils_1.camelCase)(key), { ...props, key: (0, utils_1.camelCase)(key), name: (0, utils_1.snakeCase)((0, utils_1.camelCase)(key)) }]));
101
+ const aliased = (alias) => ({ name, alias, columns, as: aliased });
102
+ return aliased(name);
103
+ };
104
+ exports.table = defineTable;
105
+ const columnOf = (table, key) => {
106
+ const col = table.columns[key];
107
+ if (!col)
108
+ throw new Error(`Unknown column "${key}" in table "${table.name}"`);
109
+ return col;
110
+ };
111
+ const quote = (ident) => `"${ident.replaceAll('"', '""')}"`;
112
+ const ref = (col, qualifier) => qualifier
113
+ ? `${quote(qualifier)}.${quote(col.name)}`
114
+ : quote(col.name);
115
+ class SqlBuilder {
116
+ dialect;
117
+ frags = [];
118
+ params = [];
119
+ constructor(dialect) {
120
+ this.dialect = dialect;
121
+ }
122
+ push(...frags) {
123
+ this.frags.push(...frags);
124
+ return this;
125
+ }
126
+ param(value) {
127
+ this.params.push(value);
128
+ return this.push(this.dialect.placeholder(this.params.length));
129
+ }
130
+ join(items, separator, fn) {
131
+ items.forEach((item, i) => {
132
+ if (i > 0)
133
+ this.push(separator);
134
+ fn(item);
135
+ });
136
+ return this;
137
+ }
138
+ build() {
139
+ return { sql: this.frags.join(''), params: this.params };
140
+ }
141
+ }
142
+ const encode = (dialect, col, value) => value === null || value === undefined
143
+ ? null
144
+ : dialect.codecs[col.type].encode(value);
145
+ const decode = (dialect, col, value) => value === null || value === undefined
146
+ ? null
147
+ : dialect.codecs[col.type].decode(value);
148
+ const decoder = (dialect, cols) => (row) => Object.fromEntries(cols.map(col => [col.key, decode(dialect, col, row[col.key])]));
149
+ const run = async (db, { sql, params }) => {
150
+ db.loggers.forEach(logger => logger.debug(sql, params));
151
+ try {
152
+ return await db.execute(sql, params);
153
+ }
154
+ catch (error) {
155
+ db.loggers.forEach(logger => logger.error(sql, params, error));
156
+ throw error;
157
+ }
158
+ };
159
+ // runs a raw statement, e.g. `CREATE EXTENSION` in a migration
160
+ const exec = async (db, sql, params = []) => {
161
+ await run(db, { sql, params });
162
+ };
163
+ exports.exec = exec;
164
+ const runReturning = async (db, b, table, returning) => {
165
+ if (returning === false) {
166
+ await run(db, b.build());
167
+ return undefined;
168
+ }
169
+ const cols = returning === true
170
+ ? Object.values(table.columns)
171
+ : returning.map(key => columnOf(table, key));
172
+ // an empty RETURNING isn't valid SQL, but it still takes one to know how
173
+ // many rows were affected
174
+ if (cols.length === 0) {
175
+ const rows = await run(db, b.push(' RETURNING 1').build());
176
+ return rows.map(() => ({}));
177
+ }
178
+ b.push(' RETURNING ').join(cols, ', ', col => b.push(ref(col), ' AS ', quote(col.key)));
179
+ const rows = await run(db, b.build());
180
+ return rows.map(decoder(db.dialect, cols));
181
+ };
182
+ const expr = Object.assign((key, op, value) => ({ key, op, value }), {
183
+ and: (exprs) => ({ group: 'AND', exprs }),
184
+ or: (exprs) => ({ group: 'OR', exprs }),
185
+ });
186
+ exports.expr = expr;
187
+ const toCondition = (table, node) => 'group' in node
188
+ ? { group: node.group, conditions: node.exprs.map(expr => toCondition(table, expr)) }
189
+ : { col: columnOf(table, node.key), op: node.op, value: node.value };
190
+ const renderCondition = (b, qualifier) => (condition) => {
191
+ if ('group' in condition) {
192
+ const { group, conditions } = condition;
193
+ if (conditions.length === 0)
194
+ return b.push(group === 'AND' ? '1 = 1' : '1 = 0');
195
+ return b.push('(').join(conditions, ` ${group} `, renderCondition(b, qualifier)).push(')');
196
+ }
197
+ const { col, op, value } = condition;
198
+ if (op === 'is' || op === 'is not')
199
+ return b.push(ref(col, qualifier), ' ', op.toUpperCase(), ' NULL');
200
+ if (op !== 'in' && op !== 'not in')
201
+ return b.push(ref(col, qualifier), ' ', op.toUpperCase(), ' ').param(encode(b.dialect, col, value));
202
+ const values = value;
203
+ if (values.length === 0)
204
+ return b.push(op === 'in' ? '1 = 0' : '1 = 1');
205
+ return b
206
+ .push(ref(col, qualifier), ' ', op.toUpperCase(), ' (')
207
+ .join(values, ', ', v => b.param(encode(b.dialect, col, v)))
208
+ .push(')');
209
+ };
210
+ const renderWhere = (b, conditions, qualifier) => conditions.length === 0
211
+ ? b
212
+ : b.push(' WHERE ').join(conditions, ' AND ', renderCondition(b, qualifier));
213
+ const parseChanges = (table, { set = {}, inc = {}, replace = [] }) => {
214
+ const updatable = Object.values(table.columns).filter(col => !col.generated && !col.primaryKey);
215
+ const numeric = updatable.filter(col => ['BIGINT', 'DECIMAL', 'FLOAT', 'INTEGER'].includes(col.type));
216
+ const sets = std.parseOrThrow(std.strictObject(Object.fromEntries(updatable.map(col => [col.key, std.optional(col.schema)]))), set);
217
+ const incs = std.parseOrThrow(std.strictObject(Object.fromEntries(numeric.map(col => [col.key, std.optional(std.number())]))), inc);
218
+ const replaced = replace.map(key => {
219
+ const col = updatable.find(col => col.key === key);
220
+ if (!col)
221
+ throw new Error(`Cannot replace "${key}": it's not an updatable field of "${table.name}"`);
222
+ return col;
223
+ });
224
+ return [
225
+ ...Object.entries(sets).filter(([, value]) => value !== undefined).map(([key, value]) => ({ col: columnOf(table, key), op: 'set', value })),
226
+ ...Object.entries(incs).filter(([, value]) => value !== undefined).map(([key, value]) => ({ col: columnOf(table, key), op: 'inc', value })),
227
+ ...replaced.map(col => ({ col, op: 'replace' })),
228
+ ];
229
+ };
230
+ // on conflict, `qualifier` tells the row's current value apart from EXCLUDED's
231
+ const renderAssignments = (b, assignments, qualifier) => b
232
+ .join(assignments, ', ', ({ col, op, value }) => {
233
+ if (op === 'replace')
234
+ return b.push(ref(col), ' = EXCLUDED.', quote(col.name));
235
+ if (op === 'inc')
236
+ return b.push(ref(col), ' = ', ref(col, qualifier), ' + ').param(value);
237
+ return b.push(ref(col), ' = ').param(encode(b.dialect, col, value));
238
+ });
239
+ const CLIENT_GENERATORS = {
240
+ UUID: () => Bun.randomUUIDv7(),
241
+ };
242
+ const isGeneratedByClient = (dialect, col) => col.generated === true && dialect.generated(col) === undefined;
243
+ const resolveInsertValue = (col, row) => {
244
+ if (col.generated) {
245
+ const generate = CLIENT_GENERATORS[col.type];
246
+ if (!generate)
247
+ throw new Error(`Cannot generate a value for column "${col.name}" client-side`);
248
+ return generate();
249
+ }
250
+ if (row[col.key] !== undefined)
251
+ return row[col.key];
252
+ if ('default' in col)
253
+ return typeof col.default === 'function' ? col.default() : col.default;
254
+ if (col.nullable)
255
+ return null;
256
+ return undefined;
257
+ };
258
+ class InsertBuilder {
259
+ table;
260
+ rows;
261
+ constructor(table, rows = []) {
262
+ this.table = table;
263
+ this.rows = rows;
264
+ }
265
+ values(rows) {
266
+ return new InsertBuilder(this.table, [...this.rows, ...(0, utils_1.wrap)(rows)]);
267
+ }
268
+ async insert(db, { returning = true, conflict } = {}) {
269
+ if (this.rows.length === 0)
270
+ return returning === false ? undefined : [];
271
+ const cols = Object.values(this.table.columns);
272
+ const insertable = cols.filter(col => !col.generated || isGeneratedByClient(db.dialect, col));
273
+ const schema = std.array(std.strictObject(Object.fromEntries(insertable.map(col => [col.key, col.schema]))));
274
+ const rows = std.parseOrThrow(schema, this.rows.map(row => ({
275
+ ...row,
276
+ ...Object.fromEntries(insertable.map(col => [col.key, resolveInsertValue(col, row)])),
277
+ })));
278
+ const b = new SqlBuilder(db.dialect)
279
+ .push('INSERT INTO ', quote(this.table.name), ' (', insertable.map(col => quote(col.name)).join(', '), ') VALUES ');
280
+ b.join(rows, ', ', row => b
281
+ .push('(')
282
+ .join(insertable, ', ', col => b.param(encode(db.dialect, col, row[col.key])))
283
+ .push(')'));
284
+ if (conflict)
285
+ this.renderConflict(b, conflict);
286
+ return await runReturning(db, b, this.table, returning);
287
+ }
288
+ renderConflict(b, conflict) {
289
+ if (conflict.do === 'throw')
290
+ return b;
291
+ b.push(' ON CONFLICT');
292
+ if (conflict.on.length > 0)
293
+ b.push(' (', conflict.on.map(key => quote(columnOf(this.table, key).name)).join(', '), ')');
294
+ if (conflict.do === 'nothing')
295
+ return b.push(' DO NOTHING');
296
+ const assignments = parseChanges(this.table, conflict.do);
297
+ if (assignments.length === 0)
298
+ throw new Error('Nothing to update on conflict: `set`, `inc` and `replace` are empty');
299
+ return renderAssignments(b.push(' DO UPDATE SET '), assignments, this.table.name);
300
+ }
301
+ }
302
+ const into = (table) => new InsertBuilder(table);
303
+ exports.into = into;
304
+ // `S` holds the selected fields, every field unless `select` says otherwise
305
+ class QueryBuilder {
306
+ query;
307
+ constructor(query) {
308
+ this.query = query;
309
+ }
310
+ select(keys) {
311
+ return new QueryBuilder({ ...this.query, select: keys });
312
+ }
313
+ where(...args) {
314
+ const node = args.length === 1 ? args[0] : { key: args[0], op: args[1], value: args[2] };
315
+ return new QueryBuilder({ ...this.query, where: [...this.query.where, toCondition(this.query.table, node)] });
316
+ }
317
+ orderBy(keys) {
318
+ const orderBy = keys.map(key => typeof key === 'string'
319
+ ? [columnOf(this.query.table, key), 'ASC']
320
+ : [columnOf(this.query.table, key[0]), key[1]]);
321
+ return new QueryBuilder({ ...this.query, orderBy: [...this.query.orderBy, ...orderBy] });
322
+ }
323
+ limit(n) {
324
+ return new QueryBuilder({ ...this.query, limit: n });
325
+ }
326
+ offset(n) {
327
+ return new QueryBuilder({ ...this.query, offset: n });
328
+ }
329
+ async all(db) {
330
+ const { table, select } = this.query;
331
+ const cols = select
332
+ ? select.map(key => columnOf(table, key))
333
+ : Object.values(table.columns);
334
+ const rows = await run(db, this.renderSelect(new SqlBuilder(db.dialect), cols).build());
335
+ return rows.map(decoder(db.dialect, cols));
336
+ }
337
+ async one(db) {
338
+ const [row] = await this.top().all(db);
339
+ return row ?? null;
340
+ }
341
+ async exists(db) {
342
+ const rows = await this.top().select([]).all(db);
343
+ return rows.length > 0;
344
+ }
345
+ // counts the rows `select` would return: as a subquery, limit and offset
346
+ // still apply, and postgres doesn't take ORDER BY next to a bare COUNT(*)
347
+ async count(db) {
348
+ const b = new SqlBuilder(db.dialect).push('SELECT COUNT(*) AS "count" FROM (');
349
+ const [row] = await run(db, this.renderSelect(b, []).push(') AS "q"').build());
350
+ // postgres returns COUNT(*) as a bigint
351
+ return Number(row['count']);
352
+ }
353
+ async update(db, { returning = true, ...changes }) {
354
+ this.assertUnbounded('update');
355
+ const { table, where } = this.query;
356
+ const assignments = parseChanges(table, changes);
357
+ if (assignments.length === 0)
358
+ throw new Error('Nothing to update: both `set` and `inc` are empty');
359
+ const b = new SqlBuilder(db.dialect).push('UPDATE ', quote(table.name), ' SET ');
360
+ renderAssignments(b, assignments);
361
+ renderWhere(b, where);
362
+ return await runReturning(db, b, table, returning);
363
+ }
364
+ async delete(db, { returning = false } = {}) {
365
+ this.assertUnbounded('delete');
366
+ const { table, where } = this.query;
367
+ const b = new SqlBuilder(db.dialect).push('DELETE FROM ', quote(table.name));
368
+ renderWhere(b, where);
369
+ return await runReturning(db, b, table, returning);
370
+ }
371
+ renderSelect(b, cols) {
372
+ const { table, where, orderBy, limit, offset } = this.query;
373
+ b.push('SELECT ');
374
+ // an empty select list isn't valid SQL, but each matching row still needs
375
+ // an (empty) object
376
+ if (cols.length === 0)
377
+ b.push('1');
378
+ else
379
+ b.join(cols, ', ', col => b.push(ref(col, table.alias), ' AS ', quote(col.key)));
380
+ b.push(' FROM ', quote(table.name), ' AS ', quote(table.alias));
381
+ renderWhere(b, where, table.alias);
382
+ if (orderBy.length > 0)
383
+ b.push(' ORDER BY ').join(orderBy, ', ', ([col, dir]) => b.push(ref(col, table.alias), ' ', dir));
384
+ // sqlite requires LIMIT whenever there's an OFFSET, and there's no "no
385
+ // limit" value both sqlite and postgres accept other than a huge number
386
+ if (limit !== undefined || offset !== undefined)
387
+ b.push(' LIMIT ').param(limit ?? Number.MAX_SAFE_INTEGER);
388
+ if (offset !== undefined)
389
+ b.push(' OFFSET ').param(offset);
390
+ return b;
391
+ }
392
+ // keeps a `limit(0)`, which can never match
393
+ top() {
394
+ return this.limit(Math.min(this.query.limit ?? 1, 1));
395
+ }
396
+ // UPDATE/DELETE ... ORDER BY/LIMIT isn't portable, so refuse it rather than
397
+ // silently affecting more rows than the query suggests
398
+ assertUnbounded(operation) {
399
+ const { orderBy, limit, offset } = this.query;
400
+ if (orderBy.length > 0 || limit !== undefined || offset !== undefined) {
401
+ throw new Error(`Cannot ${operation} with orderBy, limit or offset`);
402
+ }
403
+ }
404
+ }
405
+ const from = (table) => new QueryBuilder({ table, where: [], orderBy: [] });
406
+ exports.from = from;
407
+ // ─── ddl ─────────────────────────────────────────────────────────────────────
408
+ const renderColumnDefinition = (dialect, col) => [
409
+ quote(col.name),
410
+ dialect.type(col),
411
+ col.primaryKey && 'PRIMARY KEY',
412
+ col.generated && dialect.generated(col),
413
+ !col.nullable && 'NOT NULL',
414
+ col.unique && 'UNIQUE',
415
+ ].filter(Boolean).join(' ');
416
+ const createIndex = (table, keys, { ifNotExists = false, name, unique = false, } = {}) => ({
417
+ onto: async (db) => {
418
+ const cols = keys.map(key => columnOf(table, key).name);
419
+ const index = name ?? `${table.name}_${cols.join('_')}_index`;
420
+ const sql = `CREATE ${unique ? 'UNIQUE ' : ''}INDEX ${ifNotExists ? 'IF NOT EXISTS ' : ''}${quote(index)} ON ${quote(table.name)} (${cols.map(quote).join(', ')})`;
421
+ await run(db, { sql, params: [] });
422
+ },
423
+ });
424
+ const create = {
425
+ table: (table, { ifNotExists = false } = {}) => ({
426
+ onto: async (db) => {
427
+ const cols = Object.values(table.columns).map(col => renderColumnDefinition(db.dialect, col));
428
+ const sql = `CREATE TABLE ${ifNotExists ? 'IF NOT EXISTS ' : ''}${quote(table.name)} (${cols.join(', ')})`;
429
+ await run(db, { sql, params: [] });
430
+ },
431
+ }),
432
+ index: createIndex,
433
+ uniqueIndex: (table, keys, options = {}) => createIndex(table, keys, { ...options, unique: true }),
434
+ };
435
+ exports.create = create;
436
+ // ─── migrations ──────────────────────────────────────────────────────────────
437
+ const schemaMigrations = defineTable('schema_migrations', t => ({
438
+ id: t.string().primaryKey(),
439
+ migratedAt: t.datetime(),
440
+ }));
441
+ const defineMigrations = (migrations) => async (db) => {
442
+ await create.table(schemaMigrations, { ifNotExists: true }).onto(db);
443
+ const applied = new Set((await from(schemaMigrations).select(['id']).all(db)).map(row => row.id));
444
+ for (const id of Object.keys(migrations).sort()) {
445
+ if (applied.has(id))
446
+ continue;
447
+ await db.transaction(async (tx) => {
448
+ await migrations[id](tx);
449
+ await into(schemaMigrations).values({ id, migratedAt: new Date() }).insert(tx);
450
+ });
451
+ }
452
+ };
453
+ exports.defineMigrations = defineMigrations;