relq 1.0.96 → 1.0.98

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  **The Fully-Typed PostgreSQL ORM for TypeScript**
4
4
 
5
- Relq is a complete, type-safe ORM for PostgreSQL that brings the full power of the database to TypeScript. With support for 100+ PostgreSQL types, advanced features like partitions, domains, composite types, generated columns, enums, triggers, functions, and a git-like CLI for schema management—all with zero runtime dependencies.
5
+ Relq is a complete, type-safe ORM for PostgreSQL that brings the full power of the database to TypeScript. With support for 100+ PostgreSQL types, advanced features like partitions, domains, composite types, generated columns, enums, triggers, functions, and a CLI for schema management all with zero runtime dependencies.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/relq.svg)](https://www.npmjs.com/package/relq)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -17,11 +17,20 @@ Relq is a complete, type-safe ORM for PostgreSQL that brings the full power of t
17
17
  - [Installation](#installation)
18
18
  - [Quick Start](#quick-start)
19
19
  - [Entry Points](#entry-points)
20
+ - [Multi-Dialect Support](#multi-dialect-support)
20
21
  - [Schema Definition](#schema-definition)
21
22
  - [Query API](#query-api)
23
+ - [Joins](#joins)
24
+ - [Relation Loading](#relation-loading)
25
+ - [Computed Columns](#computed-columns)
26
+ - [Convenience Methods](#convenience-methods)
27
+ - [Raw SQL](#raw-sql)
22
28
  - [SQL Functions](#sql-functions)
23
29
  - [Condition Builders](#condition-builders)
30
+ - [Pagination](#pagination)
31
+ - [Transactions](#transactions)
24
32
  - [Advanced Schema Features](#advanced-schema-features)
33
+ - [DDL Builders](#ddl-builders)
25
34
  - [CLI Commands](#cli-commands)
26
35
  - [Configuration](#configuration)
27
36
  - [Error Handling](#error-handling)
@@ -39,10 +48,28 @@ Relq is a complete, type-safe ORM for PostgreSQL that brings the full power of t
39
48
  - **Tree-Shakeable** — Import only what you use for optimal bundle size
40
49
  - **Schema-First Design** — Define once, get types everywhere
41
50
 
51
+ ### Multi-Dialect Support
52
+
53
+ - **PostgreSQL** — Full native support
54
+ - **CockroachDB** — Dedicated client with CockroachDB-compatible feature set
55
+ - **Nile** — Multi-tenant PostgreSQL with `setTenant()` / `withTenant()`
56
+ - **AWS DSQL** — Amazon Aurora DSQL with AWS credential support
57
+
58
+ ### Query Features
59
+
60
+ - **Relation Loading** — `.with()` and `.withMany()` for auto-FK joins without writing join conditions
61
+ - **Computed Columns** — `.include()` for inline aggregates and expressions
62
+ - **One-to-Many Joins** — LATERAL subqueries via `.joinMany()` / `.leftJoinMany()`
63
+ - **Many-to-Many** — Junction table support via `{ through: 'table' }`
64
+ - **Tagged Template SQL** — `` sql`SELECT * FROM users WHERE id = ${id}` `` with auto-escaping
65
+ - **Nested Creates** — `.createWith()` inserts parent + children in a single transaction
66
+ - **INSERT FROM SELECT** — `.insertFrom()` with type-safe callback
67
+ - **Cursor Iteration** — `.each()` for memory-efficient row-by-row processing
68
+ - **Conflict Handling** — `.doUpdate()` / `.doNothing()` with EXCLUDED column access
69
+
42
70
  ### Schema Management
43
71
 
44
- - **Git-like CLI** — Familiar commands (`pull`, `push`, `diff`, `status`, `branch`, `merge`)
45
- - **Automatic Migrations** — Generate migrations from schema changes
72
+ - **CLI** — Commands for `pull`, `push`, `diff`, `generate`, `migrate`, `rollback`, `sync`
46
73
  - **Database Introspection** — Generate TypeScript schema from existing databases
47
74
  - **Tracking IDs** — Detect renames and moves, not just additions/deletions
48
75
 
@@ -54,7 +81,12 @@ Relq is a complete, type-safe ORM for PostgreSQL that brings the full power of t
54
81
  - **Triggers & Functions** — Define and track database-side logic
55
82
  - **Full-Text Search** — `tsvector`, `tsquery` with ranking functions
56
83
  - **PostGIS Support** — Geometry and geography types for spatial data
57
- - **AWS DSQL Support** — First-class support for Amazon Aurora DSQL
84
+ - **CTE** — Common Table Expressions for recursive and complex queries
85
+ - **Window Functions** — `OVER`, `PARTITION BY`, `ROW_NUMBER()`, etc.
86
+ - **LISTEN/NOTIFY** — Real-time pub/sub
87
+ - **COPY** — Bulk import/export with `COPY TO` / `COPY FROM`
88
+ - **Query Cache** — Built-in caching with TTL and invalidation strategies
89
+ - **EXPLAIN** — Query analysis and optimization
58
90
 
59
91
  ---
60
92
 
@@ -78,12 +110,10 @@ import {
78
110
  defineTable,
79
111
  uuid, text, timestamp, boolean, integer, jsonb,
80
112
  pgEnum, pgRelations
81
- } from 'relq/schema-builder';
113
+ } from 'relq/pg-builder';
82
114
 
83
- // Enums with full type inference
84
115
  export const userStatus = pgEnum('user_status', ['active', 'inactive', 'suspended']);
85
116
 
86
- // Tables with complete column typing
87
117
  export const users = defineTable('users', {
88
118
  id: uuid().primaryKey().default('gen_random_uuid()'),
89
119
  email: text().notNull().unique(),
@@ -103,7 +133,6 @@ export const posts = defineTable('posts', {
103
133
  createdAt: timestamp('created_at').default('now()'),
104
134
  });
105
135
 
106
- // Define relationships
107
136
  export const relations = pgRelations({
108
137
  users: { posts: { type: 'many', table: 'posts', foreignKey: 'authorId' } },
109
138
  posts: { author: { type: 'one', table: 'users', foreignKey: 'authorId' } }
@@ -118,7 +147,7 @@ export const schema = { users, posts };
118
147
  import { Relq } from 'relq';
119
148
  import { schema, relations } from './db/schema';
120
149
 
121
- const db = new Relq(schema, {
150
+ const db = new Relq(schema, 'postgres', {
122
151
  host: 'localhost',
123
152
  port: 5432,
124
153
  database: 'myapp',
@@ -129,7 +158,7 @@ const db = new Relq(schema, {
129
158
 
130
159
  // Types flow automatically from schema to results
131
160
  const activeUsers = await db.table.users
132
- .select(['id', 'email', 'status'])
161
+ .select('id', 'email', 'status')
133
162
  .where(q => q.equal('status', 'active'))
134
163
  .orderBy('createdAt', 'DESC')
135
164
  .limit(10)
@@ -138,31 +167,73 @@ const activeUsers = await db.table.users
138
167
 
139
168
  // Convenience methods
140
169
  const user = await db.table.users.findById('uuid-here');
141
- const user = await db.table.users.findOne({ email: 'test@example.com' });
170
+ const found = await db.table.users.findOne({ email: 'test@example.com' });
142
171
  ```
143
172
 
144
173
  ---
145
174
 
146
175
  ## Entry Points
147
176
 
148
- Relq provides three entry points for different use cases:
149
-
150
177
  ```typescript
151
- // Runtime - Client, queries, functions
152
- import { Relq, F, Case, PG } from 'relq';
178
+ // Runtime Client, queries, functions
179
+ import { Relq, F, Case, PG, sql, SqlFragment } from 'relq';
180
+
181
+ // Dialect clients (direct usage)
182
+ import { RelqPostgres, RelqNile, RelqDsql, RelqCockroachDB } from 'relq';
153
183
 
154
- // Configuration - CLI and project setup
184
+ // Configuration CLI and project setup
155
185
  import { defineConfig, loadConfig } from 'relq/config';
156
186
 
157
- // Schema Builder - Types, tables, DDL definitions
158
- import {
159
- defineTable,
160
- integer, text, uuid, jsonb, timestamp,
161
- pgEnum, pgDomain, pgComposite, pgTrigger, pgFunction,
162
- pgRelations, one, many
163
- } from 'relq/schema-builder';
187
+ // Schema Builder PostgreSQL (full)
188
+ import { defineTable, uuid, text, pgEnum, pgRelations } from 'relq/pg-builder';
189
+
190
+ // Schema Builder Dialect-specific (compile-time safety)
191
+ import { defineTable } from 'relq/cockroachdb-builder';
192
+ import { defineTable } from 'relq/nile-builder';
193
+ import { defineTable } from 'relq/dsql-builder';
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Multi-Dialect Support
199
+
200
+ Relq supports 4 PostgreSQL-family dialects. Each dialect client only exposes methods it supports, enforced at both compile time and runtime.
201
+
202
+ ```typescript
203
+ // PostgreSQL — full feature set including subscribe()
204
+ const db = new Relq(schema, 'postgres', { host, database, user, password });
205
+
206
+ // CockroachDB — no geometric types, no ranges, no full-text
207
+ const db = new Relq(schema, 'cockroachdb', { host, database, user, password });
208
+
209
+ // Nile — multi-tenant PostgreSQL
210
+ const db = new Relq(schema, 'nile', { host, database, user, password });
211
+ await db.setTenant(tenantId);
212
+
213
+ // AWS DSQL — no subscribe, no triggers, no sequences, no LATERAL
214
+ const db = new Relq(schema, 'awsdsql', { database, aws: { region, hostname } });
215
+ ```
216
+
217
+ Dialect-specific schema builders prevent using unsupported types at compile time:
218
+
219
+ ```typescript
220
+ // Using CockroachDB builder — geometric types won't be available
221
+ import { defineTable, text, uuid, jsonb } from 'relq/cockroachdb-builder';
222
+ // point(), line(), money() — not exported, compile error if used
164
223
  ```
165
224
 
225
+ ### Capability Matrix
226
+
227
+ | Feature | PostgreSQL | CockroachDB | Nile | AWS DSQL |
228
+ |---------|-----------|-------------|------|----------|
229
+ | RETURNING | Yes | Yes | Yes | Yes |
230
+ | LATERAL JOIN | Yes | Yes | Yes | No |
231
+ | DISTINCT ON | Yes | Yes | Yes | No |
232
+ | FOR UPDATE SKIP LOCKED | Yes | Yes | Yes | No |
233
+ | Cursors | Yes | Yes | Yes | No |
234
+ | LISTEN/NOTIFY | Yes | No | No | No |
235
+ | Multi-tenant | No | No | Yes | No |
236
+
166
237
  ---
167
238
 
168
239
  ## Schema Definition
@@ -217,7 +288,6 @@ uuid() // string
217
288
 
218
289
  #### Array Types
219
290
  ```typescript
220
- // Any column type can be an array
221
291
  tags: text().array() // string[]
222
292
  matrix: integer().array(2) // number[][] (2D array)
223
293
  scores: numeric().array() // string[]
@@ -246,7 +316,6 @@ macaddr(), macaddr8() // string
246
316
  int4range(), int8range() // string
247
317
  numrange(), daterange() // string
248
318
  tsrange(), tstzrange() // string
249
- // Multi-range variants also available
250
319
  ```
251
320
 
252
321
  #### Full-Text Search
@@ -281,44 +350,49 @@ semver() // string
281
350
  // All columns
282
351
  const users = await db.table.users.select().all();
283
352
 
284
- // Specific columns
353
+ // Specific columns — array or variadic
285
354
  const emails = await db.table.users.select(['id', 'email']).all();
355
+ const emails = await db.table.users.select('id', 'email').all();
286
356
 
287
- // With conditions
357
+ // With conditions, ordering, limit
288
358
  const active = await db.table.users
289
- .select(['id', 'email', 'name'])
359
+ .select('id', 'email', 'name')
290
360
  .where(q => q.equal('status', 'active'))
291
361
  .orderBy('createdAt', 'DESC')
292
362
  .limit(10)
293
363
  .all();
294
364
 
295
- // Single record
365
+ // Single record (auto LIMIT 1)
296
366
  const user = await db.table.users
297
367
  .select()
298
368
  .where(q => q.equal('id', userId))
299
369
  .get();
300
370
 
301
- // With joins
302
- const postsWithAuthors = await db.table.posts
303
- .select(['id', 'title'])
304
- .join('users', (on, posts, users) => on.equal(posts.authorId, users.id))
371
+ // NULLS FIRST / NULLS LAST ordering
372
+ await db.table.users.select()
373
+ .orderByNulls('deletedAt', 'ASC', 'LAST')
305
374
  .all();
306
375
 
307
376
  // Distinct on (PostgreSQL-specific)
308
- await db.table.logs
309
- .select()
377
+ await db.table.logs.select()
310
378
  .distinctOn('userId')
311
379
  .orderBy('userId')
312
380
  .orderBy('createdAt', 'DESC')
313
381
  .all();
314
382
 
383
+ // Set operations
384
+ const query1 = db.table.users.select('id', 'email').where(q => q.equal('status', 'active'));
385
+ const query2 = db.table.users.select('id', 'email').where(q => q.equal('role', 'admin'));
386
+ await query1.union(query2).all();
387
+ // Also: unionAll, intersect, except
388
+
315
389
  // Row locking
316
- await db.table.jobs
317
- .select()
390
+ await db.table.jobs.select()
318
391
  .where(q => q.equal('status', 'pending'))
319
392
  .forUpdateSkipLocked()
320
393
  .limit(1)
321
394
  .get();
395
+ // Also: forUpdate(), forShare()
322
396
  ```
323
397
 
324
398
  ### INSERT
@@ -327,30 +401,36 @@ await db.table.jobs
327
401
  // Single insert with returning
328
402
  const user = await db.table.users
329
403
  .insert({ email: 'new@example.com', name: 'New User' })
330
- .returning(['id', 'createdAt'])
404
+ .returning('*')
331
405
  .run();
332
406
 
333
- // Bulk insert
407
+ // Multi-row insert
334
408
  await db.table.users
335
- .insert([
336
- { email: 'user1@example.com', name: 'User 1' },
337
- { email: 'user2@example.com', name: 'User 2' }
338
- ])
409
+ .insert({ email: 'user1@example.com', name: 'User 1' })
410
+ .addRow({ email: 'user2@example.com', name: 'User 2' })
339
411
  .run();
340
412
 
341
- // Upsert - ON CONFLICT DO UPDATE
413
+ // ON CONFLICT DO UPDATE (upsert) — with EXCLUDED column access
342
414
  await db.table.users
343
- .insert({ email: 'user@example.com', name: 'Updated' })
344
- .onConflict('email')
345
- .doUpdate({ name: 'Updated', updatedAt: PG.now() })
415
+ .insert({ email: 'user@example.com', name: 'New' })
416
+ .doUpdate('email', {
417
+ name: (excluded, sql) => excluded.name, // Use EXCLUDED value
418
+ score: (excluded, sql, row) => sql.greatest(excluded.score, row.score),
419
+ })
346
420
  .run();
347
421
 
348
422
  // ON CONFLICT DO NOTHING
349
423
  await db.table.users
350
424
  .insert({ email: 'user@example.com', name: 'New' })
351
- .onConflict('email')
352
- .doNothing()
425
+ .doNothing('email')
353
426
  .run();
427
+
428
+ // INSERT FROM SELECT — callback receives schema tables
429
+ const count = await db.table.archivedUsers.insertFrom(
430
+ ['name', 'email', 'createdAt'],
431
+ t => t.users.select('name', 'email', 'createdAt')
432
+ .where(q => q.equal('status', 'inactive'))
433
+ );
354
434
  ```
355
435
 
356
436
  ### UPDATE
@@ -366,13 +446,7 @@ await db.table.users
366
446
  const updated = await db.table.posts
367
447
  .update({ viewCount: F.increment('viewCount', 1) })
368
448
  .where(q => q.equal('id', postId))
369
- .returning(['id', 'viewCount'])
370
- .run();
371
-
372
- // Bulk update
373
- await db.table.posts
374
- .update({ published: true })
375
- .where(q => q.in('id', postIds))
449
+ .returning('*')
376
450
  .run();
377
451
  ```
378
452
 
@@ -402,7 +476,7 @@ const count = await db.table.users
402
476
  .where(q => q.equal('status', 'active'))
403
477
  .get();
404
478
 
405
- // Count with groups
479
+ // Named count groups
406
480
  const counts = await db.table.results.count()
407
481
  .group('all', q => q.equal('isDeleted', false))
408
482
  .group('new', q => q.equal('isRead', false).equal('isDeleted', false))
@@ -411,9 +485,8 @@ const counts = await db.table.results.count()
411
485
  .get();
412
486
  // Returns: { all: number, new: number, favorites: number }
413
487
 
414
- // Multiple aggregations
415
- const stats = await db.table.orders
416
- .aggregate()
488
+ // Multiple aggregation functions
489
+ const stats = await db.table.orders.aggregate()
417
490
  .count('id', 'totalOrders')
418
491
  .sum('amount', 'totalRevenue')
419
492
  .avg('amount', 'avgOrderValue')
@@ -422,28 +495,272 @@ const stats = await db.table.orders
422
495
  .get();
423
496
  ```
424
497
 
425
- ### Pagination
498
+ ---
499
+
500
+ ## Joins
501
+
502
+ ### Type-Safe Joins
503
+
504
+ Join callbacks receive `(on, joined, source)` — the joined table is always the second parameter.
426
505
 
427
506
  ```typescript
428
- // Cursor-based (recommended for large datasets)
429
- const page = await db.table.posts
430
- .select(['id', 'title', 'createdAt'])
431
- .paginate({ orderBy: ['createdAt', 'DESC'] })
432
- .paging({ perPage: 20, cursor: lastCursor });
507
+ // INNER JOIN — auto FK detection (requires relations config)
508
+ const postsWithAuthors = await db.table.posts.select()
509
+ .join('users')
510
+ .all();
511
+ // Result: { ...post, users: { id, name, ... } }[]
433
512
 
434
- // page.data - results
435
- // page.pagination.next - cursor for next page
436
- // page.pagination.hasNext - boolean
513
+ // INNER JOIN — explicit callback
514
+ const postsWithAuthors = await db.table.posts.select('id', 'title')
515
+ .join('users', (on, users, posts) =>
516
+ on.equal(users.id, posts.authorId)
517
+ )
518
+ .all();
437
519
 
438
- // Offset-based
439
- const page = await db.table.posts
440
- .select(['id', 'title'])
441
- .paginate({ orderBy: ['createdAt', 'DESC'] })
442
- .offset({ perPage: 20, page: 2 });
520
+ // LEFT JOIN — joined table may be null
521
+ await db.table.orders.select()
522
+ .leftJoin('users')
523
+ .all();
524
+ // Result: { ...order, users: { ... } | null }[]
443
525
 
444
- // page.pagination.totalPages
445
- // page.pagination.currentPage
446
- // page.pagination.total
526
+ // With alias
527
+ await db.table.orders.select()
528
+ .join(['users', 'customer'], (on, customer, orders) =>
529
+ on.equal(customer.id, orders.userId)
530
+ )
531
+ .all();
532
+ // Result: { ...order, customer: { ... } }[]
533
+
534
+ // RIGHT JOIN
535
+ await db.table.orders.select()
536
+ .rightJoin('users', (on, orders, users) =>
537
+ on.equal(orders.userId, users.id)
538
+ )
539
+ .all();
540
+ ```
541
+
542
+ ### One-to-Many Joins (LATERAL)
543
+
544
+ ```typescript
545
+ // Get top 5 orders per user
546
+ await db.table.users.select()
547
+ .joinMany('orders', (on, orders, users) =>
548
+ on.equal(orders.userId, users.id)
549
+ .orderBy('createdAt', 'DESC')
550
+ .limit(5)
551
+ )
552
+ .all();
553
+ // Result: { ...user, orders: Order[] }[]
554
+
555
+ // LEFT JOIN — empty array if no matches
556
+ await db.table.users.select()
557
+ .leftJoinMany('orders', (on, orders, users) =>
558
+ on.equal(orders.userId, users.id)
559
+ )
560
+ .all();
561
+
562
+ // Many-to-many through junction table
563
+ await db.table.posts.select()
564
+ .leftJoinMany('labels', { through: 'itemLabels' }, on =>
565
+ on.select('id', 'name', 'color')
566
+ )
567
+ .all();
568
+
569
+ // Subquery join
570
+ await db.table.users.select()
571
+ .joinSubquery(
572
+ 'stats',
573
+ 'SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id',
574
+ '"stats"."user_id" = "users"."id"'
575
+ )
576
+ .all();
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Relation Loading
582
+
583
+ Sugar methods for common join patterns. No callback needed — FK is auto-detected from `relations` config.
584
+
585
+ ### `.with()` — Many-to-One / One-to-One
586
+
587
+ ```typescript
588
+ // Load the user for each order
589
+ await db.table.orders.select('id', 'total')
590
+ .with('users')
591
+ .all();
592
+ // Result: { id, total, users: { id, name, ... } | null }[]
593
+
594
+ // Chain multiple relations
595
+ await db.table.orders.select('id')
596
+ .with('users')
597
+ .with('products')
598
+ .all();
599
+ ```
600
+
601
+ ### `.withMany()` — One-to-Many
602
+
603
+ ```typescript
604
+ // Load all orders for each user
605
+ await db.table.users.select('id', 'name')
606
+ .withMany('orders')
607
+ .all();
608
+ // Result: { id, name, orders: { id, total, ... }[] }[]
609
+
610
+ // Many-to-many through junction table
611
+ await db.table.users.select('id', 'name')
612
+ .withMany('tags', { through: 'userTags' })
613
+ .all();
614
+ // Result: { id, name, tags: { id, label, ... }[] }[]
615
+ ```
616
+
617
+ ---
618
+
619
+ ## Computed Columns
620
+
621
+ Add aggregate functions and expressions to query results with `.include()`.
622
+
623
+ ```typescript
624
+ // Count items per folder
625
+ const folders = await db.table.folders.select('id', 'name')
626
+ .include((a, t) => ({
627
+ itemCount: a.count(t.items.id),
628
+ }))
629
+ .leftJoin('items')
630
+ .groupBy('id')
631
+ .all();
632
+ // Result: { id: string, name: string, itemCount: number }[]
633
+
634
+ // Multiple aggregates with table references
635
+ const users = await db.table.users.select('id', 'name')
636
+ .include((a, t) => ({
637
+ orderCount: a.count(t.orders.id),
638
+ totalSpent: a.sum(t.orders.amount),
639
+ lastOrder: a.max(t.orders.createdAt),
640
+ }))
641
+ .leftJoin('orders')
642
+ .groupBy('id')
643
+ .all();
644
+
645
+ // Raw SQL expressions
646
+ db.table.users.select('id')
647
+ .include((a) => ({
648
+ year: a.raw<number>('EXTRACT(YEAR FROM NOW())'),
649
+ }))
650
+ .all();
651
+ ```
652
+
653
+ The callback receives `(a, t)` where `a` provides aggregate functions (`count`, `sum`, `avg`, `min`, `max`, `raw`) and `t` provides type-safe table proxies for referencing columns across joined tables.
654
+
655
+ ---
656
+
657
+ ## Convenience Methods
658
+
659
+ Quick operations without building full queries.
660
+
661
+ ```typescript
662
+ // Find by primary key (auto-detects PK column from schema)
663
+ const user = await db.table.users.findById('uuid-here');
664
+
665
+ // Find first matching record
666
+ const user = await db.table.users.findOne({ email: 'test@example.com' });
667
+
668
+ // Find all matching records
669
+ const admins = await db.table.users.findMany({ role: 'admin' });
670
+
671
+ // Check existence
672
+ const exists = await db.table.users.exists({ email: 'test@example.com' });
673
+
674
+ // Upsert — insert or update
675
+ const user = await db.table.users.upsert({
676
+ where: { email: 'user@example.com' },
677
+ create: { email: 'user@example.com', name: 'New User' },
678
+ update: { name: 'Updated Name' },
679
+ });
680
+
681
+ // Bulk insert with RETURNING *
682
+ const created = await db.table.users.insertMany([
683
+ { email: 'a@example.com', name: 'A' },
684
+ { email: 'b@example.com', name: 'B' },
685
+ ]);
686
+
687
+ // INSERT FROM SELECT
688
+ const count = await db.table.archivedUsers.insertFrom(
689
+ ['name', 'email', 'createdAt'],
690
+ t => t.users.select('name', 'email', 'createdAt')
691
+ .where(q => q.equal('status', 'inactive'))
692
+ );
693
+
694
+ // Nested create — parent + children in one transaction
695
+ const user = await db.table.users.createWith({
696
+ data: { name: 'John', email: 'john@example.com' },
697
+ with: {
698
+ posts: [
699
+ { title: 'Hello', content: 'World' },
700
+ { title: 'Second', content: 'Post' },
701
+ ],
702
+ profile: { bio: 'Developer' },
703
+ }
704
+ });
705
+ // Inserts: user -> posts with userId set -> profile with userId set
706
+
707
+ // Soft delete (sets deleted_at to now)
708
+ await db.table.users.softDelete({ id: userId });
709
+
710
+ // Restore soft-deleted record (clears deleted_at)
711
+ await db.table.users.restore({ id: userId });
712
+ ```
713
+
714
+ ---
715
+
716
+ ## Raw SQL
717
+
718
+ ### Tagged Template Literal
719
+
720
+ The `sql` tagged template auto-escapes interpolated values to prevent SQL injection.
721
+
722
+ ```typescript
723
+ import { sql } from 'relq';
724
+
725
+ // Values are auto-escaped
726
+ const query = sql`SELECT * FROM users WHERE id = ${userId}`;
727
+
728
+ // Multiple parameters
729
+ const query = sql`
730
+ SELECT * FROM orders
731
+ WHERE user_id = ${userId}
732
+ AND status = ${status}
733
+ AND total > ${minTotal}
734
+ `;
735
+
736
+ // Identifiers (table/column names)
737
+ const query = sql`SELECT * FROM ${sql.id('users')} WHERE ${sql.id('email')} = ${email}`;
738
+
739
+ // Raw SQL (no escaping — use only with trusted input)
740
+ const query = sql`SELECT * FROM users ${sql.raw('ORDER BY id DESC')}`;
741
+
742
+ // Composable fragments
743
+ const condition = sql`status = ${status}`;
744
+ const query = sql`SELECT * FROM users WHERE ${condition}`;
745
+ ```
746
+
747
+ **Type handling:**
748
+ - Strings -> single-quoted and escaped
749
+ - Numbers -> literal
750
+ - Booleans -> `true` / `false`
751
+ - `null` / `undefined` -> `NULL`
752
+ - Dates -> ISO string, single-quoted
753
+ - Arrays -> parenthesized list `(val1, val2)`
754
+ - Objects -> JSON string with `::jsonb` cast
755
+ - `SqlFragment` -> inlined as-is (for composition)
756
+
757
+ ### Raw Query Builder
758
+
759
+ ```typescript
760
+ import { RawQueryBuilder } from 'relq';
761
+
762
+ const builder = new RawQueryBuilder('SELECT * FROM users WHERE status = %L', 'active');
763
+ const sql = builder.toString();
447
764
  ```
448
765
 
449
766
  ---
@@ -540,7 +857,7 @@ PG.false() // FALSE
540
857
 
541
858
  ### Logical Operators
542
859
  ```typescript
543
- // AND (default - conditions chain)
860
+ // AND (default conditions chain)
544
861
  .where(q => q
545
862
  .equal('status', 'active')
546
863
  .greaterThan('age', 18)
@@ -594,12 +911,97 @@ PG.false() // FALSE
594
911
 
595
912
  ---
596
913
 
914
+ ## Pagination
915
+
916
+ ### Paginate Builder
917
+
918
+ ```typescript
919
+ // Cursor-based (recommended for large datasets)
920
+ const page = await db.table.posts
921
+ .paginate({ orderBy: ['createdAt', 'DESC'] })
922
+ .paging({ perPage: 20, cursor: lastCursor });
923
+
924
+ // page.data — results
925
+ // page.pagination.next — cursor for next page
926
+ // page.pagination.hasNext
927
+
928
+ // Offset-based
929
+ const page = await db.table.posts
930
+ .paginate({ orderBy: ['createdAt', 'DESC'] })
931
+ .offset({ perPage: 20, page: 2 });
932
+
933
+ // page.pagination.totalPages
934
+ // page.pagination.currentPage
935
+ // page.pagination.total
936
+ ```
937
+
938
+ ### Cursor Iteration
939
+
940
+ Process large result sets row by row using PostgreSQL cursors. Memory-efficient — rows are fetched in batches.
941
+
942
+ ```typescript
943
+ await db.table.users.select('email')
944
+ .where(q => q.equal('verified', false))
945
+ .each(async (row) => {
946
+ await sendVerificationEmail(row.email);
947
+ });
948
+
949
+ // Stop early
950
+ await db.table.logs.select()
951
+ .each(async (row, index) => {
952
+ if (index >= 1000) return false;
953
+ processLog(row);
954
+ }, { batchSize: 50 });
955
+ ```
956
+
957
+ ---
958
+
959
+ ## Transactions
960
+
961
+ ```typescript
962
+ // Basic transaction
963
+ const result = await db.transaction(async (tx) => {
964
+ const user = await tx.table.users
965
+ .insert({ email: 'new@example.com', name: 'User' })
966
+ .returning(['id'])
967
+ .run();
968
+
969
+ await tx.table.posts
970
+ .insert({ title: 'First Post', authorId: user.id })
971
+ .run();
972
+
973
+ return user;
974
+ });
975
+
976
+ // With savepoints
977
+ await db.transaction(async (tx) => {
978
+ await tx.table.users.insert({ ... }).run();
979
+
980
+ try {
981
+ await tx.savepoint('optional', async (sp) => {
982
+ await sp.table.posts.insert({ ... }).run();
983
+ });
984
+ } catch (e) {
985
+ // Savepoint rolled back, transaction continues
986
+ }
987
+
988
+ await tx.table.logs.insert({ ... }).run();
989
+ });
990
+
991
+ // With isolation level
992
+ await db.transaction({ isolation: 'SERIALIZABLE' }, async (tx) => {
993
+ // ...
994
+ });
995
+ ```
996
+
997
+ ---
998
+
597
999
  ## Advanced Schema Features
598
1000
 
599
1001
  ### Domains with Validation
600
1002
 
601
1003
  ```typescript
602
- import { pgDomain, text, numeric } from 'relq/schema-builder';
1004
+ import { pgDomain, text, numeric } from 'relq/pg-builder';
603
1005
 
604
1006
  export const emailDomain = pgDomain('email', text(), (value) => [
605
1007
  value.matches('^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$')
@@ -609,17 +1011,12 @@ export const percentageDomain = pgDomain('percentage',
609
1011
  numeric().precision(5).scale(2),
610
1012
  (value) => [value.gte(0), value.lte(100)]
611
1013
  );
612
-
613
- const employees = defineTable('employees', {
614
- email: emailDomain().notNull(),
615
- bonus: percentageDomain().default(0)
616
- });
617
1014
  ```
618
1015
 
619
1016
  ### Composite Types
620
1017
 
621
1018
  ```typescript
622
- import { pgComposite, text, varchar, boolean } from 'relq/schema-builder';
1019
+ import { pgComposite, text, varchar, boolean } from 'relq/pg-builder';
623
1020
 
624
1021
  export const addressType = pgComposite('address_type', {
625
1022
  line1: text().notNull(),
@@ -629,11 +1026,6 @@ export const addressType = pgComposite('address_type', {
629
1026
  postalCode: varchar().length(20),
630
1027
  verified: boolean().default(false)
631
1028
  });
632
-
633
- const customers = defineTable('customers', {
634
- billingAddress: addressType(),
635
- shippingAddress: addressType()
636
- });
637
1029
  ```
638
1030
 
639
1031
  ### Generated Columns
@@ -643,15 +1035,11 @@ const orderItems = defineTable('order_items', {
643
1035
  quantity: integer().notNull(),
644
1036
  unitPrice: numeric().precision(10).scale(2).notNull(),
645
1037
  discount: numeric().precision(5).scale(2).default(0),
646
-
647
- // Computed from other columns
648
1038
  lineTotal: numeric().precision(12).scale(2).generatedAlwaysAs(
649
1039
  (table, F) => F(table.unitPrice)
650
1040
  .multiply(table.quantity)
651
1041
  .multiply(F.subtract(1, F.divide(table.discount, 100)))
652
1042
  ),
653
-
654
- // Full-text search vector
655
1043
  searchVector: tsvector().generatedAlwaysAs(
656
1044
  (table, F) => F.toTsvector('english', table.description)
657
1045
  )
@@ -664,35 +1052,29 @@ const orderItems = defineTable('order_items', {
664
1052
  // Range partitioning
665
1053
  const events = defineTable('events', {
666
1054
  id: uuid().primaryKey(),
667
- eventType: text().notNull(),
668
1055
  createdAt: timestamp('created_at').notNull()
669
1056
  }, {
670
1057
  partitionBy: (table, p) => p.range(table.createdAt),
671
1058
  partitions: (partition) => [
672
1059
  partition('events_2024_q1').from('2024-01-01').to('2024-04-01'),
673
1060
  partition('events_2024_q2').from('2024-04-01').to('2024-07-01'),
674
- partition('events_2024_q3').from('2024-07-01').to('2024-10-01'),
675
- partition('events_2024_q4').from('2024-10-01').to('2025-01-01'),
676
1061
  ]
677
1062
  });
678
1063
 
679
1064
  // List partitioning
680
1065
  const logs = defineTable('logs', {
681
- id: uuid().primaryKey(),
682
1066
  level: text().notNull(),
683
1067
  message: text()
684
1068
  }, {
685
1069
  partitionBy: (table, p) => p.list(table.level),
686
1070
  partitions: (partition) => [
687
1071
  partition('logs_error').forValues('error', 'fatal'),
688
- partition('logs_warn').forValues('warn'),
689
1072
  partition('logs_info').forValues('info', 'debug')
690
1073
  ]
691
1074
  });
692
1075
 
693
1076
  // Hash partitioning
694
1077
  const sessions = defineTable('sessions', {
695
- id: uuid().primaryKey(),
696
1078
  userId: uuid('user_id').notNull()
697
1079
  }, {
698
1080
  partitionBy: (table, p) => p.hash(table.userId),
@@ -708,9 +1090,8 @@ const sessions = defineTable('sessions', {
708
1090
  ### Triggers and Functions
709
1091
 
710
1092
  ```typescript
711
- import { pgTrigger, pgFunction } from 'relq/schema-builder';
1093
+ import { pgTrigger, pgFunction } from 'relq/pg-builder';
712
1094
 
713
- // Define a function
714
1095
  export const updateUpdatedAt = pgFunction('update_updated_at_column', {
715
1096
  returns: 'trigger',
716
1097
  language: 'plpgsql',
@@ -723,7 +1104,6 @@ export const updateUpdatedAt = pgFunction('update_updated_at_column', {
723
1104
  volatility: 'VOLATILE',
724
1105
  }).$id('func123');
725
1106
 
726
- // Define a trigger using the function
727
1107
  export const usersUpdatedAt = pgTrigger('users_updated_at', {
728
1108
  on: schema.users,
729
1109
  before: 'UPDATE',
@@ -736,37 +1116,22 @@ export const usersUpdatedAt = pgTrigger('users_updated_at', {
736
1116
 
737
1117
  ```typescript
738
1118
  const posts = defineTable('posts', {
739
- id: uuid().primaryKey(),
740
1119
  title: text().notNull(),
741
1120
  authorId: uuid('author_id').notNull(),
742
1121
  tags: text().array(),
743
1122
  metadata: jsonb(),
744
1123
  published: boolean().default(false),
745
- createdAt: timestamp('created_at').default('now()')
746
1124
  }, {
747
1125
  indexes: (table, index) => [
748
- // B-tree (default)
749
1126
  index('posts_author_idx').on(table.authorId),
750
-
751
- // Composite with ordering
752
1127
  index('posts_author_created_idx')
753
1128
  .on(table.authorId, table.createdAt.desc()),
754
-
755
- // Partial index
756
1129
  index('posts_published_idx')
757
1130
  .on(table.createdAt)
758
1131
  .where(table.published.eq(true)),
759
-
760
- // GIN for arrays
761
1132
  index('posts_tags_idx').on(table.tags).using('gin'),
762
-
763
- // GIN for JSONB
764
1133
  index('posts_metadata_idx').on(table.metadata).using('gin'),
765
-
766
- // Unique
767
1134
  index('posts_slug_idx').on(table.slug).unique(),
768
-
769
- // Expression index
770
1135
  index('posts_title_lower_idx')
771
1136
  .on(F => F.lower(table.title))
772
1137
  ]
@@ -775,70 +1140,59 @@ const posts = defineTable('posts', {
775
1140
 
776
1141
  ---
777
1142
 
778
- ## CLI Commands
779
-
780
- Relq provides a comprehensive git-like CLI for schema management:
781
-
782
- ### Initialization & Status
783
-
784
- ```bash
785
- relq init # Initialize a new Relq project
786
- relq status # Show current schema status and pending changes
787
- ```
788
-
789
- ### Schema Operations
790
-
791
- ```bash
792
- relq pull [--force] # Pull schema from database
793
- relq push [--dry-run] # Push schema changes to database
794
- relq diff [--sql] # Show differences between local and remote schema
795
- relq sync # Full bidirectional sync
796
- relq introspect # Generate TypeScript schema from existing database
797
- ```
798
-
799
- ### Change Management
800
-
801
- ```bash
802
- relq add [files...] # Stage schema changes
803
- relq commit -m "message" # Commit staged changes
804
- relq reset [--hard] # Unstage or reset changes
805
- ```
806
-
807
- ### Migration Commands
808
-
809
- ```bash
810
- relq generate -m "message" # Generate migration from changes
811
- relq migrate [--up|--down] # Run migrations
812
- relq rollback [n] # Rollback n migrations
813
- relq log # View migration log
814
- relq history # View full migration history
815
- ```
1143
+ ## DDL Builders
816
1144
 
817
- ### Branching & Merging
1145
+ Relq includes builders for all PostgreSQL DDL operations:
818
1146
 
819
- ```bash
820
- relq branch [name] # List or create branches
821
- relq checkout <branch> # Switch to a branch
822
- relq merge <branch> # Merge a branch into current
823
- relq cherry-pick <commit> # Apply specific commit
824
- relq stash [pop|list|drop] # Stash/unstash changes
1147
+ ```typescript
1148
+ import {
1149
+ // Tables
1150
+ CreateTableBuilder, AlterTableBuilder, TruncateBuilder,
1151
+ // Indexes
1152
+ CreateIndexBuilder, DropIndexBuilder, ReindexBuilder,
1153
+ // Views
1154
+ CreateViewBuilder, DropViewBuilder, RefreshMaterializedViewBuilder,
1155
+ // Functions & Triggers
1156
+ CreateFunctionBuilder, DropFunctionBuilder,
1157
+ CreateTriggerBuilder, DropTriggerBuilder,
1158
+ // Schemas & Roles
1159
+ CreateSchemaBuilder, DropSchemaBuilder,
1160
+ GrantBuilder, RevokeBuilder, DefaultPrivilegesBuilder,
1161
+ CreateRoleBuilder, AlterRoleBuilder, DropRoleBuilder,
1162
+ // Sequences
1163
+ CreateSequenceBuilder, AlterSequenceBuilder, DropSequenceBuilder,
1164
+ // Partitions
1165
+ PartitionBuilder, CreatePartitionBuilder,
1166
+ AttachPartitionBuilder, DetachPartitionBuilder,
1167
+ // CTE, Window, COPY
1168
+ CTEBuilder, WindowBuilder,
1169
+ CopyToBuilder, CopyFromBuilder,
1170
+ // EXPLAIN, Maintenance
1171
+ ExplainBuilder, VacuumBuilder, AnalyzeBuilder,
1172
+ // Pub/Sub
1173
+ ListenBuilder, UnlistenBuilder, NotifyBuilder,
1174
+ } from 'relq';
825
1175
  ```
826
1176
 
827
- ### Remote Operations
828
-
829
- ```bash
830
- relq remote [add|remove] # Manage remote databases
831
- relq fetch # Fetch remote schema without applying
832
- relq tag <name> # Tag current schema version
833
- ```
1177
+ ---
834
1178
 
835
- ### Utilities
1179
+ ## CLI Commands
836
1180
 
837
1181
  ```bash
838
- relq validate # Validate schema definitions
839
- relq export [--format=sql] # Export schema as SQL
840
- relq import <file> # Import schema from file
841
- relq resolve # Resolve merge conflicts
1182
+ relq init # Initialize a new Relq project
1183
+ relq status # Show current schema state
1184
+ relq diff [--sql] # Show schema differences
1185
+ relq pull [--force] # Pull schema from database
1186
+ relq push [--dry-run] # Push schema changes to database
1187
+ relq generate -m "msg" # Generate migration from changes
1188
+ relq migrate # Apply pending migrations
1189
+ relq rollback [n] # Rollback n migrations
1190
+ relq sync # Pull + Push in one command
1191
+ relq import <file> # Import SQL file to schema
1192
+ relq export # Export schema to SQL file
1193
+ relq validate # Check schema for errors
1194
+ relq seed # Seed database from SQL files
1195
+ relq introspect # Parse SQL DDL to defineTable() code
842
1196
  ```
843
1197
 
844
1198
  ---
@@ -863,7 +1217,7 @@ export default defineConfig({
863
1217
  migrations: {
864
1218
  directory: './db/migrations',
865
1219
  tableName: '_relq_migrations',
866
- format: 'timestamp' // or 'sequential'
1220
+ format: 'timestamp'
867
1221
  },
868
1222
  generate: {
869
1223
  outDir: './db/generated',
@@ -876,71 +1230,15 @@ export default defineConfig({
876
1230
  });
877
1231
  ```
878
1232
 
879
- ### Environment-Specific Configuration
1233
+ ### AWS DSQL
880
1234
 
881
1235
  ```typescript
882
- export default defineConfig({
883
- connection: process.env.NODE_ENV === 'production'
884
- ? { url: process.env.DATABASE_URL }
885
- : {
886
- host: 'localhost',
887
- database: 'myapp_dev',
888
- user: 'postgres',
889
- password: 'dev'
890
- }
891
- });
892
- ```
893
-
894
- ### AWS DSQL Support
895
-
896
- ```typescript
897
- import { Relq } from 'relq';
898
-
899
- const db = new Relq(schema, {
900
- provider: 'aws-dsql',
901
- region: 'us-east-1',
902
- hostname: 'your-cluster.dsql.us-east-1.on.aws',
903
- // Uses AWS credentials from environment
904
- });
905
- ```
906
-
907
- ---
908
-
909
- ## Transactions
910
-
911
- ```typescript
912
- // Basic transaction
913
- const result = await db.transaction(async (tx) => {
914
- const user = await tx.table.users
915
- .insert({ email: 'new@example.com', name: 'User' })
916
- .returning(['id'])
917
- .run();
918
-
919
- await tx.table.posts
920
- .insert({ title: 'First Post', authorId: user.id })
921
- .run();
922
-
923
- return user;
924
- });
925
-
926
- // With savepoints
927
- await db.transaction(async (tx) => {
928
- await tx.table.users.insert({ ... }).run();
929
-
930
- try {
931
- await tx.savepoint('optional', async (sp) => {
932
- await sp.table.posts.insert({ ... }).run();
933
- });
934
- } catch (e) {
935
- // Savepoint rolled back, transaction continues
1236
+ const db = new Relq(schema, 'awsdsql', {
1237
+ database: 'postgres',
1238
+ aws: {
1239
+ region: 'us-east-1',
1240
+ hostname: 'your-cluster.dsql.us-east-1.on.aws',
936
1241
  }
937
-
938
- await tx.table.logs.insert({ ... }).run();
939
- });
940
-
941
- // With isolation level
942
- await db.transaction({ isolation: 'SERIALIZABLE' }, async (tx) => {
943
- // ...
944
1242
  });
945
1243
  ```
946
1244
 
@@ -953,6 +1251,11 @@ import {
953
1251
  RelqError,
954
1252
  RelqConnectionError,
955
1253
  RelqQueryError,
1254
+ RelqTransactionError,
1255
+ RelqConfigError,
1256
+ RelqTimeoutError,
1257
+ RelqPoolError,
1258
+ RelqBuilderError,
956
1259
  isRelqError
957
1260
  } from 'relq';
958
1261
 
@@ -965,7 +1268,6 @@ try {
965
1268
  } else if (error instanceof RelqQueryError) {
966
1269
  console.error('Query failed:', error.message);
967
1270
  console.error('SQL:', error.sql);
968
- console.error('Parameters:', error.parameters);
969
1271
  }
970
1272
  }
971
1273
  }