@rwillians/exto 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # exto
2
2
 
3
- A teeny tiny, type-safe and zero-dependency ORM for [Bun](https://bun.sh),
4
- with adapters for SQLite and PostgreSQL.
3
+ A teeny tiny, type-safe query builder for [Bun](https://bun.sh), with
4
+ adapters for SQLite and PostgreSQL.
5
5
 
6
6
  ```ts
7
- import { create, from, into, table } from '@rwillians/exto';
7
+ import { create, from, into, table, t } from '@rwillians/exto';
8
8
  import * as sqlite from '@rwillians/exto/bun-sqlite';
9
9
 
10
- const users = table('users', t => ({
11
- id: t.sequence().primaryKey(),
10
+ const users = table('users', {
11
+ id: t.uuidv7().generated().primaryKey(),
12
12
  name: t.string(),
13
13
  email: t.citext().unique(),
14
- createdAt: t.datetime().default(() => new Date()),
15
- }));
14
+ createdAt: t.timestamp().default(() => new Date()),
15
+ });
16
16
 
17
17
  const db = sqlite.connect('./db.sqlite');
18
18
  await create.table(users, { ifNotExists: true }).onto(db);
@@ -22,19 +22,16 @@ await into(users)
22
22
  .insert(db);
23
23
 
24
24
  const jane = await from(users)
25
- .where('email', '=', 'JANE@example.com')
26
- .select(['id', 'name'])
25
+ .where(users.email, '=', 'JANE@example.com')
26
+ .select({ id: users.id, name: users.name })
27
27
  .one(db);
28
- // → { id: number, name: string } | null
28
+ // → { id: string, name: string } | null
29
29
  ```
30
30
 
31
- - Keys, operators and values are type-checked and autocompleted.
32
- - Inserted and updated values are validated before any SQL runs.
31
+ - Columns, operators and values are type-checked and autocompleted.
32
+ - Values are validated before any SQL runs.
33
33
  - No singletons: you always pass the database explicitly.
34
34
 
35
- > [!NOTE]
36
- > exto covers single-table queries for now. See [what's not supported yet](#not-supported-yet).
37
-
38
35
 
39
36
  ## Install
40
37
 
@@ -43,85 +40,96 @@ bun add @rwillians/exto
43
40
  ```
44
41
 
45
42
 
46
- ## Tables
43
+ ## Connecting
47
44
 
48
45
  ```ts
49
- import { table } from '@rwillians/exto';
50
- import { z } from 'zod';
46
+ import * as sqlite from '@rwillians/exto/bun-sqlite';
47
+ import * as pg from '@rwillians/exto/bun-postgres';
51
48
 
52
- const tasks = table('tasks', t => ({
53
- id: t.uuid().generated().primaryKey(),
54
- parentId: t.uuid().nullable(),
55
- status: t.enum(['queued', 'running', 'done']),
56
- payload: t.json(z.record(z.string(), z.any())),
57
- attempts: t.integer().default(0),
58
- scheduledAt: t.datetime(),
59
- }));
60
-
61
- type Task = typeof tasks.infer; // a row
62
- type TaskInsert = typeof tasks.inferForInsert; // what `.values()` takes
63
- type TaskUpdate = typeof tasks.inferForUpdate; // what `set` takes
64
- ```
49
+ const db = sqlite.connect(':memory:');
50
+ const db = pg.connect('postgres://user:pass@localhost:5432/app');
65
51
 
66
- Keys are camelCase in TypeScript and snake_case in the database
67
- (`scheduledAt` → `scheduled_at`).
68
-
69
- | builder | TS type | postgres | sqlite |
70
- | :--------------------------------- | :-------------------- | :----------------- | :---------------------- |
71
- | `t.bigint()` | `bigint` | `BIGINT` | `INTEGER` |
72
- | `t.binary()` | `Uint8Array` | `BYTEA` | `BLOB` |
73
- | `t.boolean()` | `boolean` | `BOOLEAN` | `INTEGER` |
74
- | `t.citext()` | `string` | `CITEXT` | `TEXT COLLATE NOCASE` |
75
- | `t.date()` | `string` (YYYY-MM-DD) | `DATE` | `TEXT` |
76
- | `t.datetime()` | `Date` | `TIMESTAMPTZ` | `INTEGER` |
77
- | `t.decimal({ precision, scale }?)` | `number` | `NUMERIC(p, s)` | `NUMERIC` |
78
- | `t.enum([...values])` | union of values | `VARCHAR(n)` | `TEXT` |
79
- | `t.float()` | `number` | `DOUBLE PRECISION` | `REAL` |
80
- | `t.integer()` | `number` | `BIGINT` | `INTEGER` |
81
- | `t.json(schema)` | schema's output | `JSONB` | `TEXT` |
82
- | `t.sequence()` | `number` | `BIGINT` identity | `INTEGER` autoincrement |
83
- | `t.string({ size? })` | `string` | `VARCHAR(size)` | `TEXT` |
84
- | `t.text()` | `string` | `TEXT` | `TEXT` |
85
- | `t.uuid()` | `string` | `UUID` | `TEXT` |
86
-
87
- | modifier | effect |
88
- | :-------------- | :---------------------------------------------- |
89
- | `.primaryKey()` | primary key |
90
- | `.nullable()` | accepts `null`, optional on insert |
91
- | `.default(v)` | value or function used when omitted on insert |
92
- | `.unique()` | unique constraint |
93
- | `.generated()` | value generated on insert (`t.uuid()` → UUIDv7) |
52
+ // log every statement
53
+ const db = sqlite.connect(':memory:', { loggers: [console] });
54
+ ```
94
55
 
95
- `t.json()` takes any [Standard Schema](https://standardschema.dev) (zod,
96
- valibot, arktype, ...). `t.citext()` needs `CREATE EXTENSION citext` on
97
- postgres (see [`exec`](#migrations)). `t.decimal()` values are JS numbers;
98
- precision and scale only shape postgres' column.
99
56
 
100
- ### Custom columns
57
+ ## Tables
101
58
 
102
59
  ```ts
103
- import { column } from '@rwillians/exto';
60
+ import { embedded, table, t, type Infer } from '@rwillians/exto';
104
61
 
105
- const email = () => column({ type: 'CITEXT', schema: z.email() });
62
+ const tasks = table('tasks', {
63
+ id: t.uuidv7().generated().primaryKey(),
64
+ parentId: t.uuidv7().optional(),
65
+ status: t.enum(['queued', 'running', 'done']),
66
+ attempts: t.integer().unsigned().default(0),
67
+ maxAttempts: t.integer().unsigned().default(3),
68
+ payload: embedded.one({
69
+ kind: t.string(),
70
+ params: t.record().default({}),
71
+ }),
72
+ events: embedded.many({
73
+ kind: t.string(),
74
+ timestamp: t.timestamp().default(() => new Date()),
75
+ }),
76
+ scheduledTo: t.timestamp().default(() => new Date()),
77
+ });
106
78
 
107
- const users = table('users', t => ({
108
- id: t.sequence().primaryKey(),
109
- email: email().unique(),
110
- }));
79
+ type Task = Infer<'select', typeof tasks>; // a row
80
+ type TaskInsert = Infer<'insert', typeof tasks>; // what `.values()` takes
81
+ type TaskUpdate = Infer<'update', typeof tasks>; // what `set` takes
111
82
  ```
112
83
 
84
+ Keys are camelCase in TypeScript and snake_case in the database
85
+ (`scheduledTo` → `scheduled_to`).
86
+
87
+ | builder | TS type | postgres | sqlite |
88
+ | :--------------------------------- | :------------------------ | :----------------- | :---------------------- |
89
+ | `t.bigint()` | `bigint` | `BIGINT` | `INTEGER` |
90
+ | `t.binary()` | `Uint8Array` | `BYTEA` | `BLOB` |
91
+ | `t.boolean()` | `boolean` | `BOOLEAN` | `INTEGER` |
92
+ | `t.citext()` | `string` | `CITEXT` | `TEXT COLLATE NOCASE` |
93
+ | `t.date()` | `string` (YYYY-MM-DD) | `DATE` | `TEXT` |
94
+ | `t.decimal({ precision, scale }?)` | `number` | `NUMERIC(p, s)` | `NUMERIC` |
95
+ | `t.enum([...values])` | union of values | `VARCHAR(n)` | `TEXT` |
96
+ | `t.float()` | `number` | `DOUBLE PRECISION` | `REAL` |
97
+ | `t.integer()` | `number` | `BIGINT` | `INTEGER` |
98
+ | `t.json(schema)` | schema's output | `JSONB` | `TEXT` |
99
+ | `t.record()` | `Record<string, unknown>` | `JSONB` | `TEXT` |
100
+ | `t.sequence()` | `number` | `BIGINT` identity | `INTEGER` autoincrement |
101
+ | `t.string({ size? })` | `string` | `VARCHAR(size)` | `TEXT` |
102
+ | `t.text()` | `string` | `TEXT` | `TEXT` |
103
+ | `t.timestamp()` | `Date` | `TIMESTAMPTZ` | `INTEGER` |
104
+ | `t.uuidv7()` | `string` | `UUID` | `TEXT` |
105
+ | `embedded.one({ ... })` | an object | `JSONB` | `TEXT` |
106
+ | `embedded.many({ ... })` | an array of objects | `JSONB` | `TEXT` |
107
+
108
+ | modifier | effect |
109
+ | :-------------- | :-------------------------------------------- |
110
+ | `.primaryKey()` | primary key |
111
+ | `.optional()` | accepts `null`, optional on insert |
112
+ | `.default(v)` | value or function used when omitted on insert |
113
+ | `.unique()` | unique constraint |
114
+ | `.generated()` | a UUIDv7 generated on insert (`t.uuidv7()`) |
115
+ | `.unsigned()` | rejects negatives (`t.integer()`) |
113
116
 
114
- ## Connecting
117
+ `t.json()` takes any [Standard Schema](https://standardschema.dev) (zod,
118
+ valibot, arktype, ...). `t.citext()` needs `CREATE EXTENSION citext` on
119
+ postgres (see [migrations](#migrations)).
120
+
121
+ ### Custom types
115
122
 
116
123
  ```ts
117
- import * as sqlite from '@rwillians/exto/bun-sqlite';
118
- import * as pg from '@rwillians/exto/bun-postgres';
124
+ import { defineType } from '@rwillians/exto';
125
+ import { z } from 'zod';
119
126
 
120
- const db = sqlite.connect(':memory:');
121
- const db = pg.connect('postgres://user:pass@localhost:5432/app');
127
+ const email = () => defineType({ primitive: 'STRING', schema: z.email() });
122
128
 
123
- // log every statement
124
- const db = sqlite.connect(':memory:', { loggers: [console] });
129
+ const users = table('users', {
130
+ id: t.uuidv7().generated().primaryKey(),
131
+ email: email(),
132
+ });
125
133
  ```
126
134
 
127
135
 
@@ -138,52 +146,48 @@ const migrate = defineMigrations({
138
146
  },
139
147
  '0002': async (db) => {
140
148
  await create.table(tasks).onto(db);
141
- await create.index(tasks, ['status', 'scheduledAt']).onto(db);
149
+ await create.index(tasks, ['status', 'scheduledTo']).onto(db);
142
150
  },
143
151
  });
144
152
 
145
153
  await migrate(db);
146
154
  ```
147
155
 
148
- Migrations run in key order, each in its own transaction, and only once.
156
+ Migrations run in key order, each in its own transaction, and only once:
157
+ applied ones are tracked in the `schema_migrations` table.
149
158
 
150
159
 
151
160
  ## Insert
152
161
 
153
162
  ```ts
154
- import { into } from '@rwillians/exto';
163
+ import { into, parse } from '@rwillians/exto';
155
164
 
156
- const row = { status: 'queued', payload: {}, scheduledAt: new Date() } as const;
165
+ const task: TaskInsert = { status: 'queued', payload: { kind: 'Prune' }, events: [] };
157
166
 
158
167
  const inserted: Task[] = await into(tasks)
159
- .values(row)
160
- .values([row, row])
168
+ .values(task)
169
+ .values([task, task])
161
170
  .insert(db);
162
171
 
163
- await into(tasks).values(row).insert(db, { returning: ['id'] }); // → { id: string }[]
164
- await into(tasks).values(row).insert(db, { returning: false }); // → undefined
172
+ await into(tasks).values(task).insert(db, { returning: ['id', tasks.status] }); // → Pick<Task, 'id' | 'status'>[]
173
+ await into(tasks).values(task).insert(db, { returning: false }); // → undefined
174
+
175
+ // validate untrusted input, filling in defaults
176
+ const row: TaskInsert = parse(tasks, JSON.parse(body));
165
177
  ```
166
178
 
167
179
  `insert`, `update` and `delete` all take `returning`. It defaults to `true`,
168
180
  except for `delete`, which defaults to `false`:
169
181
 
170
- | `returning` | result |
171
- | :---------- | :------------------------ |
172
- | `true` | full rows |
173
- | `[...keys]` | only those fields |
174
- | `false` | `undefined` |
182
+ | `returning` | result |
183
+ | :--------------------- | :---------- |
184
+ | `true` | full rows |
185
+ | `[...keys or columns]` | only those |
186
+ | `false` | `undefined` |
175
187
 
176
188
  ### On conflict
177
189
 
178
190
  ```ts
179
- const users = table('users', t => ({
180
- id: t.sequence().primaryKey(),
181
- email: t.citext().unique(),
182
- name: t.string(),
183
- logins: t.integer().default(0),
184
- banned: t.boolean().default(false),
185
- }));
186
-
187
191
  const rows = [{ email: 'jane@example.com', name: 'Jane' }];
188
192
 
189
193
  // skip rows that conflict
@@ -212,53 +216,128 @@ await into(users).values(rows).insert(db, {
212
216
 
213
217
  ## Select
214
218
 
219
+ A table is shaped like its columns: `tasks.status` references a column.
220
+
215
221
  ```ts
216
222
  import { expr, from } from '@rwillians/exto';
217
223
 
218
- const rows: Task[] = await from(tasks)
219
- .where('status', '=', 'queued')
220
- .where('scheduledAt', '<', new Date()) // multiple `where`s are ANDed
221
- .orderBy(['scheduledAt', ['attempts', 'DESC']])
224
+ const due: Task[] = await from(tasks)
225
+ .where(tasks.status, '=', 'queued')
226
+ .where(tasks.scheduledTo, '<', new Date()) // multiple `where`s are ANDed
227
+ .orderBy([tasks.scheduledTo, [tasks.attempts, 'DESC']])
222
228
  .limit(10)
223
229
  .offset(20)
224
230
  .all(db);
225
231
 
226
- const queued = from(tasks).where('status', '=', 'queued');
232
+ const queued = from(tasks).where(tasks.status, '=', 'queued');
227
233
 
228
- await queued.one(db); // → Task | null (the top row)
229
- await queued.select(['id', 'status']).all(db); // → Pick<Task, 'id' | 'status'>[]
230
- await queued.exists(db); // → boolean
231
- await queued.count(db); // → number
234
+ await queued.one(db); // → Task | null
235
+ await queued.exists(db); // → boolean
236
+ await queued.count(db); // → number
232
237
  ```
233
238
 
234
- | operator | value |
235
- | :------------------------------------- | :------------------------- |
236
- | `'='` `'!='` `'<'` `'<='` `'>'` `'>='` | the column's type |
237
- | `'like'` `'not like'` | `string` |
238
- | `'in'` `'not in'` | array of the column's type |
239
- | `'is'` `'is not'` | `null` |
239
+ | operator | value |
240
+ | :------------------------------------- | :---------------- |
241
+ | `'='` `'!='` `'<'` `'<='` `'>'` `'>='` | the column's type |
242
+ | `'is'` `'is not'` | `null` |
243
+
244
+ ### Picking columns
245
+
246
+ ```ts
247
+ await from(tasks).select({ id: tasks.id, when: tasks.scheduledTo }).all(db);
248
+ // → { id: string, when: Date }[]
249
+
250
+ await from(tasks).select({ ...tasks, parent: tasks.parentId }).all(db);
251
+ // → (Task & { parent: string | null })[]
252
+ ```
253
+
254
+ ### Comparing columns
255
+
256
+ ```ts
257
+ await from(tasks).where(tasks.attempts, '<', tasks.maxAttempts).all(db);
258
+ ```
240
259
 
241
260
  ### OR and grouping
242
261
 
243
262
  ```ts
244
263
  await from(tasks)
245
264
  .where(expr.or([
246
- expr('status', '=', 'queued'),
265
+ expr(tasks.status, '=', 'queued'),
247
266
  expr.and([
248
- expr('status', '=', 'running'),
249
- expr('attempts', '>', 3),
267
+ expr(tasks.status, '=', 'running'),
268
+ expr(tasks.attempts, '>', 3),
250
269
  ]),
251
270
  ]))
252
271
  .all(db);
253
- // WHERE ("status" = ? OR ("status" = ? AND "attempts" > ?))
254
272
  ```
255
273
 
274
+ ### Embedded fields
275
+
276
+ Fields of `embedded.one()` columns are referenced like columns:
277
+
278
+ ```ts
279
+ await from(tasks)
280
+ .where(tasks.payload.kind, '=', 'Prune')
281
+ .select({ kind: tasks.payload.kind, params: tasks.payload.params })
282
+ .all(db);
283
+ // → { kind: string, params: Record<string, unknown> }[]
284
+ ```
285
+
286
+ ### Raw SQL fragments
287
+
288
+ ```ts
289
+ import { f } from '@rwillians/exto';
290
+
291
+ await from(tasks).where(f`upper(${tasks.payload.kind})`, '=', 'PRUNE').all(db);
292
+ ```
293
+
294
+ Interpolated columns are rendered in place, and any other value is passed as
295
+ a parameter.
296
+
297
+
298
+ ## Group by
299
+
300
+ ```ts
301
+ await from(tasks)
302
+ .where(tasks.status, '!=', 'queued')
303
+ .groupBy([tasks.status])
304
+ .select({
305
+ status: tasks.status,
306
+ tasks: expr.count(tasks.id),
307
+ attempts: expr.sum(tasks.attempts),
308
+ })
309
+ .all(db);
310
+ // → { status: Task['status'], tasks: number, attempts: number }[]
311
+ ```
312
+
313
+
314
+ ## Joins
315
+
316
+ Alias a table to query it more than once:
317
+
318
+ ```ts
319
+ const parent = tasks.as('parent');
320
+ const child = tasks.as('child');
321
+
322
+ await from(parent)
323
+ .leftJoin(child, expr(parent.id, '=', child.parentId))
324
+ .where(parent.parentId, 'is', null)
325
+ .groupBy([parent.id])
326
+ .select({ ...parent, children: expr.count(child.id) })
327
+ .all(db);
328
+ // → (Task & { children: number })[]
329
+ ```
330
+
331
+ Columns of a left-joined table are nullable: selecting `child.id` gives
332
+ `string | null`. A query only takes columns of the tables it reads from, so
333
+ referencing `child` before joining it is a type error.
334
+
256
335
 
257
336
  ## Update
258
337
 
259
338
  ```ts
260
339
  const updated: Pick<Task, 'id'>[] = await from(tasks)
261
- .where('status', '=', 'queued')
340
+ .where(tasks.status, '=', 'queued')
262
341
  .update(db, {
263
342
  set: { status: 'running' },
264
343
  inc: { attempts: 1 },
@@ -270,12 +349,12 @@ const updated: Pick<Task, 'id'>[] = await from(tasks)
270
349
  ## Delete
271
350
 
272
351
  ```ts
273
- await from(tasks).where('status', '=', 'done').delete(db); // → undefined
274
- await from(tasks).where('status', '=', 'done').delete(db, { returning: true }); // → Task[]
352
+ await from(tasks).where(tasks.status, '=', 'done').delete(db); // → undefined
353
+ await from(tasks).where(tasks.status, '=', 'done').delete(db, { returning: true }); // → Task[]
275
354
  ```
276
355
 
277
- `update` and `delete` refuse `orderBy`, `limit` and `offset`, so they never
278
- touch more rows than the query suggests.
356
+ `update` and `delete` refuse joins, `groupBy`, `orderBy`, `limit` and
357
+ `offset`, so they never touch more rows than the query suggests.
279
358
 
280
359
 
281
360
  ## Transactions
@@ -295,53 +374,28 @@ The transaction rolls back if the callback throws.
295
374
 
296
375
  ## Validation
297
376
 
298
- Rows and changes are validated against the columns before any SQL runs:
299
-
300
377
  ```ts
301
378
  import { ValidationError } from '@rwillians/exto';
302
379
 
303
- const people = table('people', t => ({
304
- id: t.sequence().primaryKey(),
380
+ const people = table('people', {
381
+ id: t.uuidv7().generated().primaryKey(),
305
382
  name: t.string({ size: 5 }),
306
383
  age: t.integer(),
307
- }));
384
+ });
308
385
 
309
386
  try {
310
387
  await into(people).values({ name: 'Jane Doe', age: 1.5 }).insert(db);
311
388
  } catch (error) {
312
389
  if (error instanceof ValidationError) {
313
390
  error.message; // Failed validation:
314
- // - 0.name: must be at most 5 characters long
315
- // - 0.age: must be an integer
316
- error.issues; // [{ message, path: [0, 'name'] }, ...]
391
+ // - name: must be at most 5 characters long
392
+ // - age: must be an integer
393
+ error.issues; // [{ message, path: ['name'] }, ...]
317
394
  }
318
395
  }
319
396
  ```
320
397
 
321
398
 
322
- ## Not supported yet
323
-
324
- exto is young, and it focuses on queries against a single table. Every query
325
- reads from one table only, so joins aren't possible yet. Neither is nesting one
326
- query inside another, whether as a subquery (`WHERE id IN (SELECT ...)`) or as
327
- a common table expression (`WITH ... SELECT`).
328
-
329
- Selects return columns as they're stored: `.select()` takes column keys only,
330
- so computed values such as `attempts * 2` can't be selected. Aggregation stops
331
- at `.count()`, too. There's no `.groupBy()` to group rows, and no `.having()`
332
- to filter those groups.
333
-
334
- All of these are planned. Until then, `db.execute()` runs raw SQL and returns
335
- the rows as the driver gives them, without exto's decoding:
336
-
337
- ```ts
338
- const totals = await db.execute(
339
- 'SELECT status, COUNT(*) AS total FROM tasks GROUP BY status',
340
- [],
341
- );
342
- ```
343
-
344
-
345
399
  ## License
346
400
 
347
401
  [MIT](LICENSE)
@@ -0,0 +1,100 @@
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 TYPES = {
6
+ BIGINT: () => 'BIGINT',
7
+ BINARY: () => 'BYTEA',
8
+ BOOLEAN: () => 'BOOLEAN',
9
+ CITEXT: () => 'CITEXT',
10
+ DATE: () => 'DATE',
11
+ DECIMAL: ({ precision, scale }) => precision !== undefined && scale !== undefined ? `NUMERIC(${precision}, ${scale})` : 'NUMERIC',
12
+ FLOAT: () => 'DOUBLE PRECISION',
13
+ INTEGER: () => 'BIGINT',
14
+ JSON: () => 'JSONB',
15
+ SEQUENCE: () => 'BIGINT',
16
+ STRING: ({ length = 255 }) => `VARCHAR(${length})`,
17
+ TEXT: () => 'TEXT',
18
+ TIMESTAMP: () => 'TIMESTAMPTZ',
19
+ UUID: () => 'UUID',
20
+ };
21
+ const identity = () => ({
22
+ encode: (value) => value,
23
+ decode: (value) => value,
24
+ });
25
+ // Bun.SQL reads bigints (BIGINT columns, COUNT and SUM) and NUMERICs as
26
+ // strings
27
+ const numeric = {
28
+ encode: (value) => value,
29
+ decode: (value) => Number(value),
30
+ };
31
+ // `#>>` reads text, so fields of other types are cast back to theirs
32
+ const CASTS = {
33
+ BIGINT: 'BIGINT',
34
+ BOOLEAN: 'BOOLEAN',
35
+ DECIMAL: 'NUMERIC',
36
+ FLOAT: 'DOUBLE PRECISION',
37
+ INTEGER: 'DOUBLE PRECISION',
38
+ SEQUENCE: 'BIGINT',
39
+ };
40
+ const jsonPath = (path) => `'{${path.map(key => `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\'', '\'\'')}"`).join(',')}}'`;
41
+ const json = (sql, path, type) => {
42
+ if (type === 'JSON')
43
+ return `${sql} #> ${jsonPath(path)}`;
44
+ const text = `${sql} #>> ${jsonPath(path)}`;
45
+ return CASTS[type] ? `(${text})::${CASTS[type]}` : text;
46
+ };
47
+ // Bun.SQL sends objects and arrays as JSON, and reads JSONB already parsed
48
+ const dialect = {
49
+ name: 'postgres',
50
+ placeholder: (index) => `$${index}`,
51
+ type: (col) => TYPES[col.type](col),
52
+ generated: (col) => col.type === 'SEQUENCE'
53
+ ? 'GENERATED ALWAYS AS IDENTITY'
54
+ : undefined,
55
+ json,
56
+ codecs: {
57
+ BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
58
+ BINARY: { encode: (value) => value, decode: (value) => new Uint8Array(value) },
59
+ BOOLEAN: identity(),
60
+ CITEXT: identity(),
61
+ DATE: { encode: (value) => value, decode: (value) => value instanceof Date ? value.toISOString().slice(0, 10) : value },
62
+ DECIMAL: numeric,
63
+ FLOAT: numeric,
64
+ INTEGER: numeric,
65
+ JSON: identity(),
66
+ SEQUENCE: numeric,
67
+ STRING: identity(),
68
+ TEXT: identity(),
69
+ TIMESTAMP: { encode: (value) => value, decode: (value) => new Date(value) },
70
+ UUID: identity(),
71
+ },
72
+ };
73
+ exports.dialect = dialect;
74
+ class BunPostgres {
75
+ sql;
76
+ loggers;
77
+ inTransaction;
78
+ dialect = dialect;
79
+ constructor(sql, loggers = [], inTransaction = false) {
80
+ this.sql = sql;
81
+ this.loggers = loggers;
82
+ this.inTransaction = inTransaction;
83
+ }
84
+ async execute(sql, params) {
85
+ return await this.sql.unsafe(sql, params);
86
+ }
87
+ async close() {
88
+ await this.sql.close();
89
+ }
90
+ // nested transactions are savepoints of the outer one
91
+ async transaction(fn) {
92
+ const callback = (tx) => fn(new BunPostgres(tx, this.loggers, true));
93
+ const result = this.inTransaction
94
+ ? this.sql.savepoint(callback)
95
+ : this.sql.begin(callback);
96
+ return await result;
97
+ }
98
+ }
99
+ const connect = (url, { loggers = [] } = {}) => new BunPostgres(new bun_1.SQL(url), loggers);
100
+ exports.connect = connect;