relq 1.0.95 → 1.0.97
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 +541 -239
- package/dist/cjs/cli/adapters/cockroachdb/index.cjs +1 -1
- package/dist/cjs/cli/adapters/cockroachdb/introspect.cjs +118 -0
- package/dist/cjs/cli/adapters/dsql/index.cjs +1 -1
- package/dist/cjs/cli/adapters/dsql/introspect.cjs +125 -0
- package/dist/cjs/cli/adapters/mysql/index.cjs +1 -1
- package/dist/cjs/cli/adapters/nile/index.cjs +1 -1
- package/dist/cjs/cli/adapters/nile/introspect.cjs +115 -0
- package/dist/cjs/cli/adapters/postgres/index.cjs +1 -1
- package/dist/cjs/cli/adapters/postgres/introspect.cjs +115 -0
- package/dist/cjs/cli/commands/generate.cjs +127 -18
- package/dist/cjs/cli/commands/push.cjs +124 -19
- package/dist/cjs/cli/utils/ast-transformer.cjs +19 -0
- package/dist/cjs/cli/utils/cockroachdb/introspect.cjs +115 -3
- package/dist/cjs/cli/utils/dsql/introspect.cjs +122 -3
- package/dist/cjs/cli/utils/migration-generator.cjs +68 -1
- package/dist/cjs/cli/utils/nile/introspect.cjs +112 -3
- package/dist/cjs/cli/utils/postgres/introspect.cjs +112 -3
- package/dist/cjs/cli/utils/schema-loader.cjs +43 -5
- package/dist/cjs/core/helpers/ConnectedQueryBuilder.cjs +9 -0
- package/dist/cjs/core/helpers/ConnectedSelectBuilder.cjs +31 -0
- package/dist/cjs/core/helpers/query-convenience.cjs +125 -0
- package/dist/cjs/index.cjs +4 -2
- package/dist/cjs/insert/insert-from-select-builder.cjs +48 -0
- package/dist/cjs/raw/index.cjs +4 -1
- package/dist/cjs/raw/sql-template.cjs +73 -0
- package/dist/esm/cli/adapters/cockroachdb/index.js +1 -1
- package/dist/esm/cli/adapters/cockroachdb/introspect.js +118 -0
- package/dist/esm/cli/adapters/dsql/index.js +1 -1
- package/dist/esm/cli/adapters/dsql/introspect.js +125 -0
- package/dist/esm/cli/adapters/mysql/index.js +1 -1
- package/dist/esm/cli/adapters/nile/index.js +1 -1
- package/dist/esm/cli/adapters/nile/introspect.js +115 -0
- package/dist/esm/cli/adapters/postgres/index.js +1 -1
- package/dist/esm/cli/adapters/postgres/introspect.js +115 -0
- package/dist/esm/cli/commands/generate.js +129 -20
- package/dist/esm/cli/commands/push.js +126 -21
- package/dist/esm/cli/utils/ast-transformer.js +19 -0
- package/dist/esm/cli/utils/cockroachdb/introspect.js +115 -3
- package/dist/esm/cli/utils/dsql/introspect.js +122 -3
- package/dist/esm/cli/utils/migration-generator.js +67 -1
- package/dist/esm/cli/utils/nile/introspect.js +112 -3
- package/dist/esm/cli/utils/postgres/introspect.js +112 -3
- package/dist/esm/cli/utils/schema-loader.js +43 -5
- package/dist/esm/core/helpers/ConnectedQueryBuilder.js +10 -1
- package/dist/esm/core/helpers/ConnectedSelectBuilder.js +31 -0
- package/dist/esm/core/helpers/query-convenience.js +90 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/insert/insert-from-select-builder.js +41 -0
- package/dist/esm/raw/index.js +1 -0
- package/dist/esm/raw/sql-template.js +66 -0
- package/dist/index.d.ts +186 -0
- package/package.json +1 -1
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
|
|
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
|
[](https://www.npmjs.com/package/relq)
|
|
8
8
|
[](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
|
-
- **
|
|
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
|
-
- **
|
|
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/
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
184
|
+
// Configuration — CLI and project setup
|
|
155
185
|
import { defineConfig, loadConfig } from 'relq/config';
|
|
156
186
|
|
|
157
|
-
// Schema Builder
|
|
158
|
-
import {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} from 'relq/
|
|
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(
|
|
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
|
-
//
|
|
302
|
-
|
|
303
|
-
.
|
|
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(
|
|
404
|
+
.returning('*')
|
|
331
405
|
.run();
|
|
332
406
|
|
|
333
|
-
//
|
|
407
|
+
// Multi-row insert
|
|
334
408
|
await db.table.users
|
|
335
|
-
.insert(
|
|
336
|
-
|
|
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
|
-
//
|
|
413
|
+
// ON CONFLICT DO UPDATE (upsert) — with EXCLUDED column access
|
|
342
414
|
await db.table.users
|
|
343
|
-
.insert({ email: 'user@example.com', name: '
|
|
344
|
-
.
|
|
345
|
-
|
|
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
|
-
.
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
429
|
-
const
|
|
430
|
-
.
|
|
431
|
-
.
|
|
432
|
-
|
|
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
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
439
|
-
|
|
440
|
-
.
|
|
441
|
-
.
|
|
442
|
-
|
|
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
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
##
|
|
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
|
-
|
|
1145
|
+
Relq includes builders for all PostgreSQL DDL operations:
|
|
818
1146
|
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1179
|
+
## CLI Commands
|
|
836
1180
|
|
|
837
1181
|
```bash
|
|
838
|
-
relq
|
|
839
|
-
relq
|
|
840
|
-
relq
|
|
841
|
-
relq
|
|
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'
|
|
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
|
-
###
|
|
1233
|
+
### AWS DSQL
|
|
880
1234
|
|
|
881
1235
|
```typescript
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
:
|
|
886
|
-
|
|
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
|
}
|