metal-orm 1.0.0 → 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.
- package/.github/workflows/publish-metal-orm.yml +38 -0
- package/README.md +93 -30
- package/docs/advanced-features.md +85 -0
- package/docs/api-reference.md +22 -0
- package/docs/getting-started.md +104 -0
- package/docs/hydration.md +41 -0
- package/docs/index.md +31 -0
- package/docs/multi-dialect-support.md +34 -0
- package/docs/query-builder.md +75 -0
- package/docs/schema-definition.md +61 -0
- package/package.json +2 -2
- package/src/constants/sql.ts +2 -1
- package/src/dialect/postgres/index.ts +81 -0
- package/tests/postgres.test.ts +30 -0
- package/tests/window-function.test.ts +14 -0
|
@@ -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
|
@@ -1,30 +1,93 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
# MetalORM
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/metal-orm)
|
|
4
|
+
[](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**A TypeScript-first SQL query builder with schema-driven AST, hydrated relations, and multi-dialect compilation.**
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
For detailed information and API reference, please visit our [full documentation](docs/index.md).
|
|
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
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# npm
|
|
27
|
+
npm install metal-orm
|
|
28
|
+
|
|
29
|
+
# yarn
|
|
30
|
+
yarn add metal-orm
|
|
31
|
+
|
|
32
|
+
# pnpm
|
|
33
|
+
pnpm add metal-orm
|
|
34
|
+
```
|
|
35
|
+
|
|
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` |
|
|
45
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import mysql from 'mysql2/promise';
|
|
52
|
+
import {
|
|
53
|
+
defineTable,
|
|
54
|
+
col,
|
|
55
|
+
SelectQueryBuilder,
|
|
56
|
+
eq,
|
|
57
|
+
hydrateRows,
|
|
58
|
+
MySqlDialect,
|
|
59
|
+
} from 'metal-orm';
|
|
60
|
+
|
|
61
|
+
const users = defineTable('users', {
|
|
62
|
+
id: col.int().primaryKey(),
|
|
63
|
+
name: col.varchar(255).notNull(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const connection = await mysql.createConnection({
|
|
67
|
+
host: 'localhost',
|
|
68
|
+
user: 'root',
|
|
69
|
+
database: 'test',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const builder = new SelectQueryBuilder(users)
|
|
73
|
+
.select({
|
|
74
|
+
id: users.columns.id,
|
|
75
|
+
name: users.columns.name,
|
|
76
|
+
})
|
|
77
|
+
.where(eq(users.columns.id, 1));
|
|
78
|
+
|
|
79
|
+
const dialect = new MySqlDialect();
|
|
80
|
+
const { sql, params } = builder.compile(dialect);
|
|
81
|
+
const [rows] = await connection.execute(sql, params);
|
|
82
|
+
const hydrated = hydrateRows(rows as Record<string, unknown>[], builder.getHydrationPlan());
|
|
83
|
+
|
|
84
|
+
console.log(hydrated);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Contributing
|
|
88
|
+
|
|
89
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
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
package/src/constants/sql.ts
CHANGED
|
@@ -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
|
});
|