metal-orm 1.0.0

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.
Files changed (79) hide show
  1. package/README.md +30 -0
  2. package/ROADMAP.md +125 -0
  3. package/metadata.json +5 -0
  4. package/package.json +45 -0
  5. package/playground/api/playground-api.ts +94 -0
  6. package/playground/index.html +15 -0
  7. package/playground/src/App.css +1 -0
  8. package/playground/src/App.tsx +114 -0
  9. package/playground/src/components/CodeDisplay.tsx +43 -0
  10. package/playground/src/components/QueryExecutor.tsx +189 -0
  11. package/playground/src/components/ResultsTable.tsx +67 -0
  12. package/playground/src/components/ResultsTabs.tsx +105 -0
  13. package/playground/src/components/ScenarioList.tsx +56 -0
  14. package/playground/src/components/logo.svg +45 -0
  15. package/playground/src/data/scenarios.ts +2 -0
  16. package/playground/src/main.tsx +9 -0
  17. package/playground/src/services/PlaygroundApiService.ts +60 -0
  18. package/postcss.config.cjs +5 -0
  19. package/sql_sql-ansi-cheatsheet-2025.md +264 -0
  20. package/src/ast/expression.ts +362 -0
  21. package/src/ast/join.ts +11 -0
  22. package/src/ast/query.ts +63 -0
  23. package/src/builder/hydration-manager.ts +55 -0
  24. package/src/builder/hydration-planner.ts +77 -0
  25. package/src/builder/operations/column-selector.ts +42 -0
  26. package/src/builder/operations/cte-manager.ts +18 -0
  27. package/src/builder/operations/filter-manager.ts +36 -0
  28. package/src/builder/operations/join-manager.ts +26 -0
  29. package/src/builder/operations/pagination-manager.ts +17 -0
  30. package/src/builder/operations/relation-manager.ts +49 -0
  31. package/src/builder/query-ast-service.ts +155 -0
  32. package/src/builder/relation-conditions.ts +39 -0
  33. package/src/builder/relation-projection-helper.ts +59 -0
  34. package/src/builder/relation-service.ts +166 -0
  35. package/src/builder/select-query-builder-deps.ts +33 -0
  36. package/src/builder/select-query-state.ts +107 -0
  37. package/src/builder/select.ts +237 -0
  38. package/src/codegen/typescript.ts +295 -0
  39. package/src/constants/sql.ts +57 -0
  40. package/src/dialect/abstract.ts +221 -0
  41. package/src/dialect/mssql/index.ts +89 -0
  42. package/src/dialect/mysql/index.ts +81 -0
  43. package/src/dialect/sqlite/index.ts +85 -0
  44. package/src/index.ts +12 -0
  45. package/src/playground/features/playground/api/types.ts +16 -0
  46. package/src/playground/features/playground/clients/MockClient.ts +17 -0
  47. package/src/playground/features/playground/clients/SqliteClient.ts +57 -0
  48. package/src/playground/features/playground/common/IDatabaseClient.ts +10 -0
  49. package/src/playground/features/playground/data/scenarios/aggregation.ts +36 -0
  50. package/src/playground/features/playground/data/scenarios/basics.ts +25 -0
  51. package/src/playground/features/playground/data/scenarios/edge_cases.ts +57 -0
  52. package/src/playground/features/playground/data/scenarios/filtering.ts +94 -0
  53. package/src/playground/features/playground/data/scenarios/hydration.ts +15 -0
  54. package/src/playground/features/playground/data/scenarios/index.ts +29 -0
  55. package/src/playground/features/playground/data/scenarios/ordering.ts +25 -0
  56. package/src/playground/features/playground/data/scenarios/pagination.ts +16 -0
  57. package/src/playground/features/playground/data/scenarios/relationships.ts +75 -0
  58. package/src/playground/features/playground/data/scenarios/types.ts +67 -0
  59. package/src/playground/features/playground/data/schema.ts +87 -0
  60. package/src/playground/features/playground/data/seed.ts +104 -0
  61. package/src/playground/features/playground/services/QueryExecutionService.ts +120 -0
  62. package/src/runtime/als.ts +19 -0
  63. package/src/runtime/hydration.ts +43 -0
  64. package/src/schema/column.ts +19 -0
  65. package/src/schema/relation.ts +38 -0
  66. package/src/schema/table.ts +22 -0
  67. package/tests/between.test.ts +43 -0
  68. package/tests/case-expression.test.ts +58 -0
  69. package/tests/complex-exists.test.ts +230 -0
  70. package/tests/cte.test.ts +118 -0
  71. package/tests/exists.test.ts +127 -0
  72. package/tests/like.test.ts +33 -0
  73. package/tests/right-join.test.ts +89 -0
  74. package/tests/subquery-having.test.ts +193 -0
  75. package/tests/window-function.test.ts +137 -0
  76. package/tsconfig.json +30 -0
  77. package/tsup.config.ts +10 -0
  78. package/vite.config.ts +22 -0
  79. package/vitest.config.ts +14 -0
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SelectQueryBuilder } from '../src/builder/select';
3
+ import { SqliteDialect } from '../src/dialect/sqlite';
4
+ import { MySqlDialect } from '../src/dialect/mysql';
5
+ import { SqlServerDialect } from '../src/dialect/mssql';
6
+ import { TableDef } from '../src/schema/table';
7
+ import { rowNumber, rank, denseRank, lag, lead, ntile, firstValue, lastValue, windowFunction } from '../src/ast/expression';
8
+
9
+ const table = (name: string): TableDef => ({ name, columns: {}, relations: {} });
10
+ const col = (name: string, table?: string) => ({ type: 'Column', name, table: table || 'unknown' } as any);
11
+ const lit = (value: any) => ({ type: 'Literal', value } as any);
12
+
13
+ describe('Window Function Support', () => {
14
+ const sqlite = new SqliteDialect();
15
+ const mysql = new MySqlDialect();
16
+ const mssql = new SqlServerDialect();
17
+
18
+ it('should generate ROW_NUMBER() window function', () => {
19
+ const users = table('users');
20
+
21
+ const query = new SelectQueryBuilder(users)
22
+ .select({
23
+ id: col('id', 'users'),
24
+ name: col('name', 'users'),
25
+ row_num: rowNumber()
26
+ });
27
+
28
+ const expectedSqlite = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
29
+ const expectedMysql = 'SELECT `users`.`id` AS `id`, `users`.`name` AS `name`, ROW_NUMBER() OVER () AS `row_num` FROM `users`;';
30
+ const expectedMssql = 'SELECT [users].[id] AS [id], [users].[name] AS [name], ROW_NUMBER() OVER () AS [row_num] FROM [users];';
31
+
32
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
33
+ expect(query.toSql(mysql)).toBe(expectedMysql);
34
+ expect(query.toSql(mssql)).toBe(expectedMssql);
35
+ });
36
+
37
+ it('should generate RANK() with PARTITION BY and ORDER BY', () => {
38
+ const orders = table('orders');
39
+
40
+ const query = new SelectQueryBuilder(orders)
41
+ .select({
42
+ id: col('id', 'orders'),
43
+ customer_id: col('customer_id', 'orders'),
44
+ amount: col('amount', 'orders'),
45
+ rank: windowFunction('RANK', [], [col('customer_id', 'orders')], [{ column: col('amount', 'orders'), direction: 'DESC' }])
46
+ });
47
+
48
+ 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
+ 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
+ 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];';
51
+
52
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
53
+ expect(query.toSql(mysql)).toBe(expectedMysql);
54
+ expect(query.toSql(mssql)).toBe(expectedMssql);
55
+ });
56
+
57
+ it('should generate LAG function with offset and default value', () => {
58
+ const sales = table('sales');
59
+
60
+ const query = new SelectQueryBuilder(sales)
61
+ .select({
62
+ date: col('date', 'sales'),
63
+ amount: col('amount', 'sales'),
64
+ prev_amount: lag(col('amount', 'sales'), 1, 0)
65
+ });
66
+
67
+ const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
68
+ const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LAG(`sales`.`amount`, ?, ?) OVER () AS `prev_amount` FROM `sales`;';
69
+ const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LAG([sales].[amount], @p1, @p2) OVER () AS [prev_amount] FROM [sales];';
70
+
71
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
72
+ expect(query.toSql(mysql)).toBe(expectedMysql);
73
+ expect(query.toSql(mssql)).toBe(expectedMssql);
74
+ });
75
+
76
+ it('should generate LEAD function', () => {
77
+ const sales = table('sales');
78
+
79
+ const query = new SelectQueryBuilder(sales)
80
+ .select({
81
+ date: col('date', 'sales'),
82
+ amount: col('amount', 'sales'),
83
+ next_amount: lead(col('amount', 'sales'), 1)
84
+ });
85
+
86
+ const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
87
+ const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LEAD(`sales`.`amount`, ?) OVER () AS `next_amount` FROM `sales`;';
88
+ const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LEAD([sales].[amount], @p1) OVER () AS [next_amount] FROM [sales];';
89
+
90
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
91
+ expect(query.toSql(mysql)).toBe(expectedMysql);
92
+ expect(query.toSql(mssql)).toBe(expectedMssql);
93
+ });
94
+
95
+ it('should generate window function with both PARTITION BY and ORDER BY', () => {
96
+ const employees = table('employees');
97
+
98
+ const query = new SelectQueryBuilder(employees)
99
+ .select({
100
+ id: col('id', 'employees'),
101
+ name: col('name', 'employees'),
102
+ department: col('department', 'employees'),
103
+ salary: col('salary', 'employees'),
104
+ dept_rank: windowFunction('ROW_NUMBER', [], [col('department', 'employees')], [{ column: col('salary', 'employees'), direction: 'DESC' }])
105
+ });
106
+
107
+ 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
+ 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
+ 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];';
110
+
111
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
112
+ expect(query.toSql(mysql)).toBe(expectedMysql);
113
+ expect(query.toSql(mssql)).toBe(expectedMssql);
114
+ });
115
+
116
+ it('should generate multiple window functions in one query', () => {
117
+ const employees = table('employees');
118
+
119
+ const query = new SelectQueryBuilder(employees)
120
+ .select({
121
+ id: col('id', 'employees'),
122
+ name: col('name', 'employees'),
123
+ salary: col('salary', 'employees'),
124
+ row_num: rowNumber(),
125
+ rank: rank(),
126
+ dense_rank: denseRank()
127
+ });
128
+
129
+ 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
+ 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
+ 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];';
132
+
133
+ expect(query.toSql(sqlite)).toBe(expectedSqlite);
134
+ expect(query.toSql(mysql)).toBe(expectedMysql);
135
+ expect(query.toSql(mssql)).toBe(expectedMssql);
136
+ });
137
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "DOM",
9
+ "DOM.Iterable",
10
+ "ES2022"
11
+ ],
12
+ "jsx": "react-jsx",
13
+ "skipLibCheck": true,
14
+ "types": [
15
+ "node"
16
+ ],
17
+ "moduleResolution": "bundler",
18
+ "isolatedModules": true,
19
+ "moduleDetection": "force",
20
+ "allowJs": true,
21
+ "baseUrl": "./src",
22
+ "paths": {
23
+ "@orm/*": [
24
+ "./*"
25
+ ]
26
+ },
27
+ "allowImportingTsExtensions": true,
28
+ "noEmit": true
29
+ }
30
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });
package/vite.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import 'tsconfig-paths/register';
2
+ import { defineConfig } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { playgroundApiPlugin } from './playground/api/playground-api';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ export default defineConfig({
11
+ plugins: [
12
+ react(),
13
+ playgroundApiPlugin(),
14
+ ],
15
+ resolve: {
16
+ alias: {
17
+ '@orm': path.resolve(__dirname, './src'),
18
+ 'playground': path.resolve(__dirname, './playground'),
19
+ },
20
+ },
21
+ root: './playground',
22
+ });
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import path from 'path';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'node',
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ '@': path.resolve(__dirname, './src'),
12
+ },
13
+ },
14
+ });