relq 1.0.49 → 1.0.51
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 +386 -249
- package/dist/cjs/cli/commands/pull.cjs +31 -2
- package/dist/cjs/cli/utils/ast-codegen.cjs +6 -5
- package/dist/cjs/cli/utils/change-tracker.cjs +5 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +18 -13
- package/dist/esm/cli/commands/pull.js +32 -3
- package/dist/esm/cli/utils/ast-codegen.js +6 -5
- package/dist/esm/cli/utils/change-tracker.js +5 -0
- package/dist/esm/cli/utils/schema-comparator.js +18 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,41 +2,72 @@
|
|
|
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, and a git-like CLI
|
|
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.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/relq)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://www.postgresql.org/)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- **Zero Runtime Dependencies** - Everything bundled, no external packages at runtime
|
|
11
|
-
- **Full PostgreSQL Support** - Every PostgreSQL feature you need, properly typed
|
|
12
|
-
- **Tree-Shakeable** - Import only what you use
|
|
13
|
-
- **Schema-First** - Define once, get types everywhere
|
|
14
|
-
- **Git-like CLI** - Familiar commands for schema management
|
|
12
|
+
---
|
|
15
13
|
|
|
16
|
-
##
|
|
14
|
+
## Table of Contents
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [Entry Points](#entry-points)
|
|
20
|
+
- [Schema Definition](#schema-definition)
|
|
21
|
+
- [Query API](#query-api)
|
|
22
|
+
- [SQL Functions](#sql-functions)
|
|
23
|
+
- [Condition Builders](#condition-builders)
|
|
24
|
+
- [Advanced Schema Features](#advanced-schema-features)
|
|
25
|
+
- [CLI Commands](#cli-commands)
|
|
26
|
+
- [Configuration](#configuration)
|
|
27
|
+
- [Error Handling](#error-handling)
|
|
28
|
+
- [Requirements](#requirements)
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
---
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
// Runtime - Client, queries, functions
|
|
26
|
-
import { Relq, F, Case, PG } from 'relq';
|
|
32
|
+
## Features
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
import { defineConfig, loadConfig } from 'relq/config';
|
|
34
|
+
### Core Capabilities
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
- **Complete Type Safety** — End-to-end TypeScript inference from schema definition to query results
|
|
37
|
+
- **Zero Runtime Dependencies** — Everything bundled, no external packages needed at runtime
|
|
38
|
+
- **Full PostgreSQL Support** — 100+ column types, all properly typed
|
|
39
|
+
- **Tree-Shakeable** — Import only what you use for optimal bundle size
|
|
40
|
+
- **Schema-First Design** — Define once, get types everywhere
|
|
41
|
+
|
|
42
|
+
### Schema Management
|
|
43
|
+
|
|
44
|
+
- **Git-like CLI** — Familiar commands (`pull`, `push`, `diff`, `status`, `branch`, `merge`)
|
|
45
|
+
- **Automatic Migrations** — Generate migrations from schema changes
|
|
46
|
+
- **Database Introspection** — Generate TypeScript schema from existing databases
|
|
47
|
+
- **Tracking IDs** — Detect renames and moves, not just additions/deletions
|
|
48
|
+
|
|
49
|
+
### Advanced Features
|
|
50
|
+
|
|
51
|
+
- **Table Partitioning** — Range, list, and hash partitioning with typed definitions
|
|
52
|
+
- **Generated Columns** — Computed columns with expression builders
|
|
53
|
+
- **Domains & Composites** — Custom types with validation
|
|
54
|
+
- **Triggers & Functions** — Define and track database-side logic
|
|
55
|
+
- **Full-Text Search** — `tsvector`, `tsquery` with ranking functions
|
|
56
|
+
- **PostGIS Support** — Geometry and geography types for spatial data
|
|
57
|
+
- **AWS DSQL Support** — First-class support for Amazon Aurora DSQL
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install relq
|
|
65
|
+
# or
|
|
66
|
+
bun add relq
|
|
38
67
|
```
|
|
39
68
|
|
|
69
|
+
---
|
|
70
|
+
|
|
40
71
|
## Quick Start
|
|
41
72
|
|
|
42
73
|
### 1. Define Your Schema
|
|
@@ -46,7 +77,7 @@ import {
|
|
|
46
77
|
import {
|
|
47
78
|
defineTable,
|
|
48
79
|
uuid, text, timestamp, boolean, integer, jsonb,
|
|
49
|
-
pgEnum
|
|
80
|
+
pgEnum, pgRelations
|
|
50
81
|
} from 'relq/schema-builder';
|
|
51
82
|
|
|
52
83
|
// Enums with full type inference
|
|
@@ -72,35 +103,36 @@ export const posts = defineTable('posts', {
|
|
|
72
103
|
createdAt: timestamp('created_at').default('now()'),
|
|
73
104
|
});
|
|
74
105
|
|
|
106
|
+
// Define relationships
|
|
107
|
+
export const relations = pgRelations({
|
|
108
|
+
users: { posts: { type: 'many', table: 'posts', foreignKey: 'authorId' } },
|
|
109
|
+
posts: { author: { type: 'one', table: 'users', foreignKey: 'authorId' } }
|
|
110
|
+
});
|
|
111
|
+
|
|
75
112
|
export const schema = { users, posts };
|
|
76
113
|
```
|
|
77
114
|
|
|
78
|
-
### 2. Connect
|
|
115
|
+
### 2. Connect and Query
|
|
79
116
|
|
|
80
117
|
```typescript
|
|
81
118
|
import { Relq } from 'relq';
|
|
82
|
-
import { schema } from './schema';
|
|
119
|
+
import { schema, relations } from './db/schema';
|
|
83
120
|
|
|
84
121
|
const db = new Relq(schema, {
|
|
85
122
|
host: 'localhost',
|
|
123
|
+
port: 5432,
|
|
86
124
|
database: 'myapp',
|
|
87
125
|
user: 'postgres',
|
|
88
|
-
password: 'secret'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// Or with connection URL
|
|
92
|
-
const db = new Relq(schema, {
|
|
93
|
-
url: process.env.DATABASE_URL
|
|
126
|
+
password: 'secret',
|
|
127
|
+
relations
|
|
94
128
|
});
|
|
95
|
-
```
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
// Types flow from schema to results
|
|
101
|
-
const users = await db.table.users
|
|
130
|
+
// Types flow automatically from schema to results
|
|
131
|
+
const activeUsers = await db.table.users
|
|
102
132
|
.select(['id', 'email', 'status'])
|
|
103
133
|
.where(q => q.equal('status', 'active'))
|
|
134
|
+
.orderBy('createdAt', 'DESC')
|
|
135
|
+
.limit(10)
|
|
104
136
|
.all();
|
|
105
137
|
// Type: { id: string; email: string; status: 'active' | 'inactive' | 'suspended' }[]
|
|
106
138
|
|
|
@@ -109,11 +141,37 @@ const user = await db.table.users.findById('uuid-here');
|
|
|
109
141
|
const user = await db.table.users.findOne({ email: 'test@example.com' });
|
|
110
142
|
```
|
|
111
143
|
|
|
112
|
-
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Entry Points
|
|
147
|
+
|
|
148
|
+
Relq provides three entry points for different use cases:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Runtime - Client, queries, functions
|
|
152
|
+
import { Relq, F, Case, PG } from 'relq';
|
|
153
|
+
|
|
154
|
+
// Configuration - CLI and project setup
|
|
155
|
+
import { defineConfig, loadConfig } from 'relq/config';
|
|
156
|
+
|
|
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';
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Schema Definition
|
|
169
|
+
|
|
170
|
+
### Column Types
|
|
113
171
|
|
|
114
172
|
Relq supports 100+ PostgreSQL types with proper TypeScript mapping:
|
|
115
173
|
|
|
116
|
-
|
|
174
|
+
#### Numeric Types
|
|
117
175
|
```typescript
|
|
118
176
|
integer(), int(), int4() // number
|
|
119
177
|
smallint(), int2() // number
|
|
@@ -126,23 +184,23 @@ doublePrecision(), float8() // number
|
|
|
126
184
|
money() // string
|
|
127
185
|
```
|
|
128
186
|
|
|
129
|
-
|
|
187
|
+
#### String Types
|
|
130
188
|
```typescript
|
|
131
189
|
text() // string
|
|
132
190
|
varchar(), char() // string
|
|
133
|
-
citext() // string (case-insensitive
|
|
191
|
+
citext() // string (case-insensitive)
|
|
134
192
|
```
|
|
135
193
|
|
|
136
|
-
|
|
194
|
+
#### Date/Time Types
|
|
137
195
|
```typescript
|
|
138
196
|
timestamp() // Date
|
|
139
|
-
timestamptz()
|
|
197
|
+
timestamptz() // Date (with timezone)
|
|
140
198
|
date() // Date | string
|
|
141
199
|
time(), timetz() // string
|
|
142
200
|
interval() // string
|
|
143
201
|
```
|
|
144
202
|
|
|
145
|
-
|
|
203
|
+
#### JSON Types
|
|
146
204
|
```typescript
|
|
147
205
|
json<T>() // T (typed JSON)
|
|
148
206
|
jsonb<T>() // T (typed JSONB)
|
|
@@ -151,13 +209,13 @@ jsonb<T>() // T (typed JSONB)
|
|
|
151
209
|
metadata: jsonb<{ theme: string; settings: Record<string, boolean> }>()
|
|
152
210
|
```
|
|
153
211
|
|
|
154
|
-
|
|
212
|
+
#### Boolean & UUID
|
|
155
213
|
```typescript
|
|
156
214
|
boolean(), bool() // boolean
|
|
157
215
|
uuid() // string
|
|
158
216
|
```
|
|
159
217
|
|
|
160
|
-
|
|
218
|
+
#### Array Types
|
|
161
219
|
```typescript
|
|
162
220
|
// Any column type can be an array
|
|
163
221
|
tags: text().array() // string[]
|
|
@@ -165,7 +223,7 @@ matrix: integer().array(2) // number[][] (2D array)
|
|
|
165
223
|
scores: numeric().array() // string[]
|
|
166
224
|
```
|
|
167
225
|
|
|
168
|
-
|
|
226
|
+
#### Geometric Types
|
|
169
227
|
```typescript
|
|
170
228
|
point() // { x: number; y: number }
|
|
171
229
|
line() // { a: number; b: number; c: number }
|
|
@@ -176,15 +234,14 @@ polygon() // Array<{ x: number; y: number }>
|
|
|
176
234
|
circle() // { x: number; y: number; r: number }
|
|
177
235
|
```
|
|
178
236
|
|
|
179
|
-
|
|
237
|
+
#### Network Types
|
|
180
238
|
```typescript
|
|
181
239
|
inet() // string (IP address)
|
|
182
240
|
cidr() // string (IP network)
|
|
183
|
-
macaddr()
|
|
184
|
-
macaddr8() // string
|
|
241
|
+
macaddr(), macaddr8() // string
|
|
185
242
|
```
|
|
186
243
|
|
|
187
|
-
|
|
244
|
+
#### Range Types
|
|
188
245
|
```typescript
|
|
189
246
|
int4range(), int8range() // string
|
|
190
247
|
numrange(), daterange() // string
|
|
@@ -192,13 +249,13 @@ tsrange(), tstzrange() // string
|
|
|
192
249
|
// Multi-range variants also available
|
|
193
250
|
```
|
|
194
251
|
|
|
195
|
-
|
|
252
|
+
#### Full-Text Search
|
|
196
253
|
```typescript
|
|
197
254
|
tsvector() // string
|
|
198
255
|
tsquery() // string
|
|
199
256
|
```
|
|
200
257
|
|
|
201
|
-
|
|
258
|
+
#### PostGIS (requires extension)
|
|
202
259
|
```typescript
|
|
203
260
|
geometry('location', 4326, 'POINT') // GeoJSON
|
|
204
261
|
geography('area', 4326, 'POLYGON') // GeoJSON
|
|
@@ -206,7 +263,7 @@ geoPoint('coords') // { x, y, srid }
|
|
|
206
263
|
box2d(), box3d() // string
|
|
207
264
|
```
|
|
208
265
|
|
|
209
|
-
|
|
266
|
+
#### Extension Types
|
|
210
267
|
```typescript
|
|
211
268
|
ltree() // string (hierarchical labels)
|
|
212
269
|
hstore() // Record<string, string | null>
|
|
@@ -214,17 +271,18 @@ cube() // number[]
|
|
|
214
271
|
semver() // string
|
|
215
272
|
```
|
|
216
273
|
|
|
274
|
+
---
|
|
275
|
+
|
|
217
276
|
## Query API
|
|
218
277
|
|
|
219
|
-
###
|
|
278
|
+
### SELECT
|
|
279
|
+
|
|
220
280
|
```typescript
|
|
221
281
|
// All columns
|
|
222
282
|
const users = await db.table.users.select().all();
|
|
223
283
|
|
|
224
284
|
// Specific columns
|
|
225
|
-
const emails = await db.table.users
|
|
226
|
-
.select(['id', 'email'])
|
|
227
|
-
.all();
|
|
285
|
+
const emails = await db.table.users.select(['id', 'email']).all();
|
|
228
286
|
|
|
229
287
|
// With conditions
|
|
230
288
|
const active = await db.table.users
|
|
@@ -238,17 +296,14 @@ const active = await db.table.users
|
|
|
238
296
|
const user = await db.table.users
|
|
239
297
|
.select()
|
|
240
298
|
.where(q => q.equal('id', userId))
|
|
241
|
-
.
|
|
299
|
+
.get();
|
|
242
300
|
|
|
243
301
|
// With joins
|
|
244
302
|
const postsWithAuthors = await db.table.posts
|
|
245
|
-
.select(['
|
|
246
|
-
.
|
|
303
|
+
.select(['id', 'title'])
|
|
304
|
+
.join('users', (on, posts, users) => on.equal(posts.authorId, users.id))
|
|
247
305
|
.all();
|
|
248
306
|
|
|
249
|
-
// Distinct
|
|
250
|
-
await db.table.users.select(['status']).distinct().all();
|
|
251
|
-
|
|
252
307
|
// Distinct on (PostgreSQL-specific)
|
|
253
308
|
await db.table.logs
|
|
254
309
|
.select()
|
|
@@ -257,22 +312,23 @@ await db.table.logs
|
|
|
257
312
|
.orderBy('createdAt', 'DESC')
|
|
258
313
|
.all();
|
|
259
314
|
|
|
260
|
-
//
|
|
315
|
+
// Row locking
|
|
261
316
|
await db.table.jobs
|
|
262
317
|
.select()
|
|
263
318
|
.where(q => q.equal('status', 'pending'))
|
|
264
319
|
.forUpdateSkipLocked()
|
|
265
320
|
.limit(1)
|
|
266
|
-
.
|
|
321
|
+
.get();
|
|
267
322
|
```
|
|
268
323
|
|
|
269
|
-
###
|
|
324
|
+
### INSERT
|
|
325
|
+
|
|
270
326
|
```typescript
|
|
271
327
|
// Single insert with returning
|
|
272
328
|
const user = await db.table.users
|
|
273
329
|
.insert({ email: 'new@example.com', name: 'New User' })
|
|
274
330
|
.returning(['id', 'createdAt'])
|
|
275
|
-
.
|
|
331
|
+
.run();
|
|
276
332
|
|
|
277
333
|
// Bulk insert
|
|
278
334
|
await db.table.users
|
|
@@ -297,7 +353,8 @@ await db.table.users
|
|
|
297
353
|
.run();
|
|
298
354
|
```
|
|
299
355
|
|
|
300
|
-
###
|
|
356
|
+
### UPDATE
|
|
357
|
+
|
|
301
358
|
```typescript
|
|
302
359
|
// Basic update
|
|
303
360
|
await db.table.users
|
|
@@ -310,7 +367,7 @@ const updated = await db.table.posts
|
|
|
310
367
|
.update({ viewCount: F.increment('viewCount', 1) })
|
|
311
368
|
.where(q => q.equal('id', postId))
|
|
312
369
|
.returning(['id', 'viewCount'])
|
|
313
|
-
.
|
|
370
|
+
.run();
|
|
314
371
|
|
|
315
372
|
// Bulk update
|
|
316
373
|
await db.table.posts
|
|
@@ -319,7 +376,8 @@ await db.table.posts
|
|
|
319
376
|
.run();
|
|
320
377
|
```
|
|
321
378
|
|
|
322
|
-
###
|
|
379
|
+
### DELETE
|
|
380
|
+
|
|
323
381
|
```typescript
|
|
324
382
|
// Delete with condition
|
|
325
383
|
await db.table.users
|
|
@@ -332,22 +390,26 @@ const deleted = await db.table.posts
|
|
|
332
390
|
.delete()
|
|
333
391
|
.where(q => q.equal('authorId', userId))
|
|
334
392
|
.returning(['id', 'title'])
|
|
335
|
-
.
|
|
393
|
+
.run();
|
|
336
394
|
```
|
|
337
395
|
|
|
338
396
|
### Aggregations
|
|
397
|
+
|
|
339
398
|
```typescript
|
|
340
399
|
// Count
|
|
341
400
|
const count = await db.table.users
|
|
342
401
|
.count()
|
|
343
402
|
.where(q => q.equal('status', 'active'))
|
|
344
|
-
.
|
|
403
|
+
.get();
|
|
345
404
|
|
|
346
|
-
// Count with
|
|
347
|
-
const
|
|
348
|
-
.
|
|
349
|
-
.
|
|
350
|
-
.
|
|
405
|
+
// Count with groups
|
|
406
|
+
const counts = await db.table.results.count()
|
|
407
|
+
.group('all', q => q.equal('isDeleted', false))
|
|
408
|
+
.group('new', q => q.equal('isRead', false).equal('isDeleted', false))
|
|
409
|
+
.group('favorites', q => q.equal('favorite', true).equal('isDeleted', false))
|
|
410
|
+
.where(q => q.equal('userId', userId))
|
|
411
|
+
.get();
|
|
412
|
+
// Returns: { all: number, new: number, favorites: number }
|
|
351
413
|
|
|
352
414
|
// Multiple aggregations
|
|
353
415
|
const stats = await db.table.orders
|
|
@@ -357,12 +419,13 @@ const stats = await db.table.orders
|
|
|
357
419
|
.avg('amount', 'avgOrderValue')
|
|
358
420
|
.min('amount', 'minOrder')
|
|
359
421
|
.max('amount', 'maxOrder')
|
|
360
|
-
.
|
|
422
|
+
.get();
|
|
361
423
|
```
|
|
362
424
|
|
|
363
425
|
### Pagination
|
|
426
|
+
|
|
364
427
|
```typescript
|
|
365
|
-
// Cursor-based (recommended)
|
|
428
|
+
// Cursor-based (recommended for large datasets)
|
|
366
429
|
const page = await db.table.posts
|
|
367
430
|
.select(['id', 'title', 'createdAt'])
|
|
368
431
|
.paginate({ orderBy: ['createdAt', 'DESC'] })
|
|
@@ -383,6 +446,66 @@ const page = await db.table.posts
|
|
|
383
446
|
// page.pagination.total
|
|
384
447
|
```
|
|
385
448
|
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## SQL Functions
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import { F, Case, PG } from 'relq';
|
|
455
|
+
|
|
456
|
+
// String Functions
|
|
457
|
+
F.lower('email'), F.upper('name')
|
|
458
|
+
F.concat('first', ' ', 'last')
|
|
459
|
+
F.substring('text', 1, 10)
|
|
460
|
+
F.trim('value'), F.ltrim('value'), F.rtrim('value')
|
|
461
|
+
F.length('text'), F.replace('text', 'old', 'new')
|
|
462
|
+
|
|
463
|
+
// Date/Time Functions
|
|
464
|
+
F.now(), F.currentDate(), F.currentTimestamp()
|
|
465
|
+
F.extract('year', 'created_at')
|
|
466
|
+
F.dateTrunc('month', 'created_at')
|
|
467
|
+
F.age('birth_date')
|
|
468
|
+
|
|
469
|
+
// Math Functions
|
|
470
|
+
F.abs('value'), F.ceil('value'), F.floor('value')
|
|
471
|
+
F.round('price', 2), F.trunc('value', 2)
|
|
472
|
+
F.power('base', 2), F.sqrt('value')
|
|
473
|
+
F.greatest('a', 'b', 'c'), F.least('a', 'b', 'c')
|
|
474
|
+
|
|
475
|
+
// Aggregate Functions
|
|
476
|
+
F.count('id'), F.sum('amount'), F.avg('rating')
|
|
477
|
+
F.min('price'), F.max('price')
|
|
478
|
+
F.arrayAgg('tag'), F.stringAgg('name', ', ')
|
|
479
|
+
|
|
480
|
+
// JSONB Functions
|
|
481
|
+
F.jsonbSet('data', ['key'], 'value')
|
|
482
|
+
F.jsonbExtract('data', 'key')
|
|
483
|
+
F.jsonbArrayLength('items')
|
|
484
|
+
|
|
485
|
+
// Array Functions
|
|
486
|
+
F.arrayAppend('tags', 'new')
|
|
487
|
+
F.arrayRemove('tags', 'old')
|
|
488
|
+
F.arrayLength('items', 1)
|
|
489
|
+
F.unnest('tags')
|
|
490
|
+
|
|
491
|
+
// Conditional (CASE)
|
|
492
|
+
Case()
|
|
493
|
+
.when(F.gt('price', 100), 'expensive')
|
|
494
|
+
.when(F.gt('price', 50), 'moderate')
|
|
495
|
+
.else('cheap')
|
|
496
|
+
.end()
|
|
497
|
+
|
|
498
|
+
// PostgreSQL Values
|
|
499
|
+
PG.now() // NOW()
|
|
500
|
+
PG.currentDate() // CURRENT_DATE
|
|
501
|
+
PG.currentUser() // CURRENT_USER
|
|
502
|
+
PG.null() // NULL
|
|
503
|
+
PG.true() // TRUE
|
|
504
|
+
PG.false() // FALSE
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
386
509
|
## Condition Builders
|
|
387
510
|
|
|
388
511
|
### Basic Comparisons
|
|
@@ -390,9 +513,7 @@ const page = await db.table.posts
|
|
|
390
513
|
.where(q => q.equal('status', 'active'))
|
|
391
514
|
.where(q => q.notEqual('role', 'guest'))
|
|
392
515
|
.where(q => q.greaterThan('age', 18))
|
|
393
|
-
.where(q => q.greaterThanEqual('score', 100))
|
|
394
516
|
.where(q => q.lessThan('price', 50))
|
|
395
|
-
.where(q => q.lessThanEqual('quantity', 10))
|
|
396
517
|
.where(q => q.between('createdAt', startDate, endDate))
|
|
397
518
|
```
|
|
398
519
|
|
|
@@ -455,7 +576,6 @@ const page = await db.table.posts
|
|
|
455
576
|
// Typed array conditions
|
|
456
577
|
.where(q => q.array.string.startsWith('emails', 'admin@'))
|
|
457
578
|
.where(q => q.array.numeric.greaterThan('scores', 90))
|
|
458
|
-
.where(q => q.array.date.after('dates', '2024-01-01'))
|
|
459
579
|
```
|
|
460
580
|
|
|
461
581
|
### Full-Text Search
|
|
@@ -465,45 +585,31 @@ const page = await db.table.posts
|
|
|
465
585
|
.where(q => q.fulltext.rank('body', 'search terms', 0.1))
|
|
466
586
|
```
|
|
467
587
|
|
|
468
|
-
### Range Conditions
|
|
588
|
+
### Range & Geometric Conditions
|
|
469
589
|
```typescript
|
|
470
590
|
.where(q => q.range.contains('dateRange', '2024-06-15'))
|
|
471
|
-
.where(q => q.range.containedBy('priceRange', '[0, 1000]'))
|
|
472
591
|
.where(q => q.range.overlaps('availability', '[2024-01-01, 2024-12-31]'))
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### Geometric Conditions
|
|
476
|
-
```typescript
|
|
477
|
-
.where(q => q.geometric.contains('area', '(0,0),(10,10)'))
|
|
478
|
-
.where(q => q.geometric.overlaps('region', box))
|
|
479
592
|
.where(q => q.geometric.distanceLessThan('location', '(5,5)', 10))
|
|
480
593
|
```
|
|
481
594
|
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
.where(q => q.network.containsOrEqual('subnet', '192.168.1.0/24'))
|
|
485
|
-
.where(q => q.network.isIPv4('address'))
|
|
486
|
-
.where(q => q.network.isIPv6('address'))
|
|
487
|
-
```
|
|
595
|
+
---
|
|
488
596
|
|
|
489
597
|
## Advanced Schema Features
|
|
490
598
|
|
|
491
599
|
### Domains with Validation
|
|
600
|
+
|
|
492
601
|
```typescript
|
|
493
602
|
import { pgDomain, text, numeric } from 'relq/schema-builder';
|
|
494
603
|
|
|
495
|
-
// Email domain with pattern validation
|
|
496
604
|
export const emailDomain = pgDomain('email', text(), (value) => [
|
|
497
605
|
value.matches('^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$')
|
|
498
606
|
]);
|
|
499
607
|
|
|
500
|
-
// Percentage domain with range validation
|
|
501
608
|
export const percentageDomain = pgDomain('percentage',
|
|
502
609
|
numeric().precision(5).scale(2),
|
|
503
610
|
(value) => [value.gte(0), value.lte(100)]
|
|
504
611
|
);
|
|
505
612
|
|
|
506
|
-
// Use in tables
|
|
507
613
|
const employees = defineTable('employees', {
|
|
508
614
|
email: emailDomain().notNull(),
|
|
509
615
|
bonus: percentageDomain().default(0)
|
|
@@ -511,6 +617,7 @@ const employees = defineTable('employees', {
|
|
|
511
617
|
```
|
|
512
618
|
|
|
513
619
|
### Composite Types
|
|
620
|
+
|
|
514
621
|
```typescript
|
|
515
622
|
import { pgComposite, text, varchar, boolean } from 'relq/schema-builder';
|
|
516
623
|
|
|
@@ -530,6 +637,7 @@ const customers = defineTable('customers', {
|
|
|
530
637
|
```
|
|
531
638
|
|
|
532
639
|
### Generated Columns
|
|
640
|
+
|
|
533
641
|
```typescript
|
|
534
642
|
const orderItems = defineTable('order_items', {
|
|
535
643
|
quantity: integer().notNull(),
|
|
@@ -543,19 +651,15 @@ const orderItems = defineTable('order_items', {
|
|
|
543
651
|
.multiply(F.subtract(1, F.divide(table.discount, 100)))
|
|
544
652
|
),
|
|
545
653
|
|
|
546
|
-
//
|
|
654
|
+
// Full-text search vector
|
|
547
655
|
searchVector: tsvector().generatedAlwaysAs(
|
|
548
656
|
(table, F) => F.toTsvector('english', table.description)
|
|
549
|
-
),
|
|
550
|
-
|
|
551
|
-
// String concatenation
|
|
552
|
-
fullName: text().generatedAlwaysAs(
|
|
553
|
-
(table, F) => F.concat(table.firstName, ' ', table.lastName)
|
|
554
657
|
)
|
|
555
658
|
});
|
|
556
659
|
```
|
|
557
660
|
|
|
558
661
|
### Table Partitioning
|
|
662
|
+
|
|
559
663
|
```typescript
|
|
560
664
|
// Range partitioning
|
|
561
665
|
const events = defineTable('events', {
|
|
@@ -601,24 +705,35 @@ const sessions = defineTable('sessions', {
|
|
|
601
705
|
});
|
|
602
706
|
```
|
|
603
707
|
|
|
604
|
-
###
|
|
708
|
+
### Triggers and Functions
|
|
709
|
+
|
|
605
710
|
```typescript
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
711
|
+
import { pgTrigger, pgFunction } from 'relq/schema-builder';
|
|
712
|
+
|
|
713
|
+
// Define a function
|
|
714
|
+
export const updateUpdatedAt = pgFunction('update_updated_at_column', {
|
|
715
|
+
returns: 'trigger',
|
|
716
|
+
language: 'plpgsql',
|
|
717
|
+
body: `
|
|
718
|
+
BEGIN
|
|
719
|
+
NEW.updated_at = NOW();
|
|
720
|
+
RETURN NEW;
|
|
721
|
+
END;
|
|
722
|
+
`,
|
|
723
|
+
volatility: 'VOLATILE',
|
|
724
|
+
}).$id('func123');
|
|
725
|
+
|
|
726
|
+
// Define a trigger using the function
|
|
727
|
+
export const usersUpdatedAt = pgTrigger('users_updated_at', {
|
|
728
|
+
on: schema.users,
|
|
729
|
+
before: 'UPDATE',
|
|
730
|
+
forEach: 'ROW',
|
|
731
|
+
execute: updateUpdatedAt,
|
|
732
|
+
}).$id('trig456');
|
|
619
733
|
```
|
|
620
734
|
|
|
621
735
|
### Indexes
|
|
736
|
+
|
|
622
737
|
```typescript
|
|
623
738
|
const posts = defineTable('posts', {
|
|
624
739
|
id: uuid().primaryKey(),
|
|
@@ -653,150 +768,81 @@ const posts = defineTable('posts', {
|
|
|
653
768
|
|
|
654
769
|
// Expression index
|
|
655
770
|
index('posts_title_lower_idx')
|
|
656
|
-
.on(F => F.lower(table.title))
|
|
657
|
-
|
|
658
|
-
// With storage options
|
|
659
|
-
index('posts_search_idx')
|
|
660
|
-
.on(table.searchVector)
|
|
661
|
-
.using('gin')
|
|
662
|
-
.with({ fastupdate: false })
|
|
771
|
+
.on(F => F.lower(table.title))
|
|
663
772
|
]
|
|
664
773
|
});
|
|
665
774
|
```
|
|
666
775
|
|
|
667
|
-
|
|
668
|
-
```typescript
|
|
669
|
-
import { one, many, manyToMany } from 'relq/schema-builder';
|
|
776
|
+
---
|
|
670
777
|
|
|
671
|
-
|
|
672
|
-
id: uuid().primaryKey(),
|
|
673
|
-
email: text().notNull().unique()
|
|
674
|
-
}, {
|
|
675
|
-
relations: {
|
|
676
|
-
posts: many('posts', { foreignKey: 'authorId' }),
|
|
677
|
-
profile: one('profiles', { foreignKey: 'userId' })
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
export const posts = defineTable('posts', {
|
|
682
|
-
id: uuid().primaryKey(),
|
|
683
|
-
authorId: uuid('author_id').references('users', 'id')
|
|
684
|
-
}, {
|
|
685
|
-
relations: {
|
|
686
|
-
author: one('users', { foreignKey: 'authorId' }),
|
|
687
|
-
tags: manyToMany('tags', {
|
|
688
|
-
through: 'post_tags',
|
|
689
|
-
foreignKey: 'postId',
|
|
690
|
-
otherKey: 'tagId'
|
|
691
|
-
})
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
## SQL Functions
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
import { F, Case, PG } from 'relq';
|
|
700
|
-
|
|
701
|
-
// String
|
|
702
|
-
F.lower('email'), F.upper('name')
|
|
703
|
-
F.concat('first', ' ', 'last')
|
|
704
|
-
F.substring('text', 1, 10)
|
|
705
|
-
F.trim('value'), F.ltrim('value'), F.rtrim('value')
|
|
706
|
-
F.length('text'), F.replace('text', 'old', 'new')
|
|
778
|
+
## CLI Commands
|
|
707
779
|
|
|
708
|
-
|
|
709
|
-
F.now(), F.currentDate(), F.currentTimestamp()
|
|
710
|
-
F.extract('year', 'created_at')
|
|
711
|
-
F.dateTrunc('month', 'created_at')
|
|
712
|
-
F.age('birth_date')
|
|
780
|
+
Relq provides a comprehensive git-like CLI for schema management:
|
|
713
781
|
|
|
714
|
-
|
|
715
|
-
F.abs('value'), F.ceil('value'), F.floor('value')
|
|
716
|
-
F.round('price', 2), F.trunc('value', 2)
|
|
717
|
-
F.power('base', 2), F.sqrt('value')
|
|
718
|
-
F.greatest('a', 'b', 'c'), F.least('a', 'b', 'c')
|
|
782
|
+
### Initialization & Status
|
|
719
783
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
784
|
+
```bash
|
|
785
|
+
relq init # Initialize a new Relq project
|
|
786
|
+
relq status # Show current schema status and pending changes
|
|
787
|
+
```
|
|
724
788
|
|
|
725
|
-
|
|
726
|
-
F.jsonbSet('data', ['key'], 'value')
|
|
727
|
-
F.jsonbExtract('data', 'key')
|
|
728
|
-
F.jsonbArrayLength('items')
|
|
789
|
+
### Schema Operations
|
|
729
790
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
```
|
|
735
798
|
|
|
736
|
-
|
|
737
|
-
Case()
|
|
738
|
-
.when(F.gt('price', 100), 'expensive')
|
|
739
|
-
.when(F.gt('price', 50), 'moderate')
|
|
740
|
-
.else('cheap')
|
|
741
|
-
.end()
|
|
799
|
+
### Change Management
|
|
742
800
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
PG.null() // NULL
|
|
748
|
-
PG.true() // TRUE
|
|
749
|
-
PG.false() // FALSE
|
|
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
|
|
750
805
|
```
|
|
751
806
|
|
|
752
|
-
|
|
807
|
+
### Migration Commands
|
|
753
808
|
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
await tx.table.posts
|
|
763
|
-
.insert({ title: 'First Post', authorId: user.id })
|
|
764
|
-
.run();
|
|
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
|
+
```
|
|
765
816
|
|
|
766
|
-
|
|
767
|
-
});
|
|
817
|
+
### Branching & Merging
|
|
768
818
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
825
|
+
```
|
|
772
826
|
|
|
773
|
-
|
|
774
|
-
await tx.savepoint('optional', async (sp) => {
|
|
775
|
-
await sp.table.posts.insert({ ... }).run();
|
|
776
|
-
});
|
|
777
|
-
} catch (e) {
|
|
778
|
-
// Savepoint rolled back, transaction continues
|
|
779
|
-
}
|
|
827
|
+
### Remote Operations
|
|
780
828
|
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
783
833
|
```
|
|
784
834
|
|
|
785
|
-
|
|
835
|
+
### Utilities
|
|
786
836
|
|
|
787
837
|
```bash
|
|
788
|
-
relq
|
|
789
|
-
relq
|
|
790
|
-
relq
|
|
791
|
-
relq
|
|
792
|
-
relq generate -m "message" # Create migration
|
|
793
|
-
relq push [--dry-run] # Apply migrations
|
|
794
|
-
relq log / relq history # View history
|
|
795
|
-
relq rollback [n] # Rollback migrations
|
|
796
|
-
relq sync # Full sync
|
|
797
|
-
relq introspect # Generate schema from DB
|
|
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
|
|
798
842
|
```
|
|
799
843
|
|
|
844
|
+
---
|
|
845
|
+
|
|
800
846
|
## Configuration
|
|
801
847
|
|
|
802
848
|
```typescript
|
|
@@ -809,13 +855,15 @@ export default defineConfig({
|
|
|
809
855
|
port: 5432,
|
|
810
856
|
database: 'myapp',
|
|
811
857
|
user: 'postgres',
|
|
812
|
-
password: process.env.DB_PASSWORD
|
|
858
|
+
password: process.env.DB_PASSWORD,
|
|
859
|
+
// Or use connection string
|
|
860
|
+
// url: process.env.DATABASE_URL
|
|
813
861
|
},
|
|
814
862
|
schema: './db/schema.ts',
|
|
815
863
|
migrations: {
|
|
816
864
|
directory: './db/migrations',
|
|
817
865
|
tableName: '_relq_migrations',
|
|
818
|
-
format: 'timestamp'
|
|
866
|
+
format: 'timestamp' // or 'sequential'
|
|
819
867
|
},
|
|
820
868
|
generate: {
|
|
821
869
|
outDir: './db/generated',
|
|
@@ -828,35 +876,124 @@ export default defineConfig({
|
|
|
828
876
|
});
|
|
829
877
|
```
|
|
830
878
|
|
|
879
|
+
### Environment-Specific Configuration
|
|
880
|
+
|
|
881
|
+
```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
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
await tx.table.logs.insert({ ... }).run();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// With isolation level
|
|
942
|
+
await db.transaction({ isolation: 'SERIALIZABLE' }, async (tx) => {
|
|
943
|
+
// ...
|
|
944
|
+
});
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
831
949
|
## Error Handling
|
|
832
950
|
|
|
833
951
|
```typescript
|
|
834
|
-
import {
|
|
952
|
+
import {
|
|
953
|
+
RelqError,
|
|
954
|
+
RelqConnectionError,
|
|
955
|
+
RelqQueryError,
|
|
956
|
+
isRelqError
|
|
957
|
+
} from 'relq';
|
|
835
958
|
|
|
836
959
|
try {
|
|
837
960
|
await db.table.users.insert({ ... }).run();
|
|
838
961
|
} catch (error) {
|
|
839
962
|
if (isRelqError(error)) {
|
|
840
963
|
if (error instanceof RelqConnectionError) {
|
|
841
|
-
|
|
964
|
+
console.error('Connection failed:', error.message);
|
|
842
965
|
} else if (error instanceof RelqQueryError) {
|
|
966
|
+
console.error('Query failed:', error.message);
|
|
843
967
|
console.error('SQL:', error.sql);
|
|
968
|
+
console.error('Parameters:', error.parameters);
|
|
844
969
|
}
|
|
845
970
|
}
|
|
846
971
|
}
|
|
847
972
|
```
|
|
848
973
|
|
|
974
|
+
---
|
|
975
|
+
|
|
849
976
|
## Requirements
|
|
850
977
|
|
|
851
|
-
- Node.js
|
|
852
|
-
- PostgreSQL 12+
|
|
853
|
-
- TypeScript 5.0+
|
|
978
|
+
- **Node.js** 22+ or **Bun** 1.0+
|
|
979
|
+
- **PostgreSQL** 12+
|
|
980
|
+
- **TypeScript** 5.0+
|
|
981
|
+
|
|
982
|
+
---
|
|
854
983
|
|
|
855
984
|
## License
|
|
856
985
|
|
|
857
986
|
MIT
|
|
858
987
|
|
|
988
|
+
---
|
|
989
|
+
|
|
859
990
|
## Links
|
|
860
991
|
|
|
861
992
|
- [GitHub](https://github.com/yuniqsolutions/relq)
|
|
862
993
|
- [npm](https://www.npmjs.com/package/relq)
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
<p align="center">
|
|
998
|
+
<strong>Built with TypeScript for TypeScript developers</strong>
|
|
999
|
+
</p>
|