metal-orm 1.0.1 → 1.0.3
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 +46 -482
- 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 +1 -1
- package/src/ast/expression.ts +433 -175
- package/src/ast/join.ts +8 -1
- package/src/ast/query.ts +64 -9
- package/src/builder/hydration-manager.ts +42 -11
- package/src/builder/hydration-planner.ts +80 -31
- package/src/builder/operations/column-selector.ts +37 -1
- package/src/builder/operations/cte-manager.ts +16 -0
- package/src/builder/operations/filter-manager.ts +32 -0
- package/src/builder/operations/join-manager.ts +17 -7
- package/src/builder/operations/pagination-manager.ts +19 -0
- package/src/builder/operations/relation-manager.ts +58 -3
- package/src/builder/query-ast-service.ts +100 -29
- package/src/builder/relation-conditions.ts +30 -1
- package/src/builder/relation-projection-helper.ts +43 -1
- package/src/builder/relation-service.ts +68 -13
- package/src/builder/relation-types.ts +6 -0
- package/src/builder/select-query-builder-deps.ts +64 -3
- package/src/builder/select-query-state.ts +72 -0
- package/src/builder/select.ts +166 -0
- package/src/codegen/typescript.ts +142 -44
- package/src/constants/sql-operator-config.ts +36 -0
- package/src/constants/sql.ts +125 -57
- package/src/dialect/abstract.ts +97 -22
- package/src/dialect/mssql/index.ts +27 -0
- package/src/dialect/mysql/index.ts +22 -0
- package/src/dialect/postgres/index.ts +103 -0
- package/src/dialect/sqlite/index.ts +22 -0
- package/src/runtime/als.ts +15 -1
- package/src/runtime/hydration.ts +20 -15
- package/src/schema/column.ts +45 -5
- package/src/schema/relation.ts +49 -2
- package/src/schema/table.ts +27 -3
- package/src/utils/join-node.ts +20 -0
- package/src/utils/raw-column-parser.ts +32 -0
- package/src/utils/relation-alias.ts +43 -0
- package/tests/postgres.test.ts +30 -0
- package/tests/window-function.test.ts +14 -0
package/src/schema/table.ts
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { ColumnDef } from './column';
|
|
2
2
|
import { RelationDef } from './relation';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Definition of a database table with its columns and relationships
|
|
6
|
+
* @typeParam T - Type of the columns record
|
|
7
|
+
*/
|
|
4
8
|
export interface TableDef<T extends Record<string, ColumnDef> = Record<string, ColumnDef>> {
|
|
9
|
+
/** Name of the table */
|
|
5
10
|
name: string;
|
|
11
|
+
/** Record of column definitions keyed by column name */
|
|
6
12
|
columns: T;
|
|
13
|
+
/** Record of relationship definitions keyed by relation name */
|
|
7
14
|
relations: Record<string, RelationDef>;
|
|
8
15
|
}
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a table definition with columns and relationships
|
|
19
|
+
* @typeParam T - Type of the columns record
|
|
20
|
+
* @param name - Name of the table
|
|
21
|
+
* @param columns - Record of column definitions
|
|
22
|
+
* @param relations - Record of relationship definitions (optional)
|
|
23
|
+
* @returns Complete table definition with runtime-filled column metadata
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const usersTable = defineTable('users', {
|
|
28
|
+
* id: col.primaryKey(col.int()),
|
|
29
|
+
* name: col.varchar(255),
|
|
30
|
+
* email: col.varchar(255)
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
10
34
|
export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
11
|
-
name: string,
|
|
35
|
+
name: string,
|
|
12
36
|
columns: T,
|
|
13
37
|
relations: Record<string, RelationDef> = {}
|
|
14
38
|
): TableDef<T> => {
|
|
@@ -17,6 +41,6 @@ export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
|
17
41
|
(acc as any)[key] = { ...def, name: key, table: name };
|
|
18
42
|
return acc;
|
|
19
43
|
}, {} as T);
|
|
20
|
-
|
|
44
|
+
|
|
21
45
|
return { name, columns: colsWithNames, relations };
|
|
22
|
-
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JoinNode } from '../ast/join';
|
|
2
|
+
import { ExpressionNode } from '../ast/expression';
|
|
3
|
+
import { JoinKind } from '../constants/sql';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a JoinNode ready for AST insertion.
|
|
7
|
+
* Centralizing this avoids copy/pasted object literals when multiple services need to synthesize joins.
|
|
8
|
+
*/
|
|
9
|
+
export const createJoinNode = (
|
|
10
|
+
kind: JoinKind,
|
|
11
|
+
tableName: string,
|
|
12
|
+
condition: ExpressionNode,
|
|
13
|
+
relationName?: string
|
|
14
|
+
): JoinNode => ({
|
|
15
|
+
type: 'Join',
|
|
16
|
+
kind,
|
|
17
|
+
table: { type: 'Table', name: tableName },
|
|
18
|
+
condition,
|
|
19
|
+
relationName
|
|
20
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ColumnNode } from '../ast/expression';
|
|
2
|
+
import { CommonTableExpressionNode } from '../ast/query';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Best-effort helper that tries to convert a raw column expression into a `ColumnNode`.
|
|
6
|
+
* This parser is intentionally limited; use it only for simple references or function calls.
|
|
7
|
+
*/
|
|
8
|
+
export const parseRawColumn = (
|
|
9
|
+
col: string,
|
|
10
|
+
tableName: string,
|
|
11
|
+
ctes?: CommonTableExpressionNode[]
|
|
12
|
+
): ColumnNode => {
|
|
13
|
+
if (col.includes('(')) {
|
|
14
|
+
const [fn, rest] = col.split('(');
|
|
15
|
+
const colName = rest.replace(')', '');
|
|
16
|
+
const [table, name] = colName.includes('.') ? colName.split('.') : [tableName, colName];
|
|
17
|
+
return { type: 'Column', table, name, alias: col };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (col.includes('.')) {
|
|
21
|
+
const [potentialCteName, columnName] = col.split('.');
|
|
22
|
+
const hasCte = ctes?.some(cte => cte.name === potentialCteName);
|
|
23
|
+
|
|
24
|
+
if (hasCte) {
|
|
25
|
+
return { type: 'Column', table: tableName, name: col };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { type: 'Column', table: potentialCteName, name: columnName };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { type: 'Column', table: tableName, name: col };
|
|
32
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Separator used when projecting relational columns
|
|
3
|
+
*/
|
|
4
|
+
const RELATION_SEPARATOR = '__';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parts of a relation alias
|
|
8
|
+
*/
|
|
9
|
+
export interface RelationAliasParts {
|
|
10
|
+
/**
|
|
11
|
+
* Relation name (left side of the separator)
|
|
12
|
+
*/
|
|
13
|
+
relationName: string;
|
|
14
|
+
/**
|
|
15
|
+
* Column name (right side of the separator)
|
|
16
|
+
*/
|
|
17
|
+
columnName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds a relation alias from the relation name and column name components.
|
|
22
|
+
*/
|
|
23
|
+
export const makeRelationAlias = (relationName: string, columnName: string): string =>
|
|
24
|
+
`${relationName}${RELATION_SEPARATOR}${columnName}`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parses a relation alias into its relation/column components.
|
|
28
|
+
* Returns `null` when the alias does not follow the `relation__column` pattern.
|
|
29
|
+
*/
|
|
30
|
+
export const parseRelationAlias = (alias: string): RelationAliasParts | null => {
|
|
31
|
+
const idx = alias.indexOf(RELATION_SEPARATOR);
|
|
32
|
+
if (idx === -1) return null;
|
|
33
|
+
return {
|
|
34
|
+
relationName: alias.slice(0, idx),
|
|
35
|
+
columnName: alias.slice(idx + RELATION_SEPARATOR.length)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Determines whether an alias represents a relation column by checking the `__` convention.
|
|
41
|
+
*/
|
|
42
|
+
export const isRelationAlias = (alias?: string): boolean =>
|
|
43
|
+
!!alias && alias.includes(RELATION_SEPARATOR);
|
|
@@ -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
|
});
|