metal-orm 1.0.1 → 1.0.2

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.
@@ -0,0 +1,38 @@
1
+ name: Publish metal-orm to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write # OIDC for Trusted Publishing
11
+ packages: write
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20'
25
+ registry-url: 'https://registry.npmjs.org'
26
+
27
+ - name: Install deps
28
+ run: npm ci
29
+
30
+ - name: Test
31
+ run: npm test
32
+ continue-on-error: true # ou remove se quiser quebrar se falhar
33
+
34
+ - name: Build
35
+ run: npm run build
36
+
37
+ - name: Publish (Trusted Publishing)
38
+ run: npm publish --provenance --access public
package/README.md CHANGED
@@ -8,152 +8,17 @@
8
8
 
9
9
  MetalORM keeps SQL generation deterministic (CTEs, aggregates, window functions, EXISTS/subqueries) while letting you introspect the AST or reuse builders inside larger queries. It's designed for developers who want the power of raw SQL with the convenience of a modern ORM.
10
10
 
11
- ## Philosophy
11
+ ## Documentation
12
12
 
13
- MetalORM follows these core principles:
14
-
15
- - **Type Safety First**: Leverage TypeScript to catch errors at compile time
16
- - **SQL Transparency**: Generate predictable, readable SQL that you can inspect
17
- - **Composition Over Configuration**: Build complex queries by composing simple parts
18
- - **Zero Magic**: Explicit operations with clear AST representation
19
- - **Multi-Dialect Support**: Write once, compile to MySQL, SQLite, or SQL Server
13
+ For detailed information and API reference, please visit our [full documentation](docs/index.md).
20
14
 
21
15
  ## Features
22
16
 
23
- ### Declarative Schema Definition
24
- Define your database structure in TypeScript with full type inference:
25
-
26
- ```typescript
27
- const users = defineTable(
28
- 'users',
29
- {
30
- id: col.int().primaryKey(),
31
- name: col.varchar(255).notNull(),
32
- email: col.varchar(255).unique(),
33
- createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
34
- },
35
- {
36
- posts: hasMany(posts, 'userId'),
37
- profile: hasOne(profiles, 'userId')
38
- }
39
- );
40
- ```
41
-
42
- ### Rich Query Building
43
- ```typescript
44
- // Simple queries
45
- const simpleQuery = new SelectQueryBuilder(users)
46
- .selectRaw('*')
47
- .where(eq(users.columns.id, 1))
48
- .limit(1);
49
-
50
- // Complex joins with relations
51
- const complexQuery = new SelectQueryBuilder(users)
52
- .select({
53
- userId: users.columns.id,
54
- userName: users.columns.name,
55
- postCount: count(posts.columns.id),
56
- })
57
- .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
58
- .where(and(
59
- like(users.columns.name, '%John%'),
60
- gt(posts.columns.createdAt, new Date('2023-01-01'))
61
- ))
62
- .groupBy(users.columns.id)
63
- .having(gt(count(posts.columns.id), 5))
64
- .orderBy(count(posts.columns.id), 'DESC');
65
- ```
66
-
67
- ### Advanced SQL Features
68
- ```typescript
69
- // CTEs (Common Table Expressions)
70
- const since = new Date();
71
- since.setDate(since.getDate() - 30);
72
-
73
- const activeUsers = new SelectQueryBuilder(users)
74
- .selectRaw('*')
75
- .where(gt(users.columns.lastLogin, since))
76
- .as('active_users');
77
-
78
- const query = new SelectQueryBuilder(activeUsers)
79
- .with(activeUsers)
80
- .selectRaw('*')
81
- .where(eq(activeUsers.columns.id, 1));
82
-
83
- // Window Functions
84
- const rankedPosts = new SelectQueryBuilder(posts)
85
- .select({
86
- id: posts.columns.id,
87
- title: posts.columns.title,
88
- rank: windowFunction('RANK', [], [posts.columns.userId], [
89
- { column: posts.columns.createdAt, direction: 'DESC' }
90
- ])
91
- });
92
-
93
- // Subqueries and EXISTS
94
- const usersWithPosts = new SelectQueryBuilder(users)
95
- .selectRaw('*')
96
- .where(exists(
97
- new SelectQueryBuilder(posts)
98
- .selectRaw('1')
99
- .where(eq(posts.columns.userId, users.columns.id))
100
- ));
101
- ```
102
-
103
- ### Relation Hydration
104
- Automatically transform flat database rows into nested JavaScript objects:
105
-
106
- ```typescript
107
- const builder = new SelectQueryBuilder(users)
108
- .selectRaw('*')
109
- .include('posts', {
110
- columns: ['id', 'title', 'content'],
111
- include: {
112
- comments: {
113
- columns: ['id', 'content', 'createdAt']
114
- }
115
- }
116
- });
117
-
118
- const { sql, params } = builder.compile(new MySqlDialect());
119
- const rows = await db.execute(sql, params);
120
-
121
- // Automatically hydrates to:
122
- // {
123
- // id: 1,
124
- // name: 'John',
125
- // posts: [
126
- // {
127
- // id: 1,
128
- // title: 'First Post',
129
- // comments: [...]
130
- // }
131
- // ]
132
- // }
133
- const hydrated = hydrateRows(rows, builder.getHydrationPlan());
134
- ```
135
-
136
- ### Multi-Dialect Support
137
- Compile the same query to different SQL dialects:
138
-
139
- ```typescript
140
- const query = new SelectQueryBuilder(users)
141
- .selectRaw('*')
142
- .where(eq(users.columns.id, 1))
143
- .limit(10);
144
-
145
- // MySQL
146
- const mysql = query.compile(new MySqlDialect());
147
- // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
148
-
149
- // SQLite
150
- const sqlite = query.compile(new SQLiteDialect());
151
- // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
152
-
153
- // SQL Server
154
- const mssql = query.compile(new MSSQLDialect());
155
- // SQL: SELECT TOP 10 * FROM users WHERE id = @p1
156
- ```
17
+ - **Declarative Schema Definition**: Define your database structure in TypeScript with full type inference.
18
+ - **Rich Query Building**: A fluent API to build simple and complex queries.
19
+ - **Advanced SQL Features**: Support for CTEs, window functions, subqueries, and more.
20
+ - **Relation Hydration**: Automatically transform flat database rows into nested JavaScript objects.
21
+ - **Multi-Dialect Support**: Compile the same query to different SQL dialects.
157
22
 
158
23
  ## Installation
159
24
 
@@ -168,382 +33,61 @@ yarn add metal-orm
168
33
  pnpm add metal-orm
169
34
  ```
170
35
 
171
- ## Quick Start
36
+ ## Database Drivers
37
+
38
+ MetalORM compiles SQL; it does not bundle a database driver. Install one that matches your database, compile your query with the matching dialect, and hand the SQL + parameters to the driver.
39
+
40
+ | Dialect | Driver | Install |
41
+ | --- | --- | --- |
42
+ | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
43
+ | SQLite | `sqlite3` | `npm install sqlite3` |
44
+ | SQL Server | `tedious` | `npm install tedious` |
172
45
 
173
- Here's a complete example to get you started:
46
+ After installing the driver, pick the matching dialect implementation before compiling the query (`MySqlDialect`, `SQLiteDialect`, `MSSQLDialect`), and execute the resulting SQL string with the driver of your choice.
47
+
48
+ ## Quick Start
174
49
 
175
50
  ```typescript
51
+ import mysql from 'mysql2/promise';
176
52
  import {
177
53
  defineTable,
178
54
  col,
179
- hasMany,
180
55
  SelectQueryBuilder,
181
56
  eq,
182
- count,
183
57
  hydrateRows,
184
- MySqlDialect
58
+ MySqlDialect,
185
59
  } from 'metal-orm';
186
60
 
187
- // 1. Define your schema
188
- const posts = defineTable(
189
- 'posts',
190
- {
191
- id: col.int().primaryKey(),
192
- title: col.varchar(255).notNull(),
193
- content: col.text(),
194
- userId: col.int().notNull(),
195
- createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
196
- updatedAt: col.timestamp()
197
- }
198
- );
61
+ const users = defineTable('users', {
62
+ id: col.int().primaryKey(),
63
+ name: col.varchar(255).notNull(),
64
+ });
199
65
 
200
- const users = defineTable(
201
- 'users',
202
- {
203
- id: col.int().primaryKey(),
204
- name: col.varchar(255).notNull(),
205
- email: col.varchar(255).unique(),
206
- createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
207
- },
208
- {
209
- posts: hasMany(posts, 'userId')
210
- }
211
- );
66
+ const connection = await mysql.createConnection({
67
+ host: 'localhost',
68
+ user: 'root',
69
+ database: 'test',
70
+ });
212
71
 
213
- // 2. Build your query
214
72
  const builder = new SelectQueryBuilder(users)
215
73
  .select({
216
74
  id: users.columns.id,
217
75
  name: users.columns.name,
218
- email: users.columns.email,
219
- totalPosts: count(posts.columns.id)
220
76
  })
221
- .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
222
- .groupBy(users.columns.id, users.columns.name, users.columns.email)
223
- .orderBy(count(posts.columns.id), 'DESC')
224
- .limit(20)
225
- .include('posts', {
226
- columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt]
227
- });
77
+ .where(eq(users.columns.id, 1));
228
78
 
229
- // 3. Compile to SQL
230
79
  const dialect = new MySqlDialect();
231
80
  const { sql, params } = builder.compile(dialect);
232
-
233
- // 4. Execute and hydrate
234
- const rows = await connection.execute(sql, params);
235
- const hydrated = hydrateRows(rows, builder.getHydrationPlan());
81
+ const [rows] = await connection.execute(sql, params);
82
+ const hydrated = hydrateRows(rows as Record<string, unknown>[], builder.getHydrationPlan());
236
83
 
237
84
  console.log(hydrated);
238
- // [
239
- // {
240
- // id: 1,
241
- // name: 'John Doe',
242
- // email: 'john@example.com',
243
- // totalPosts: 15,
244
- // posts: [
245
- // {
246
- // id: 101,
247
- // title: 'Latest Post',
248
- // createdAt: '2023-05-15T10:00:00.000Z'
249
- // },
250
- // // ... more posts
251
- // ]
252
- // }
253
- // // ... more users
254
- // ]
255
- ```
256
-
257
- ## Advanced Expression Helpers
258
-
259
- ### JSON filters and comparisons
260
- ```typescript
261
- const userData = defineTable('user_data', {
262
- id: col.int().primaryKey(),
263
- userId: col.int().notNull(),
264
- preferences: col.json().notNull()
265
- });
266
-
267
- const jsonQuery = new SelectQueryBuilder(userData)
268
- .select({
269
- id: userData.columns.id,
270
- userId: userData.columns.userId,
271
- theme: jsonPath(userData.columns.preferences, '$.theme')
272
- })
273
- .where(and(
274
- eq(jsonPath(userData.columns.preferences, '$.theme'), 'dark'),
275
- inList(userData.columns.userId, [1, 2, 3])
276
- ));
277
- ```
278
-
279
- ### CASE expressions and window helpers
280
- ```typescript
281
- const tieredUsers = new SelectQueryBuilder(users)
282
- .select({
283
- id: users.columns.id,
284
- tier: caseWhen([
285
- { when: gt(count(posts.columns.id), 10), then: 'power user' }
286
- ], 'regular')
287
- })
288
- .groupBy(users.columns.id);
289
-
290
- const rankedPosts = new SelectQueryBuilder(posts)
291
- .select({
292
- id: posts.columns.id,
293
- createdAt: posts.columns.createdAt,
294
- rank: windowFunction('RANK', [], [posts.columns.userId], [
295
- { column: posts.columns.createdAt, direction: 'DESC' }
296
- ])
297
- });
298
- ```
299
-
300
- ## Performance Considerations
301
-
302
- ### Query Optimization
303
- - **Use `.select()` explicitly** instead of `select('*')` to only fetch needed columns
304
- - **Leverage CTEs** for complex queries to improve readability and sometimes performance
305
- - **Use indexes** on frequently queried columns and join conditions
306
- - **Reuse compiled hydration plans** when transforming rows to avoid repeating row reconstruction
307
-
308
- ### Caching Strategies
309
- ```typescript
310
- // Implement query result caching
311
- const cache = new Map<string, any>();
312
-
313
- async function getUserWithPosts(userId: number) {
314
- const cacheKey = `user:${userId}:with-posts`;
315
-
316
- if (cache.has(cacheKey)) {
317
- return cache.get(cacheKey);
318
- }
319
-
320
- const builder = new SelectQueryBuilder(users)
321
- .selectRaw('*')
322
- .where(eq(users.columns.id, userId))
323
- .include('posts');
324
-
325
- const { sql, params } = builder.compile(dialect);
326
- const rows = await db.execute(sql, params);
327
- const result = hydrateRows(rows, builder.getHydrationPlan());
328
-
329
- cache.set(cacheKey, result);
330
- return result;
331
- }
332
85
  ```
333
86
 
334
- ## Comparison with Other ORMs
335
-
336
- | Feature | MetalORM | TypeORM | Prisma | Knex |
337
- |------------------------|------------------------|------------------|-------------------|------------------|
338
- | Type Safety | ✅ Full TypeScript | ✅ Good | ✅ Excellent | ❌ Limited |
339
- | SQL Generation | ✅ Deterministic | ❌ ORM-style | ✅ Good | ✅ Good |
340
- | AST Inspection | ✅ Full access | ❌ No | ❌ No | ❌ No |
341
- | Multi-Dialect | ✅ MySQL/SQLite/MSSQL | ✅ Many | ✅ Many | ✅ Many |
342
- | Relation Hydration | ✅ Automatic | ✅ Manual | ✅ Automatic | ❌ Manual |
343
- | Query Builder | ✅ Rich | ✅ Good | ❌ Limited | ✅ Good |
344
- | Learning Curve | ⚠️ Moderate | ⚠️ Moderate | ✅ Low | ⚠️ Moderate |
345
- | Bundle Size | ✅ Small | ⚠️ Medium | ⚠️ Medium | ✅ Small |
346
-
347
- ## Migration Guide
348
-
349
- ### From Knex
350
- ```typescript
351
- // Before (Knex)
352
- const users = await knex('users')
353
- .select('users.*', knex.raw('COUNT(posts.id) as post_count'))
354
- .leftJoin('posts', 'users.id', 'posts.user_id')
355
- .groupBy('users.id')
356
- .orderBy('post_count', 'desc');
357
-
358
- // After (MetalORM)
359
- const users = defineTable('users', { /* ... */ });
360
- const posts = defineTable('posts', { /* ... */ });
361
-
362
- const result = await new SelectQueryBuilder(users)
363
- .select({
364
- ...allUserColumns,
365
- postCount: count(posts.columns.id)
366
- })
367
- .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
368
- .groupBy(users.columns.id)
369
- .orderBy(count(posts.columns.id), 'DESC')
370
- .compile(dialect);
371
- ```
372
-
373
- ### From TypeORM
374
- ```typescript
375
- // Before (TypeORM)
376
- const users = await userRepository
377
- .createQueryBuilder('user')
378
- .leftJoinAndSelect('user.posts', 'post')
379
- .where('user.name LIKE :name', { name: '%John%' })
380
- .orderBy('user.createdAt', 'DESC')
381
- .getMany();
382
-
383
- // After (MetalORM)
384
- const result = await new SelectQueryBuilder(users)
385
- .selectRaw('*')
386
- .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
387
- .where(like(users.columns.name, '%John%'))
388
- .orderBy(users.columns.createdAt, 'DESC')
389
- .include('posts')
390
- .compile(dialect);
391
- ```
392
-
393
- ## Project Structure
394
-
395
- ```
396
- metal-orm/
397
- ├── src/
398
- │ ├── schema/ # Table/column/relation definitions
399
- │ ├── builder/ # Query builder and managers
400
- │ ├── ast/ # AST nodes and expression builders
401
- │ ├── dialect/ # SQL dialect implementations
402
- │ ├── runtime/ # Hydration and utility functions
403
- │ ├── codegen/ # Code generation helpers
404
- │ └── playground/ # Interactive playground
405
- ├── tests/ # Test suites
406
- ├── dist/ # Compiled output
407
- └── playground/ # Vite-based playground app
408
- ```
409
-
410
- ## API Documentation
411
-
412
- ### Core Classes
413
- - `SelectQueryBuilder` - Main query builder class
414
- - `MySqlDialect` / `SQLiteDialect` / `MSSQLDialect` - SQL dialect compilers
415
- - `HydrationManager` - Handles relation hydration logic
416
-
417
- ### Key Functions
418
- - `defineTable()` - Define database tables
419
- - `col.*()` - Column type definitions
420
- - `hasMany()` / `belongsTo()` - Relation definitions
421
- - `eq()`, `and()`, `or()`, etc. - Expression builders
422
- - `hydrateRows()` - Transform flat rows to nested objects
423
-
424
- ### Utility Functions
425
- - `count()`, `sum()`, `avg()` - Aggregate functions
426
- - `like()`, `between()`, `inList()`, `notInList()` - Comparison operators
427
- - `jsonPath()` - JSON extraction
428
- - `caseWhen()`, `exists()`, `notExists()` - Conditional and subquery helpers
429
- - `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `windowFunction()` - Window function helpers
430
-
431
- ## Project Commands
432
-
433
- ```bash
434
- # Build the project
435
- npm run build
436
-
437
- # Type checking
438
- npm run check
439
-
440
- # Run tests
441
- npm run test
442
-
443
- # Interactive test UI
444
- npm run test:ui
445
-
446
- # Launch playground
447
- npm run playground
448
-
449
- # Clean build
450
- npm run clean
451
- ```
452
-
453
- ## Development Setup
454
-
455
- 1. **Clone the repository:**
456
- ```bash
457
- git clone https://github.com/celsowm/metal-orm.git
458
- cd metal-orm
459
- ```
460
-
461
- 2. **Install dependencies:**
462
- ```bash
463
- npm install
464
- ```
465
-
466
- 3. **Run the playground:**
467
- ```bash
468
- npm run playground
469
- ```
470
- This launches a Vite-based UI where you can experiment with queries.
471
-
472
- 4. **Run tests:**
473
- ```bash
474
- npm test
475
- ```
476
-
477
87
  ## Contributing
478
88
 
479
- We welcome contributions! Here's how you can help:
480
-
481
- ### Getting Started
482
- 1. Fork the repository
483
- 2. Clone your fork and install dependencies
484
- 3. Run `npm run playground` to see MetalORM in action
485
-
486
- ### Development Workflow
487
- 1. **Write tests first** - Add test cases in the `tests/` directory
488
- 2. **Implement features** - Follow existing code patterns
489
- 3. **Update documentation** - Keep docs in sync with changes
490
- 4. **Run all checks** - Ensure tests pass and formatting is correct
491
-
492
- ### Code Guidelines
493
- - Follow existing TypeScript patterns and conventions
494
- - Keep functions small and focused
495
- - Add JSDoc comments for public APIs
496
- - Write comprehensive tests for new features
497
- - Maintain 100% type coverage
498
-
499
- ### Pull Request Process
500
- 1. Create a feature branch from `main`
501
- 2. Implement your changes with tests
502
- 3. Update documentation as needed
503
- 4. Ensure all tests pass (`npm test`)
504
- 5. Submit PR with clear description of changes
505
-
506
- ### Documentation Standards
507
- - Update `ROADMAP.md` for major feature plans
508
- - Add `SOLID_*.md` files for architectural decisions
509
- - Keep API documentation in sync with code changes
510
- - Add examples for new features
89
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
511
90
 
512
91
  ## License
513
92
 
514
- MetalORM is [MIT licensed](https://github.com/celsowm/metal-orm/blob/main/LICENSE).
515
-
516
- ## Support
517
-
518
- - **Issues**: Report bugs or request features on [GitHub Issues](https://github.com/celsowm/metal-orm/issues)
519
- - **Discussions**: Join the conversation on [GitHub Discussions](https://github.com/celsowm/metal-orm/discussions)
520
- - **Contributing**: See the [Contributing](#contributing) section above
521
-
522
- ## Roadmap
523
-
524
- Check out our [ROADMAP.md](ROADMAP.md) for upcoming features and long-term vision.
525
-
526
- ## Examples
527
-
528
- See the [playground scenarios](playground/src/data/scenarios/) for comprehensive examples covering:
529
- - Basic CRUD operations
530
- - Complex joins and relations
531
- - Aggregations and window functions
532
- - CTEs and subqueries
533
- - JSON operations
534
- - And much more!
535
-
536
- ## Who's Using MetalORM?
537
-
538
- MetalORM is used in production by various projects. If you're using MetalORM, consider adding your project to this list!
539
-
540
- ## Acknowledgements
541
-
542
- Special thanks to all contributors and the open-source community for their support and feedback.
543
-
544
- ---
545
-
546
- **Star this repo if you find it useful!** ⭐
547
-
548
- [GitHub Repository](https://github.com/celsowm/metal-orm) |
549
- [npm package](https://www.npmjs.com/package/metal-orm)
93
+ MetalORM is [MIT licensed](LICENSE).
@@ -0,0 +1,85 @@
1
+ # Advanced Features
2
+
3
+ MetalORM supports a wide range of advanced SQL features to handle complex scenarios.
4
+
5
+ ## Common Table Expressions (CTEs)
6
+
7
+ CTEs help organize complex queries. You can define a CTE using a `SelectQueryBuilder` and reference it in the main query.
8
+
9
+ ```typescript
10
+ const since = new Date();
11
+ since.setDate(since.getDate() - 30);
12
+
13
+ const activeUsers = new SelectQueryBuilder(users)
14
+ .selectRaw('*')
15
+ .where(gt(users.columns.lastLogin, since))
16
+ .as('active_users');
17
+
18
+ const query = new SelectQueryBuilder(activeUsers)
19
+ .with(activeUsers)
20
+ .selectRaw('*')
21
+ .where(eq(activeUsers.columns.id, 1));
22
+ ```
23
+
24
+ ## Window Functions
25
+
26
+ MetalORM provides helpers for window functions like `RANK()`, `ROW_NUMBER()`, etc.
27
+
28
+ ```typescript
29
+ const rankedPosts = new SelectQueryBuilder(posts)
30
+ .select({
31
+ id: posts.columns.id,
32
+ title: posts.columns.title,
33
+ rank: windowFunction('RANK', [], [posts.columns.userId], [
34
+ { column: posts.columns.createdAt, direction: 'DESC' }
35
+ ])
36
+ });
37
+ ```
38
+
39
+ ## Subqueries and EXISTS
40
+
41
+ You can use subqueries and `EXISTS` to perform complex checks.
42
+
43
+ ```typescript
44
+ const usersWithPosts = new SelectQueryBuilder(users)
45
+ .selectRaw('*')
46
+ .where(exists(
47
+ new SelectQueryBuilder(posts)
48
+ .selectRaw('1')
49
+ .where(eq(posts.columns.userId, users.columns.id))
50
+ ));
51
+ ```
52
+
53
+ ## JSON Operations
54
+
55
+ MetalORM provides helpers for working with JSON data.
56
+
57
+ ```typescript
58
+ const userData = defineTable('user_data', {
59
+ id: col.int().primaryKey(),
60
+ userId: col.int().notNull(),
61
+ preferences: col.json().notNull()
62
+ });
63
+
64
+ const jsonQuery = new SelectQueryBuilder(userData)
65
+ .select({
66
+ id: userData.columns.id,
67
+ theme: jsonPath(userData.columns.preferences, '$.theme')
68
+ })
69
+ .where(eq(jsonPath(userData.columns.preferences, '$.theme'), 'dark'));
70
+ ```
71
+
72
+ ## CASE Expressions
73
+
74
+ You can use `caseWhen()` to create `CASE` expressions for conditional logic.
75
+
76
+ ```typescript
77
+ const tieredUsers = new SelectQueryBuilder(users)
78
+ .select({
79
+ id: users.columns.id,
80
+ tier: caseWhen([
81
+ { when: gt(count(posts.columns.id), 10), then: 'power user' }
82
+ ], 'regular')
83
+ })
84
+ .groupBy(users.columns.id);
85
+ ```
@@ -0,0 +1,22 @@
1
+ # API Reference
2
+
3
+ This section provides a reference for the core classes, key functions, and utility functions in MetalORM.
4
+
5
+ ### Core Classes
6
+ - `SelectQueryBuilder` - Main query builder class
7
+ - `MySqlDialect` / `SQLiteDialect` / `MSSQLDialect` - SQL dialect compilers
8
+ - `HydrationManager` - Handles relation hydration logic
9
+
10
+ ### Key Functions
11
+ - `defineTable()` - Define database tables
12
+ - `col.*()` - Column type definitions
13
+ - `hasMany()` / `belongsTo()` - Relation definitions
14
+ - `eq()`, `and()`, `or()`, etc. - Expression builders
15
+ - `hydrateRows()` - Transform flat rows to nested objects
16
+
17
+ ### Utility Functions
18
+ - `count()`, `sum()`, `avg()` - Aggregate functions
19
+ - `like()`, `between()`, `inList()`, `notInList()` - Comparison operators
20
+ - `jsonPath()` - JSON extraction
21
+ - `caseWhen()`, `exists()`, `notExists()` - Conditional and subquery helpers
22
+ - `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `windowFunction()` - Window function helpers
@@ -0,0 +1,104 @@
1
+ # Getting Started
2
+
3
+ This guide will help you get started with MetalORM.
4
+
5
+ ## Installation
6
+
7
+ You can install MetalORM using npm, yarn, or pnpm:
8
+
9
+ ```bash
10
+ # npm
11
+ npm install metal-orm
12
+
13
+ # yarn
14
+ yarn add metal-orm
15
+
16
+ # pnpm
17
+ pnpm add metal-orm
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ Here's a complete example to get you started:
23
+
24
+ ```typescript
25
+ import {
26
+ defineTable,
27
+ col,
28
+ hasMany,
29
+ SelectQueryBuilder,
30
+ eq,
31
+ count,
32
+ hydrateRows,
33
+ MySqlDialect
34
+ } from 'metal-orm';
35
+
36
+ // 1. Define your schema
37
+ const posts = defineTable(
38
+ 'posts',
39
+ {
40
+ id: col.int().primaryKey(),
41
+ title: col.varchar(255).notNull(),
42
+ content: col.text(),
43
+ userId: col.int().notNull(),
44
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
45
+ updatedAt: col.timestamp()
46
+ }
47
+ );
48
+
49
+ const users = defineTable(
50
+ 'users',
51
+ {
52
+ id: col.int().primaryKey(),
53
+ name: col.varchar(255).notNull(),
54
+ email: col.varchar(255).unique(),
55
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
56
+ },
57
+ {
58
+ posts: hasMany(posts, 'userId')
59
+ }
60
+ );
61
+
62
+ // 2. Build your query
63
+ const builder = new SelectQueryBuilder(users)
64
+ .select({
65
+ id: users.columns.id,
66
+ name: users.columns.name,
67
+ email: users.columns.email,
68
+ totalPosts: count(posts.columns.id)
69
+ })
70
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
71
+ .groupBy(users.columns.id, users.columns.name, users.columns.email)
72
+ .orderBy(count(posts.columns.id), 'DESC')
73
+ .limit(20)
74
+ .include('posts', {
75
+ columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt]
76
+ });
77
+
78
+ // 3. Compile to SQL
79
+ const dialect = new MySqlDialect();
80
+ const { sql, params } = builder.compile(dialect);
81
+
82
+ // 4. Execute and hydrate
83
+ const rows = await connection.execute(sql, params);
84
+ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
85
+
86
+ console.log(hydrated);
87
+ // [
88
+ // {
89
+ // id: 1,
90
+ // name: 'John Doe',
91
+ // email: 'john@example.com',
92
+ // totalPosts: 15,
93
+ // posts: [
94
+ // {
95
+ // id: 101,
96
+ // title: 'Latest Post',
97
+ // createdAt: '2023-05-15T10:00:00.000Z'
98
+ // },
99
+ // // ... more posts
100
+ // ]
101
+ // }
102
+ // // ... more users
103
+ // ]
104
+ ```
@@ -0,0 +1,41 @@
1
+ # Relation Hydration
2
+
3
+ MetalORM can automatically transform flat database rows into nested JavaScript objects, making it easy to work with relational data.
4
+
5
+ ## Hydrating Results
6
+
7
+ The `hydrateRows()` function takes an array of database rows and a hydration plan to produce nested objects. The hydration plan is generated by the `SelectQueryBuilder` when you use the `include()` method.
8
+
9
+ ```typescript
10
+ const builder = new SelectQueryBuilder(users)
11
+ .selectRaw('*')
12
+ .include('posts', {
13
+ columns: ['id', 'title', 'content'],
14
+ include: {
15
+ comments: {
16
+ columns: ['id', 'content', 'createdAt']
17
+ }
18
+ }
19
+ });
20
+
21
+ const { sql, params } = builder.compile(new MySqlDialect());
22
+ const rows = await db.execute(sql, params);
23
+
24
+ // Automatically hydrates to:
25
+ // {
26
+ // id: 1,
27
+ // name: 'John',
28
+ // posts: [
29
+ // {
30
+ // id: 1,
31
+ // title: 'First Post',
32
+ // comments: [...]
33
+ // }
34
+ // ]
35
+ // }
36
+ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
37
+ ```
38
+
39
+ ## How it Works
40
+
41
+ The `SelectQueryBuilder` analyzes the `include()` configuration and generates a `HydrationPlan`. This plan contains the necessary information to map the flat rows to a nested structure, including relation details and column aliases. The `hydrateRows()` function then uses this plan to efficiently process the result set.
package/docs/index.md ADDED
@@ -0,0 +1,31 @@
1
+ # Introduction to MetalORM
2
+
3
+ MetalORM is a TypeScript-first SQL query builder designed for developers who want the power of raw SQL with the convenience of a modern ORM. It keeps SQL generation deterministic (CTEs, aggregates, window functions, EXISTS/subqueries) while letting you introspect the AST or reuse builders inside larger queries.
4
+
5
+ ## Philosophy
6
+
7
+ MetalORM follows these core principles:
8
+
9
+ - **Type Safety First**: Leverage TypeScript to catch errors at compile time
10
+ - **SQL Transparency**: Generate predictable, readable SQL that you can inspect
11
+ - **Composition Over Configuration**: Build complex queries by composing simple parts
12
+ - **Zero Magic**: Explicit operations with clear AST representation
13
+ - **Multi-Dialect Support**: Write once, compile to MySQL, SQLite, or SQL Server
14
+
15
+ ## Features
16
+
17
+ - **Declarative Schema Definition**: Define your database structure in TypeScript with full type inference.
18
+ - **Rich Query Building**: A fluent API to build simple and complex queries.
19
+ - **Advanced SQL Features**: Support for CTEs, window functions, subqueries, and more.
20
+ - **Relation Hydration**: Automatically transform flat database rows into nested JavaScript objects.
21
+ - **Multi-Dialect Support**: Compile the same query to different SQL dialects.
22
+
23
+ ## Table of Contents
24
+
25
+ - [Getting Started](./getting-started.md)
26
+ - [Schema Definition](./schema-definition.md)
27
+ - [Query Builder](./query-builder.md)
28
+ - [Advanced Features](./advanced-features.md)
29
+ - [Hydration](./hydration.md)
30
+ - [Multi-Dialect Support](./multi-dialect-support.md)
31
+ - [API Reference](./api-reference.md)
@@ -0,0 +1,34 @@
1
+ # Multi-Dialect Support
2
+
3
+ MetalORM is designed to be database-agnostic. You can write your queries once and compile them to different SQL dialects.
4
+
5
+ ## Compiling Queries
6
+
7
+ The `compile()` method on the `SelectQueryBuilder` takes a dialect instance and returns the compiled SQL and parameters.
8
+
9
+ ```typescript
10
+ const query = new SelectQueryBuilder(users)
11
+ .selectRaw('*')
12
+ .where(eq(users.columns.id, 1))
13
+ .limit(10);
14
+
15
+ // MySQL
16
+ const mysql = query.compile(new MySqlDialect());
17
+ // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
18
+
19
+ // SQLite
20
+ const sqlite = query.compile(new SQLiteDialect());
21
+ // SQL: SELECT * FROM users WHERE id = ? LIMIT ?
22
+
23
+ // SQL Server
24
+ const mssql = query.compile(new MSSQLDialect());
25
+ // SQL: SELECT TOP 10 * FROM users WHERE id = @p1
26
+ ```
27
+
28
+ ## Supported Dialects
29
+
30
+ - **MySQL**: `MySqlDialect`
31
+ - **SQLite**: `SQLiteDialect`
32
+ - **SQL Server**: `MSSQLDialect`
33
+
34
+ Each dialect handles the specific syntax and parameterization of the target database.
@@ -0,0 +1,75 @@
1
+ # Query Builder
2
+
3
+ MetalORM's query builder provides a fluent and expressive API for constructing SQL queries.
4
+
5
+ ## Selecting Data
6
+
7
+ The `SelectQueryBuilder` is the main entry point for building `SELECT` queries.
8
+
9
+ ### Basic Selections
10
+
11
+ You can select all columns using `selectRaw('*')` or specify columns using `select()`:
12
+
13
+ ```typescript
14
+ // Select all columns
15
+ const query = new SelectQueryBuilder(users).selectRaw('*');
16
+
17
+ // Select specific columns
18
+ const query = new SelectQueryBuilder(users).select({
19
+ id: users.columns.id,
20
+ name: users.columns.name,
21
+ });
22
+ ```
23
+
24
+ ### Joins
25
+
26
+ You can join tables using `leftJoin`, `innerJoin`, `rightJoin`, etc.
27
+
28
+ ```typescript
29
+ const query = new SelectQueryBuilder(users)
30
+ .select({
31
+ userId: users.columns.id,
32
+ postTitle: posts.columns.title,
33
+ })
34
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id));
35
+ ```
36
+
37
+ ### Filtering
38
+
39
+ You can filter results using the `where()` method with expression helpers:
40
+
41
+ ```typescript
42
+ const query = new SelectQueryBuilder(users)
43
+ .selectRaw('*')
44
+ .where(and(
45
+ like(users.columns.name, '%John%'),
46
+ gt(users.columns.createdAt, new Date('2023-01-01'))
47
+ ));
48
+ ```
49
+
50
+ ### Aggregation
51
+
52
+ You can use aggregate functions like `count()`, `sum()`, `avg()`, etc., and group the results.
53
+
54
+ ```typescript
55
+ const query = new SelectQueryBuilder(users)
56
+ .select({
57
+ userId: users.columns.id,
58
+ postCount: count(posts.columns.id),
59
+ })
60
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
61
+ .groupBy(users.columns.id)
62
+ .having(gt(count(posts.columns.id), 5));
63
+ ```
64
+
65
+ ### Ordering and Pagination
66
+
67
+ You can order the results using `orderBy()` and paginate using `limit()` and `offset()`.
68
+
69
+ ```typescript
70
+ const query = new SelectQueryBuilder(posts)
71
+ .selectRaw('*')
72
+ .orderBy(posts.columns.createdAt, 'DESC')
73
+ .limit(10)
74
+ .offset(20);
75
+ ```
@@ -0,0 +1,61 @@
1
+ # Schema Definition
2
+
3
+ MetalORM allows you to define your database schema in TypeScript, providing full type inference and a single source of truth for your data structures.
4
+
5
+ ## Defining Tables
6
+
7
+ You can define a table using the `defineTable` function. It takes the table name, a columns object, and an optional relations object.
8
+
9
+ ```typescript
10
+ import { defineTable, col } from 'metal-orm';
11
+
12
+ const users = defineTable('users', {
13
+ id: col.int().primaryKey(),
14
+ name: col.varchar(255).notNull(),
15
+ email: col.varchar(255).unique(),
16
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
17
+ });
18
+ ```
19
+
20
+ ## Column Types
21
+
22
+ MetalORM provides a variety of column types through the `col` object:
23
+
24
+ - `col.int()`: Integer
25
+ - `col.varchar(length)`: Variable-length string
26
+ - `col.text()`: Text
27
+ - `col.timestamp()`: Timestamp
28
+ - `col.json()`: JSON
29
+ - ...and more.
30
+
31
+ You can also chain modifiers to define column constraints:
32
+
33
+ - `.primaryKey()`: Marks the column as a primary key.
34
+ - `.notNull()`: Adds a `NOT NULL` constraint.
35
+ - `.unique()`: Adds a `UNIQUE` constraint.
36
+ - `.default(value)`: Sets a default value.
37
+
38
+ ## Relations
39
+
40
+ You can define relations between tables using `hasMany` and `belongsTo`:
41
+
42
+ ```typescript
43
+ import { defineTable, col, hasMany } from 'metal-orm';
44
+
45
+ const posts = defineTable('posts', {
46
+ id: col.int().primaryKey(),
47
+ title: col.varchar(255).notNull(),
48
+ userId: col.int().notNull(),
49
+ });
50
+
51
+ const users = defineTable(
52
+ 'users',
53
+ {
54
+ id: col.int().primaryKey(),
55
+ name: col.varchar(255).notNull(),
56
+ },
57
+ {
58
+ posts: hasMany(posts, 'userId'),
59
+ }
60
+ );
61
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -51,7 +51,8 @@ export type OrderDirection = (typeof ORDER_DIRECTIONS)[keyof typeof ORDER_DIRECT
51
51
  export const SUPPORTED_DIALECTS = {
52
52
  MYSQL: 'mysql',
53
53
  SQLITE: 'sqlite',
54
- MSSQL: 'mssql'
54
+ MSSQL: 'mssql',
55
+ POSTGRES: 'postgres'
55
56
  } as const;
56
57
 
57
58
  export type DialectName = (typeof SUPPORTED_DIALECTS)[keyof typeof SUPPORTED_DIALECTS];
@@ -0,0 +1,81 @@
1
+ import { CompilerContext, Dialect } from '../abstract';
2
+ import { SelectQueryNode } from '../../ast/query';
3
+ import { JsonPathNode } from '../../ast/expression';
4
+
5
+ export class PostgresDialect extends Dialect {
6
+ public constructor() {
7
+ super();
8
+ }
9
+
10
+ quoteIdentifier(id: string): string {
11
+ return `"${id}"`;
12
+ }
13
+
14
+ protected compileJsonPath(node: JsonPathNode): string {
15
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
16
+ // Postgres uses col->>'path' for text extraction
17
+ return `${col}->>'${node.path}'`;
18
+ }
19
+
20
+ protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
21
+ const columns = ast.columns.map(c => {
22
+ let expr = '';
23
+ if (c.type === 'Function') {
24
+ expr = this.compileOperand(c, ctx);
25
+ } else if (c.type === 'Column') {
26
+ expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
27
+ } else if (c.type === 'ScalarSubquery') {
28
+ expr = this.compileOperand(c, ctx);
29
+ } else if (c.type === 'WindowFunction') {
30
+ expr = this.compileOperand(c, ctx);
31
+ }
32
+
33
+ if (c.alias) {
34
+ if (c.alias.includes('(')) return c.alias;
35
+ return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
36
+ }
37
+ return expr;
38
+ }).join(', ');
39
+
40
+ const distinct = ast.distinct ? 'DISTINCT ' : '';
41
+ const from = `${this.quoteIdentifier(ast.from.name)}`;
42
+
43
+ const joins = ast.joins.map(j => {
44
+ const table = this.quoteIdentifier(j.table.name);
45
+ const cond = this.compileExpression(j.condition, ctx);
46
+ return `${j.kind} JOIN ${table} ON ${cond}`;
47
+ }).join(' ');
48
+ const whereClause = this.compileWhere(ast.where, ctx);
49
+
50
+ const groupBy = ast.groupBy && ast.groupBy.length > 0
51
+ ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
52
+ : '';
53
+
54
+ const having = ast.having
55
+ ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
56
+ : '';
57
+
58
+ const orderBy = ast.orderBy && ast.orderBy.length > 0
59
+ ? ' ORDER BY ' + ast.orderBy.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(', ')
60
+ : '';
61
+
62
+ const limit = ast.limit ? ` LIMIT ${ast.limit}` : '';
63
+ const offset = ast.offset ? ` OFFSET ${ast.offset}` : '';
64
+
65
+ const ctes = ast.ctes && ast.ctes.length > 0
66
+ ? (() => {
67
+ const hasRecursive = ast.ctes.some(cte => cte.recursive);
68
+ const prefix = hasRecursive ? 'WITH RECURSIVE ' : 'WITH ';
69
+ const cteDefs = ast.ctes.map(cte => {
70
+ const name = this.quoteIdentifier(cte.name);
71
+ const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
72
+ const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, '');
73
+ return `${name}${cols} AS (${query})`;
74
+ }).join(', ');
75
+ return prefix + cteDefs + ' ';
76
+ })()
77
+ : '';
78
+
79
+ return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
80
+ }
81
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SelectQueryBuilder } from '../src/builder/select';
3
+ import { PostgresDialect } from '../src/dialect/postgres';
4
+ import { Users } from '../src/playground/features/playground/data/schema';
5
+ import { jsonPath, eq } from '../src/ast/expression';
6
+
7
+ describe('PostgresDialect', () => {
8
+ it('should compile a simple select', () => {
9
+ const query = new SelectQueryBuilder(Users).selectRaw('*');
10
+ const dialect = new PostgresDialect();
11
+ const compiled = query.compile(dialect);
12
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users";');
13
+ });
14
+
15
+ it('should compile a select with a where clause', () => {
16
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(Users.columns.id, 1));
17
+ const dialect = new PostgresDialect();
18
+ const compiled = query.compile(dialect);
19
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."id" = ?;');
20
+ expect(compiled.params).toEqual([1]);
21
+ });
22
+
23
+ it('should compile a select with a json path', () => {
24
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(jsonPath(Users.columns.settings, '$.first'), 'John'));
25
+ const dialect = new PostgresDialect();
26
+ const compiled = query.compile(dialect);
27
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."settings"->>\'$.first\' = ?;');
28
+ expect(compiled.params).toEqual(['John']);
29
+ });
30
+ });
@@ -3,6 +3,7 @@ import { SelectQueryBuilder } from '../src/builder/select';
3
3
  import { SqliteDialect } from '../src/dialect/sqlite';
4
4
  import { MySqlDialect } from '../src/dialect/mysql';
5
5
  import { SqlServerDialect } from '../src/dialect/mssql';
6
+ import { PostgresDialect } from '../src/dialect/postgres';
6
7
  import { TableDef } from '../src/schema/table';
7
8
  import { rowNumber, rank, denseRank, lag, lead, ntile, firstValue, lastValue, windowFunction } from '../src/ast/expression';
8
9
 
@@ -14,6 +15,7 @@ describe('Window Function Support', () => {
14
15
  const sqlite = new SqliteDialect();
15
16
  const mysql = new MySqlDialect();
16
17
  const mssql = new SqlServerDialect();
18
+ const postgres = new PostgresDialect();
17
19
 
18
20
  it('should generate ROW_NUMBER() window function', () => {
19
21
  const users = table('users');
@@ -28,10 +30,12 @@ describe('Window Function Support', () => {
28
30
  const expectedSqlite = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
29
31
  const expectedMysql = 'SELECT `users`.`id` AS `id`, `users`.`name` AS `name`, ROW_NUMBER() OVER () AS `row_num` FROM `users`;';
30
32
  const expectedMssql = 'SELECT [users].[id] AS [id], [users].[name] AS [name], ROW_NUMBER() OVER () AS [row_num] FROM [users];';
33
+ const expectedPostgres = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
31
34
 
32
35
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
33
36
  expect(query.toSql(mysql)).toBe(expectedMysql);
34
37
  expect(query.toSql(mssql)).toBe(expectedMssql);
38
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
35
39
  });
36
40
 
37
41
  it('should generate RANK() with PARTITION BY and ORDER BY', () => {
@@ -48,10 +52,12 @@ describe('Window Function Support', () => {
48
52
  const expectedSqlite = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
49
53
  const expectedMysql = 'SELECT `orders`.`id` AS `id`, `orders`.`customer_id` AS `customer_id`, `orders`.`amount` AS `amount`, RANK() OVER (PARTITION BY `orders`.`customer_id` ORDER BY `orders`.`amount` DESC) AS `rank` FROM `orders`;';
50
54
  const expectedMssql = 'SELECT [orders].[id] AS [id], [orders].[customer_id] AS [customer_id], [orders].[amount] AS [amount], RANK() OVER (PARTITION BY [orders].[customer_id] ORDER BY [orders].[amount] DESC) AS [rank] FROM [orders];';
55
+ const expectedPostgres = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
51
56
 
52
57
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
53
58
  expect(query.toSql(mysql)).toBe(expectedMysql);
54
59
  expect(query.toSql(mssql)).toBe(expectedMssql);
60
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
55
61
  });
56
62
 
57
63
  it('should generate LAG function with offset and default value', () => {
@@ -67,10 +73,12 @@ describe('Window Function Support', () => {
67
73
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
68
74
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LAG(`sales`.`amount`, ?, ?) OVER () AS `prev_amount` FROM `sales`;';
69
75
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LAG([sales].[amount], @p1, @p2) OVER () AS [prev_amount] FROM [sales];';
76
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
70
77
 
71
78
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
72
79
  expect(query.toSql(mysql)).toBe(expectedMysql);
73
80
  expect(query.toSql(mssql)).toBe(expectedMssql);
81
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
74
82
  });
75
83
 
76
84
  it('should generate LEAD function', () => {
@@ -86,10 +94,12 @@ describe('Window Function Support', () => {
86
94
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
87
95
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LEAD(`sales`.`amount`, ?) OVER () AS `next_amount` FROM `sales`;';
88
96
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LEAD([sales].[amount], @p1) OVER () AS [next_amount] FROM [sales];';
97
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
89
98
 
90
99
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
91
100
  expect(query.toSql(mysql)).toBe(expectedMysql);
92
101
  expect(query.toSql(mssql)).toBe(expectedMssql);
102
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
93
103
  });
94
104
 
95
105
  it('should generate window function with both PARTITION BY and ORDER BY', () => {
@@ -107,10 +117,12 @@ describe('Window Function Support', () => {
107
117
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
108
118
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`department` AS `department`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER (PARTITION BY `employees`.`department` ORDER BY `employees`.`salary` DESC) AS `dept_rank` FROM `employees`;';
109
119
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[department] AS [department], [employees].[salary] AS [salary], ROW_NUMBER() OVER (PARTITION BY [employees].[department] ORDER BY [employees].[salary] DESC) AS [dept_rank] FROM [employees];';
120
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
110
121
 
111
122
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
112
123
  expect(query.toSql(mysql)).toBe(expectedMysql);
113
124
  expect(query.toSql(mssql)).toBe(expectedMssql);
125
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
114
126
  });
115
127
 
116
128
  it('should generate multiple window functions in one query', () => {
@@ -129,9 +141,11 @@ describe('Window Function Support', () => {
129
141
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
130
142
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER () AS `row_num`, RANK() OVER () AS `rank`, DENSE_RANK() OVER () AS `dense_rank` FROM `employees`;';
131
143
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[salary] AS [salary], ROW_NUMBER() OVER () AS [row_num], RANK() OVER () AS [rank], DENSE_RANK() OVER () AS [dense_rank] FROM [employees];';
144
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
132
145
 
133
146
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
134
147
  expect(query.toSql(mysql)).toBe(expectedMysql);
135
148
  expect(query.toSql(mssql)).toBe(expectedMssql);
149
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
136
150
  });
137
151
  });