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 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 - 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 git-like CLI for schema management—all with zero runtime dependencies.
6
6
 
7
- ## Why Relq?
7
+ [![npm version](https://img.shields.io/npm/v/relq.svg)](https://www.npmjs.com/package/relq)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-22+-green.svg)](https://nodejs.org/)
10
+ [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-12+-blue.svg)](https://www.postgresql.org/)
8
11
 
9
- - **Complete Type Safety** - End-to-end TypeScript inference from schema to query results
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
- ## Installation
14
+ ## Table of Contents
17
15
 
18
- ```bash
19
- npm install relq
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
- ## Entry Points
30
+ ---
23
31
 
24
- ```typescript
25
- // Runtime - Client, queries, functions
26
- import { Relq, F, Case, PG } from 'relq';
32
+ ## Features
27
33
 
28
- // Configuration
29
- import { defineConfig, loadConfig } from 'relq/config';
34
+ ### Core Capabilities
30
35
 
31
- // Schema Builder - Types, tables, DDL
32
- import {
33
- defineTable,
34
- integer, text, uuid, jsonb, timestamp,
35
- pgEnum, pgDomain, pgComposite,
36
- one, many
37
- } from 'relq/schema-builder';
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
- ### 3. Query with Full Type Safety
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
- ## Column Types
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
- ### Numeric Types
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
- ### String Types
187
+ #### String Types
130
188
  ```typescript
131
189
  text() // string
132
190
  varchar(), char() // string
133
- citext() // string (case-insensitive, requires extension)
191
+ citext() // string (case-insensitive)
134
192
  ```
135
193
 
136
- ### Date/Time Types
194
+ #### Date/Time Types
137
195
  ```typescript
138
196
  timestamp() // Date
139
- timestamptz(), timestampWithTimeZone() // Date
197
+ timestamptz() // Date (with timezone)
140
198
  date() // Date | string
141
199
  time(), timetz() // string
142
200
  interval() // string
143
201
  ```
144
202
 
145
- ### JSON Types
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
- ### Boolean & UUID
212
+ #### Boolean & UUID
155
213
  ```typescript
156
214
  boolean(), bool() // boolean
157
215
  uuid() // string
158
216
  ```
159
217
 
160
- ### Array Types
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
- ### Geometric Types
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
- ### Network Types
237
+ #### Network Types
180
238
  ```typescript
181
239
  inet() // string (IP address)
182
240
  cidr() // string (IP network)
183
- macaddr() // string
184
- macaddr8() // string
241
+ macaddr(), macaddr8() // string
185
242
  ```
186
243
 
187
- ### Range Types
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
- ### Full-Text Search
252
+ #### Full-Text Search
196
253
  ```typescript
197
254
  tsvector() // string
198
255
  tsquery() // string
199
256
  ```
200
257
 
201
- ### PostGIS (requires extension)
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
- ### Extension Types
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
- ### Select
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
- .one();
299
+ .get();
242
300
 
243
301
  // With joins
244
302
  const postsWithAuthors = await db.table.posts
245
- .select(['posts.id', 'posts.title', 'users.name'])
246
- .leftJoin('users', 'users.id = posts.author_id')
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
- // Locking
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
- .one();
321
+ .get();
267
322
  ```
268
323
 
269
- ### Insert
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
- .one();
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
- ### Update
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
- .one();
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
- ### Delete
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
- .all();
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
- .run();
403
+ .get();
345
404
 
346
- // Count with group by
347
- const byStatus = await db.table.users
348
- .count()
349
- .groupBy('status')
350
- .run();
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
- .one();
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
- ### Network Conditions
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
- // Using SQL functions
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
- ### Check Constraints
708
+ ### Triggers and Functions
709
+
605
710
  ```typescript
606
- const products = defineTable('products', {
607
- price: numeric().precision(10).scale(2).notNull(),
608
- salePrice: numeric().precision(10).scale(2),
609
- stockQuantity: integer().default(0)
610
- }, {
611
- checkConstraints: (table, check) => [
612
- check.constraint('price_positive', table.price.gt(0)),
613
- check.constraint('sale_price_valid',
614
- table.salePrice.isNull().or(table.salePrice.lte(table.price))
615
- ),
616
- check.constraint('stock_non_negative', table.stockQuantity.gte(0))
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
- ### Relations
668
- ```typescript
669
- import { one, many, manyToMany } from 'relq/schema-builder';
776
+ ---
670
777
 
671
- export const users = defineTable('users', {
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
- // Date/Time
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
- // Math
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
- // Aggregates
721
- F.count('id'), F.sum('amount'), F.avg('rating')
722
- F.min('price'), F.max('price')
723
- F.arrayAgg('tag'), F.stringAgg('name', ', ')
784
+ ```bash
785
+ relq init # Initialize a new Relq project
786
+ relq status # Show current schema status and pending changes
787
+ ```
724
788
 
725
- // JSONB
726
- F.jsonbSet('data', ['key'], 'value')
727
- F.jsonbExtract('data', 'key')
728
- F.jsonbArrayLength('items')
789
+ ### Schema Operations
729
790
 
730
- // Arrays
731
- F.arrayAppend('tags', 'new')
732
- F.arrayRemove('tags', 'old')
733
- F.arrayLength('items', 1)
734
- F.unnest('tags')
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
- // Conditional
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
- // PostgreSQL values
744
- PG.now() // NOW()
745
- PG.currentDate() // CURRENT_DATE
746
- PG.currentUser() // CURRENT_USER
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
- ## Transactions
807
+ ### Migration Commands
753
808
 
754
- ```typescript
755
- // Basic transaction
756
- const result = await db.transaction(async (tx) => {
757
- const user = await tx.table.users
758
- .insert({ email: 'new@example.com', name: 'User' })
759
- .returning(['id'])
760
- .one();
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
- return user;
767
- });
817
+ ### Branching & Merging
768
818
 
769
- // With savepoints
770
- await db.transaction(async (tx) => {
771
- await tx.table.users.insert({ ... }).run();
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
- try {
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
- await tx.table.logs.insert({ ... }).run();
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
- ## CLI Commands
835
+ ### Utilities
786
836
 
787
837
  ```bash
788
- relq init # Initialize project
789
- relq status # Show pending changes
790
- relq diff [--sql] # Show differences
791
- relq pull [--force] # Pull from database
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 { RelqError, RelqConnectionError, RelqQueryError, isRelqError } from 'relq';
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
- // Connection issues
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 18+ or Bun 1.0+
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>