metal-orm 1.0.5 → 1.0.7
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 +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +78 -13
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +38 -18
- package/src/builder/hydration-planner.ts +74 -74
- package/src/builder/select.ts +427 -395
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/index.ts +16 -8
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +10 -10
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +99 -49
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +82 -58
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/orm-runtime.test.ts +254 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# DML Operations
|
|
2
|
+
|
|
3
|
+
MetalORM provides comprehensive support for Data Manipulation Language (DML) operations including INSERT, UPDATE, and DELETE queries.
|
|
4
|
+
|
|
5
|
+
## INSERT Operations
|
|
6
|
+
|
|
7
|
+
The `InsertQueryBuilder` allows you to insert data into your tables with full type safety.
|
|
8
|
+
|
|
9
|
+
### Basic Insert
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { InsertQueryBuilder } from 'metal-orm';
|
|
13
|
+
|
|
14
|
+
const query = new InsertQueryBuilder(users)
|
|
15
|
+
.values({
|
|
16
|
+
name: 'John Doe',
|
|
17
|
+
email: 'john@example.com',
|
|
18
|
+
createdAt: new Date()
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { sql, params } = query.compile(new MySqlDialect());
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Multi-row Insert
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const query = new InsertQueryBuilder(users)
|
|
28
|
+
.values([
|
|
29
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
30
|
+
{ name: 'Jane Smith', email: 'jane@example.com' }
|
|
31
|
+
]);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### RETURNING Clause
|
|
35
|
+
|
|
36
|
+
Some databases support returning inserted data:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const query = new InsertQueryBuilder(users)
|
|
40
|
+
.values({ name: 'John Doe', email: 'john@example.com' })
|
|
41
|
+
.returning(users.columns.id, users.columns.name);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## UPDATE Operations
|
|
45
|
+
|
|
46
|
+
The `UpdateQueryBuilder` provides a fluent API for updating records.
|
|
47
|
+
|
|
48
|
+
### Basic Update
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const query = new UpdateQueryBuilder(users)
|
|
52
|
+
.set({ name: 'John Updated', email: 'john.updated@example.com' })
|
|
53
|
+
.where(eq(users.columns.id, 1));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Conditional Update
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const query = new UpdateQueryBuilder(users)
|
|
60
|
+
.set({ lastLogin: new Date() })
|
|
61
|
+
.where(and(
|
|
62
|
+
eq(users.columns.id, 1),
|
|
63
|
+
isNull(users.columns.deletedAt)
|
|
64
|
+
));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### RETURNING Clause
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const query = new UpdateQueryBuilder(users)
|
|
71
|
+
.set({ status: 'active' })
|
|
72
|
+
.where(eq(users.columns.id, 1))
|
|
73
|
+
.returning(users.columns.id, users.columns.status);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## DELETE Operations
|
|
77
|
+
|
|
78
|
+
The `DeleteQueryBuilder` allows you to delete records with safety.
|
|
79
|
+
|
|
80
|
+
### Basic Delete
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const query = new DeleteQueryBuilder(users)
|
|
84
|
+
.where(eq(users.columns.id, 1));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Conditional Delete
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const query = new DeleteQueryBuilder(users)
|
|
91
|
+
.where(and(
|
|
92
|
+
eq(users.columns.status, 'inactive'),
|
|
93
|
+
lt(users.columns.lastLogin, new Date('2023-01-01'))
|
|
94
|
+
));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### RETURNING Clause
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const query = new DeleteQueryBuilder(users)
|
|
101
|
+
.where(eq(users.columns.id, 1))
|
|
102
|
+
.returning(users.columns.id, users.columns.name);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Multi-Dialect Support
|
|
106
|
+
|
|
107
|
+
All DML operations support the same multi-dialect compilation as SELECT queries:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// MySQL
|
|
111
|
+
const mysqlResult = query.compile(new MySqlDialect());
|
|
112
|
+
|
|
113
|
+
// SQLite
|
|
114
|
+
const sqliteResult = query.compile(new SQLiteDialect());
|
|
115
|
+
|
|
116
|
+
// SQL Server
|
|
117
|
+
const mssqlResult = query.compile(new MSSQLDialect());
|
|
118
|
+
|
|
119
|
+
// PostgreSQL
|
|
120
|
+
const postgresResult = query.compile(new PostgresDialect());
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Best Practices
|
|
124
|
+
|
|
125
|
+
1. **Always use WHERE clauses**: For UPDATE and DELETE operations, always include WHERE clauses to avoid accidental mass updates/deletes.
|
|
126
|
+
|
|
127
|
+
2. **Use RETURNING for verification**: When supported by your database, use RETURNING clauses to verify what was affected.
|
|
128
|
+
|
|
129
|
+
3. **Batch operations**: For large datasets, consider batching INSERT operations to avoid parameter limits.
|
|
130
|
+
|
|
131
|
+
4. **Transaction safety**: Wrap DML operations in transactions when performing multiple related operations.
|
|
132
|
+
|
|
133
|
+
## Using the Unit of Work (optional)
|
|
134
|
+
|
|
135
|
+
If you're using `OrmContext`, you don't have to manually build `InsertQueryBuilder` / `UpdateQueryBuilder` / `DeleteQueryBuilder` for every change.
|
|
136
|
+
|
|
137
|
+
Instead, you can:
|
|
138
|
+
|
|
139
|
+
1. Load entities via the query builder + `execute(ctx)`.
|
|
140
|
+
2. Modify fields and relations in memory.
|
|
141
|
+
3. Call `ctx.saveChanges()` once.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const [user] = await new SelectQueryBuilder(users)
|
|
145
|
+
.select({ id: users.columns.id, name: users.columns.name })
|
|
146
|
+
.includeLazy('posts')
|
|
147
|
+
.where(eq(users.columns.id, 1))
|
|
148
|
+
.execute(ctx);
|
|
149
|
+
|
|
150
|
+
user.name = 'Updated Name';
|
|
151
|
+
user.posts.add({ title: 'New from runtime' });
|
|
152
|
+
|
|
153
|
+
await ctx.saveChanges();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Internally, MetalORM uses the same DML ASTs and dialect compilers described above to generate INSERT, UPDATE, DELETE, and pivot operations.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
This guide
|
|
3
|
+
This guide walks you through:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. Using MetalORM as a **simple query builder**.
|
|
6
|
+
2. Hydrating relations into nested objects.
|
|
7
|
+
3. Taking a first look at the **OrmContext** runtime.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## 1. Installation
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
# npm
|
|
11
12
|
npm install metal-orm
|
|
12
|
-
|
|
13
|
-
# yarn
|
|
13
|
+
# or
|
|
14
14
|
yarn add metal-orm
|
|
15
|
-
|
|
16
|
-
# pnpm
|
|
15
|
+
# or
|
|
17
16
|
pnpm add metal-orm
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
##
|
|
19
|
+
## 2. Your first query (builder only)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { defineTable, col, SelectQueryBuilder, eq, MySqlDialect } from 'metal-orm';
|
|
23
|
+
|
|
24
|
+
const todos = defineTable('todos', {
|
|
25
|
+
id: col.int().primaryKey(),
|
|
26
|
+
title: col.varchar(255).notNull(),
|
|
27
|
+
done: col.boolean().default(false),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const query = new SelectQueryBuilder(todos)
|
|
31
|
+
.select({
|
|
32
|
+
id: todos.columns.id,
|
|
33
|
+
title: todos.columns.title,
|
|
34
|
+
})
|
|
35
|
+
.where(eq(todos.columns.done, false));
|
|
36
|
+
|
|
37
|
+
const dialect = new MySqlDialect();
|
|
38
|
+
const { sql, params } = query.compile(dialect);
|
|
39
|
+
|
|
40
|
+
// Execute `sql` + `params` with your DB driver.
|
|
41
|
+
```
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
## 3. Adding relations & hydration
|
|
23
44
|
|
|
24
45
|
```typescript
|
|
25
46
|
import {
|
|
@@ -29,59 +50,50 @@ import {
|
|
|
29
50
|
SelectQueryBuilder,
|
|
30
51
|
eq,
|
|
31
52
|
count,
|
|
53
|
+
rowNumber,
|
|
32
54
|
hydrateRows,
|
|
33
|
-
MySqlDialect
|
|
34
55
|
} from 'metal-orm';
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
content: col.text(),
|
|
43
|
-
userId: col.int().notNull(),
|
|
44
|
-
createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
|
|
45
|
-
updatedAt: col.timestamp()
|
|
46
|
-
}
|
|
47
|
-
);
|
|
57
|
+
const posts = defineTable('posts', {
|
|
58
|
+
id: col.int().primaryKey(),
|
|
59
|
+
title: col.varchar(255).notNull(),
|
|
60
|
+
userId: col.int().notNull(),
|
|
61
|
+
createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
|
|
62
|
+
});
|
|
48
63
|
|
|
49
|
-
const users = defineTable(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
posts: hasMany(posts, 'userId')
|
|
59
|
-
}
|
|
60
|
-
);
|
|
64
|
+
const users = defineTable('users', {
|
|
65
|
+
id: col.int().primaryKey(),
|
|
66
|
+
name: col.varchar(255).notNull(),
|
|
67
|
+
email: col.varchar(255).unique(),
|
|
68
|
+
}, {
|
|
69
|
+
posts: hasMany(posts, 'userId'),
|
|
70
|
+
});
|
|
61
71
|
|
|
62
|
-
//
|
|
72
|
+
// Build a query with relation & window function
|
|
63
73
|
const builder = new SelectQueryBuilder(users)
|
|
64
74
|
.select({
|
|
65
75
|
id: users.columns.id,
|
|
66
76
|
name: users.columns.name,
|
|
67
77
|
email: users.columns.email,
|
|
68
|
-
|
|
78
|
+
postCount: count(posts.columns.id),
|
|
79
|
+
rank: rowNumber(), // window function helper
|
|
69
80
|
})
|
|
70
81
|
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
71
82
|
.groupBy(users.columns.id, users.columns.name, users.columns.email)
|
|
72
83
|
.orderBy(count(posts.columns.id), 'DESC')
|
|
73
|
-
.limit(
|
|
84
|
+
.limit(10)
|
|
74
85
|
.include('posts', {
|
|
75
|
-
columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt]
|
|
76
|
-
});
|
|
86
|
+
columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt],
|
|
87
|
+
}); // eager relation for hydration
|
|
77
88
|
|
|
78
|
-
// 3. Compile to SQL
|
|
79
|
-
const dialect = new MySqlDialect();
|
|
80
89
|
const { sql, params } = builder.compile(dialect);
|
|
90
|
+
const [rows] = await connection.execute(sql, params);
|
|
81
91
|
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
92
|
+
// Turn flat rows into nested objects
|
|
93
|
+
const hydrated = hydrateRows(
|
|
94
|
+
rows as Record<string, unknown>[],
|
|
95
|
+
builder.getHydrationPlan(),
|
|
96
|
+
);
|
|
85
97
|
|
|
86
98
|
console.log(hydrated);
|
|
87
99
|
// [
|
|
@@ -89,16 +101,71 @@ console.log(hydrated);
|
|
|
89
101
|
// id: 1,
|
|
90
102
|
// name: 'John Doe',
|
|
91
103
|
// email: 'john@example.com',
|
|
92
|
-
//
|
|
104
|
+
// postCount: 15,
|
|
105
|
+
// rank: 1,
|
|
93
106
|
// posts: [
|
|
94
|
-
// {
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// // ... more posts
|
|
100
|
-
// ]
|
|
101
|
-
// }
|
|
102
|
-
// // ... more users
|
|
107
|
+
// { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
|
|
108
|
+
// // ...
|
|
109
|
+
// ],
|
|
110
|
+
// },
|
|
111
|
+
// // ...
|
|
103
112
|
// ]
|
|
104
113
|
```
|
|
114
|
+
|
|
115
|
+
## 4. A taste of the runtime (optional)
|
|
116
|
+
|
|
117
|
+
When you're ready to let MetalORM manage entities and relations, you can use the OrmContext:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import {
|
|
121
|
+
OrmContext,
|
|
122
|
+
MySqlDialect,
|
|
123
|
+
SelectQueryBuilder,
|
|
124
|
+
eq,
|
|
125
|
+
} from 'metal-orm';
|
|
126
|
+
|
|
127
|
+
// 1) Create an OrmContext for this request
|
|
128
|
+
const ctx = new OrmContext({
|
|
129
|
+
dialect: new MySqlDialect(),
|
|
130
|
+
db: {
|
|
131
|
+
async executeSql(sql, params) {
|
|
132
|
+
const [rows] = await connection.execute(sql, params);
|
|
133
|
+
// MetalORM expects columns + values; adapt as needed
|
|
134
|
+
return [{
|
|
135
|
+
columns: Object.keys(rows[0] ?? {}),
|
|
136
|
+
values: rows.map(row => Object.values(row)),
|
|
137
|
+
}];
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 2) Load entities with lazy relations
|
|
143
|
+
const [user] = await new SelectQueryBuilder(users)
|
|
144
|
+
.select({
|
|
145
|
+
id: users.columns.id,
|
|
146
|
+
name: users.columns.name,
|
|
147
|
+
email: users.columns.email,
|
|
148
|
+
})
|
|
149
|
+
.includeLazy('posts') // HasMany as a lazy collection
|
|
150
|
+
.includeLazy('roles') // BelongsToMany as a lazy collection
|
|
151
|
+
.where(eq(users.columns.id, 1))
|
|
152
|
+
.execute(ctx);
|
|
153
|
+
|
|
154
|
+
// user is an Entity<typeof users>
|
|
155
|
+
// scalar props are normal:
|
|
156
|
+
user.name = 'Updated Name'; // marks entity as Dirty
|
|
157
|
+
|
|
158
|
+
// relations are live collections:
|
|
159
|
+
const postsCollection = await user.posts.load(); // batched lazy load
|
|
160
|
+
const newPost = user.posts.add({ title: 'Hello from ORM mode' });
|
|
161
|
+
|
|
162
|
+
// Many-to-many via pivot:
|
|
163
|
+
await user.roles.syncByIds([1, 2, 3]);
|
|
164
|
+
|
|
165
|
+
// 3) Persist the entire graph
|
|
166
|
+
await ctx.saveChanges();
|
|
167
|
+
// INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See [Runtime & Unit of Work](./runtime.md) for full details on entities and the Unit of Work.
|
package/docs/hydration.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hydration & Entities
|
|
2
2
|
|
|
3
|
-
MetalORM
|
|
3
|
+
MetalORM offers two ways to go from SQL rows to richer structures:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **Hydration only** – `hydrateRows(rows, plan)` → nested plain objects.
|
|
6
|
+
2. **Entity runtime** – `builder.execute(ctx)` → entities with lazy relations.
|
|
7
|
+
|
|
8
|
+
## 1. Hydrating with `hydrateRows`
|
|
6
9
|
|
|
7
10
|
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
11
|
|
|
@@ -38,13 +41,75 @@ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
|
|
|
38
41
|
|
|
39
42
|
## How it Works
|
|
40
43
|
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
## Pivot Column Hydration
|
|
47
|
+
|
|
48
|
+
For belongs-to-many relationships, you can request pivot columns via the `pivot` option. Pivot columns are hydrated alongside each child row under the `_pivot` key:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
.include('projects', {
|
|
52
|
+
columns: ['id', 'name', 'client'],
|
|
53
|
+
pivot: { columns: ['assigned_at', 'role_id'] }
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This would hydrate to:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
{
|
|
61
|
+
id: 1,
|
|
62
|
+
name: 'John Doe',
|
|
63
|
+
projects: [
|
|
64
|
+
{
|
|
65
|
+
id: 101,
|
|
66
|
+
name: 'Project Alpha',
|
|
67
|
+
client: 'Acme Corp',
|
|
68
|
+
_pivot: {
|
|
69
|
+
assigned_at: '2023-01-15T10:00:00.000Z',
|
|
70
|
+
role_id: 2
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Advanced Hydration Options
|
|
78
|
+
|
|
79
|
+
You can also specify which pivot columns to include and customize the hydration:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
.include('projects', {
|
|
83
|
+
columns: ['id', 'name'],
|
|
84
|
+
pivot: {
|
|
85
|
+
columns: ['assigned_at', 'role_id'],
|
|
86
|
+
alias: 'assignment_info' // Custom alias instead of '_pivot'
|
|
87
|
+
},
|
|
88
|
+
include: {
|
|
89
|
+
client: {
|
|
90
|
+
columns: ['id', 'name']
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
## 2. Entities on top of hydration
|
|
96
|
+
|
|
97
|
+
When you call `.execute(ctx)` instead of compiling manually:
|
|
98
|
+
|
|
99
|
+
- MetalORM compiles and executes the query using the dialect in the `OrmContext`.
|
|
100
|
+
- If a `HydrationPlan` exists, it is applied.
|
|
101
|
+
- Each root row is wrapped in an entity proxy:
|
|
102
|
+
- scalar fields behave like normal properties
|
|
103
|
+
- relation properties expose `HasManyCollection`, `BelongsToReference`, or `ManyToManyCollection`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const [user] = await new SelectQueryBuilder(users)
|
|
107
|
+
.select({ id: users.columns.id, name: users.columns.name })
|
|
108
|
+
.include('posts', { columns: [posts.columns.id, posts.columns.title] }) // eager
|
|
109
|
+
.includeLazy('roles') // lazy
|
|
110
|
+
.execute(ctx);
|
|
111
|
+
|
|
112
|
+
const eagerPosts = user.posts.getItems(); // hydrated from the join
|
|
113
|
+
const lazyRoles = await user.roles.load(); // resolved on demand
|
|
114
|
+
```
|
|
115
|
+
```
|
package/docs/index.md
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
# Introduction to MetalORM
|
|
2
2
|
|
|
3
|
-
MetalORM is a TypeScript-first SQL
|
|
3
|
+
MetalORM is a TypeScript-first SQL toolkit that can be used as:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. A **typed query builder** over a real SQL AST.
|
|
6
|
+
2. A **hydration layer** for turning flat rows into nested objects.
|
|
7
|
+
3. An optional **entity runtime** with lazy relations and a Unit of Work.
|
|
8
|
+
|
|
9
|
+
You can adopt these layers independently, in this order.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## Philosophy
|
|
8
12
|
|
|
9
|
-
- **Type Safety First
|
|
10
|
-
- **SQL Transparency
|
|
11
|
-
- **Composition Over Configuration
|
|
12
|
-
- **Zero Magic
|
|
13
|
-
- **Multi-Dialect
|
|
13
|
+
- **Type Safety First** – rely on TypeScript to catch mistakes early.:contentReference[oaicite:8]{index=8}
|
|
14
|
+
- **SQL Transparency** – inspect the AST and generated SQL at any time.
|
|
15
|
+
- **Composition Over Configuration** – build complex queries from small, pure pieces.
|
|
16
|
+
- **Zero Magic, Opt-in Runtime** – the query builder and ORM runtime are separate layers.
|
|
17
|
+
- **Multi-Dialect** – MySQL, PostgreSQL, SQLite, SQL Server out of the box.:contentReference[oaicite:9]{index=9}
|
|
14
18
|
|
|
15
19
|
## Features
|
|
16
20
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- **Multi-Dialect Support**: Compile the same query to different SQL dialects.
|
|
21
|
+
- Declarative schema definition with relations.
|
|
22
|
+
- Fluent query builder, including CTEs, window functions, subqueries, JSON, CASE.
|
|
23
|
+
- Relation hydration from flat result sets.
|
|
24
|
+
- Optional entity runtime + Unit of Work.
|
|
22
25
|
|
|
23
26
|
## Table of Contents
|
|
24
27
|
|
|
25
28
|
- [Getting Started](./getting-started.md)
|
|
26
29
|
- [Schema Definition](./schema-definition.md)
|
|
27
30
|
- [Query Builder](./query-builder.md)
|
|
31
|
+
- [DML Operations](./dml-operations.md)
|
|
32
|
+
- [Hydration & Entities](./hydration.md)
|
|
33
|
+
- [Runtime & Unit of Work](./runtime.md)
|
|
28
34
|
- [Advanced Features](./advanced-features.md)
|
|
29
|
-
- [Hydration](./hydration.md)
|
|
30
35
|
- [Multi-Dialect Support](./multi-dialect-support.md)
|
|
31
36
|
- [API Reference](./api-reference.md)
|
|
@@ -30,5 +30,30 @@ const mssql = query.compile(new MSSQLDialect());
|
|
|
30
30
|
- **MySQL**: `MySqlDialect`
|
|
31
31
|
- **SQLite**: `SQLiteDialect`
|
|
32
32
|
- **SQL Server**: `MSSQLDialect`
|
|
33
|
+
- **PostgreSQL**: `PostgresDialect`
|
|
33
34
|
|
|
34
35
|
Each dialect handles the specific syntax and parameterization of the target database.
|
|
36
|
+
|
|
37
|
+
### Dialect-Specific Features
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// PostgreSQL-specific JSON operations
|
|
41
|
+
const query = new SelectQueryBuilder(users)
|
|
42
|
+
.select({
|
|
43
|
+
id: users.columns.id,
|
|
44
|
+
name: users.columns.name,
|
|
45
|
+
settings: jsonPath(users.columns.settings, '$.notifications')
|
|
46
|
+
})
|
|
47
|
+
.compile(new PostgresDialect());
|
|
48
|
+
|
|
49
|
+
// SQL Server TOP clause vs LIMIT
|
|
50
|
+
const limitedQuery = new SelectQueryBuilder(users)
|
|
51
|
+
.selectRaw('*')
|
|
52
|
+
.limit(10);
|
|
53
|
+
|
|
54
|
+
// MySQL/SQLite/PostgreSQL: SELECT * FROM users LIMIT 10
|
|
55
|
+
// SQL Server: SELECT TOP 10 * FROM users
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Note**: When using the runtime (`OrmContext`), the same dialects are used to generate INSERT, UPDATE, DELETE, and pivot operations in `saveChanges()`.
|
|
59
|
+
```
|
package/docs/query-builder.md
CHANGED
|
@@ -73,3 +73,63 @@ const query = new SelectQueryBuilder(posts)
|
|
|
73
73
|
.limit(10)
|
|
74
74
|
.offset(20);
|
|
75
75
|
```
|
|
76
|
+
|
|
77
|
+
### Window Functions
|
|
78
|
+
|
|
79
|
+
The query builder supports window functions for advanced analytics:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { rowNumber, rank } from 'metal-orm';
|
|
83
|
+
|
|
84
|
+
const query = new SelectQueryBuilder(users)
|
|
85
|
+
.select({
|
|
86
|
+
id: users.columns.id,
|
|
87
|
+
name: users.columns.name,
|
|
88
|
+
rowNum: rowNumber(),
|
|
89
|
+
userRank: rank()
|
|
90
|
+
})
|
|
91
|
+
.partitionBy(users.columns.department)
|
|
92
|
+
.orderBy(users.columns.salary, 'DESC');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### CTEs (Common Table Expressions)
|
|
96
|
+
|
|
97
|
+
You can use CTEs to organize complex queries:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const activeUsers = new SelectQueryBuilder(users)
|
|
101
|
+
.selectRaw('*')
|
|
102
|
+
.where(gt(users.columns.lastLogin, new Date('2023-01-01')))
|
|
103
|
+
.as('active_users');
|
|
104
|
+
|
|
105
|
+
const query = new SelectQueryBuilder(activeUsers)
|
|
106
|
+
.with(activeUsers)
|
|
107
|
+
.selectRaw('*')
|
|
108
|
+
.where(eq(activeUsers.columns.id, 1));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Subqueries
|
|
112
|
+
|
|
113
|
+
Support for subqueries in SELECT and WHERE clauses:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const subquery = new SelectQueryBuilder(posts)
|
|
117
|
+
.select({ count: count(posts.columns.id) })
|
|
118
|
+
.where(eq(posts.columns.userId, users.columns.id));
|
|
119
|
+
|
|
120
|
+
const query = new SelectQueryBuilder(users)
|
|
121
|
+
.select({
|
|
122
|
+
id: users.columns.id,
|
|
123
|
+
name: users.columns.name,
|
|
124
|
+
postCount: subquery
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
## From Builder to Entities
|
|
128
|
+
|
|
129
|
+
You can keep using the query builder on its own, or plug it into the entity runtime:
|
|
130
|
+
|
|
131
|
+
- `builder.compile(dialect)` → SQL + params → driver (builder-only usage).
|
|
132
|
+
- `builder.execute(ctx)` → entities tracked by an `OrmContext` (runtime usage).
|
|
133
|
+
|
|
134
|
+
See [Runtime & Unit of Work](./runtime.md) for how `execute(ctx)` integrates with entities and lazy relations.
|
|
135
|
+
```
|