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
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ <div align="center">
2
+ <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
+ </div>
4
+
5
+ # Run and deploy your AI Studio app
6
+
7
+ This contains everything you need to run your app locally.
8
+
9
+ View your app in AI Studio: https://ai.studio/apps/drive/1vXchpJ36cRXpwzrizwz6I-M1Ks1GAkz2
10
+
11
+ ## Run Locally
12
+
13
+ **Prerequisites:** Node.js
14
+
15
+
16
+ 1. Install dependencies:
17
+ `npm install`
18
+ 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
+ 3. Run the app:
20
+ `npm run dev`
21
+
22
+ ## Project layout
23
+
24
+ - `src/` hosts the React-based playground UI that backs the demo.
25
+ - `src/metal-orm/src/` contains the real MetalORM implementation (AST, builder, dialects, runtime) consumed by the playground.
26
+ - Legacy `orm/` and `services/orm/` directories were removed because they were unused duplicates, so future work belongs in `src/metal-orm/src`.
27
+
28
+ ## Parameterized Queries
29
+
30
+ Literal values in expressions are now emitted as parameter placeholders and collected in a binding list. Use `SelectQueryBuilder.compile(dialect)` to get `{ sql, params }` and pass both to your database driver (`client.executeSql(sql, params)`); `SelectQueryBuilder.toSql(dialect)` still returns just the SQL string for quick debugging.
package/ROADMAP.md ADDED
@@ -0,0 +1,125 @@
1
+ # Metal ORM Roadmap
2
+
3
+ ## Current ORM Capabilities
4
+
5
+ The current implementation supports:
6
+
7
+ - Basic SELECT with projections and aliases
8
+ - INNER/LEFT JOINs with manual conditions
9
+ - Smart relationship joins via `joinRelation()`
10
+ - Eager loading with `include()` for 1:1 and 1:N relationships
11
+ - Basic WHERE clauses with operators (eq, gt, like, in, null checks)
12
+ - GROUP BY, ORDER BY, LIMIT, OFFSET
13
+ - Aggregation functions (COUNT, SUM, AVG)
14
+ - JSON path extraction (dialect-specific)
15
+ - Hydration of nested objects from flat SQL results
16
+ - EXISTS and NOT EXISTS subqueries
17
+ - Scalar correlated subqueries in SELECT and WHERE
18
+ - CASE expressions (simple and searched)
19
+ - HAVING clause for post-aggregation filtering
20
+
21
+ ## Identified Absences
22
+
23
+ ### 1. CTE (Common Table Expressions) ✅
24
+
25
+ **Completed:** Full CTE support has been implemented
26
+
27
+ - Features: Simple CTEs, Recursive CTEs, Multiple CTEs, CTE with column aliases
28
+ - **Implementation:** CTE AST node and dialect compilation for SQLite, MySQL, and MSSQL
29
+ - Recursive CTEs properly handle the `WITH RECURSIVE` keyword (SQLite/MySQL only, MSSQL uses plain `WITH`)
30
+ - Mixed recursive and non-recursive CTEs are supported
31
+
32
+ ### 2. Window Functions ✅
33
+
34
+ **Completed:** Comprehensive window function support has been implemented
35
+
36
+ - Features: `ROW_NUMBER()`, `RANK()`, `DENSE_RANK()`, `LAG()`, `LEAD()`, `NTILE()`, `FIRST_VALUE()`, `LAST_VALUE()`
37
+ - **Implementation:** Window function AST nodes with `PARTITION BY` and `ORDER BY` support
38
+ - All three dialects (SQLite, MySQL, MSSQL) support window functions
39
+
40
+ ### 3. RIGHT JOIN Support
41
+
42
+ **Missing:** Only INNER, LEFT, and CROSS joins are defined
43
+
44
+ - **Required Addition:** RIGHT JOIN support in join.ts and dialect compilation
45
+
46
+ ### 4. Complex Aggregation Functions
47
+
48
+ **Missing:** Limited aggregation functions
49
+
50
+ - Queries: `5-top-platform-contributor-per-project`, `5-mega-user-engagement-analytics`
51
+ - **Required Addition:** MIN, MAX, AVG functions, and GROUP_CONCAT
52
+
53
+ ### 5. Parameterized Queries
54
+
55
+ **Missing:** No parameter binding support
56
+
57
+ - Queries: `1-1-parameterized-user-by-twitter`
58
+ - **Required Addition:** Parameter placeholder support and binding mechanism
59
+
60
+ ### 6. Advanced JSON Operations
61
+
62
+ **Missing:** Limited JSON functionality
63
+
64
+ - Queries: `1-1-profile-json-as-subquery-column`, `5-mega-user-engagement-analytics`
65
+ - **Required Addition:** JSON_OBJECT, JSON_ARRAYAGG functions
66
+
67
+ ### 7. Recursive CTEs ✅
68
+
69
+ **Completed:** See Section 1 (CTE) above
70
+
71
+ - Recursive CTEs are fully supported as part of the CTE implementation
72
+
73
+ ### 8. Complex Ordering
74
+
75
+ **Missing:** Limited ORDER BY expressions
76
+
77
+ - Queries: `1-1-order-by-subquery-profile-field`, `5-mega-user-engagement-analytics`
78
+ - **Required Addition:** ORDER BY with expressions and NULLS FIRST/LAST
79
+
80
+ ### 9. DISTINCT ON (PostgreSQL-style)
81
+
82
+ **Missing:** No DISTINCT ON support
83
+
84
+ - **Required Addition:** Dialect-specific DISTINCT ON compilation
85
+
86
+ ### 10. Subquery Aliasing
87
+
88
+ **Missing:** No support for subqueries as derived tables
89
+
90
+ - **Required Addition:** Derived table AST node
91
+
92
+ ### 11. Advanced EXISTS Patterns
93
+
94
+ **Missing:** EXISTS with complex correlated subqueries
95
+
96
+ - Queries: `1-1-boolean-flag-from-subquery-on-profile`
97
+ - **Required Addition:** Complex correlation support
98
+
99
+ ## Priority Implementation Order
100
+
101
+ ### Completed ✅
102
+
103
+ - ~~CTE support~~ (Completed)
104
+ - ~~Window functions~~ (Completed)
105
+ - ~~Recursive CTEs~~ (Completed)
106
+
107
+ ### High Priority:
108
+
109
+ - Parameterized queries
110
+ - RIGHT JOIN
111
+ - Complex aggregation functions (MIN, MAX, GROUP_CONCAT)
112
+
113
+ ### Medium Priority:
114
+
115
+ - Advanced JSON operations
116
+ - Complex ordering (expressions, NULLS FIRST/LAST)
117
+
118
+ ### Lower Priority:
119
+
120
+ - Subquery Aliasing (Derived tables)
121
+ - Advanced EXISTS Patterns
122
+
123
+ ## Implementation Notes
124
+
125
+ The current ORM is well-architected with a clear AST structure and dialect abstraction, making it relatively straightforward to add these missing features by extending the existing patterns.
package/metadata.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "metal-orm",
3
+ "description": "A high-performance, close-to-metal TypeScript ORM playground and documentation site, demonstrating AST-based query building and dialect compilation.",
4
+ "requestFramePermissions": []
5
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "metal-orm",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/index.mjs",
8
+ "require": "./dist/index.js"
9
+ }
10
+ },
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "check": "tsc --noEmit",
14
+ "playground": "vite",
15
+ "test": "vitest",
16
+ "test:ui": "vitest --ui"
17
+ },
18
+ "dependencies": {
19
+ "@mantine/core": "^8.3.9",
20
+ "@mantine/hooks": "^8.3.9",
21
+ "@tabler/icons-react": "^3.35.0",
22
+ "@types/react-syntax-highlighter": "^15.5.13",
23
+ "mysql2": "^3.9.0",
24
+ "postcss": "^8.5.6",
25
+ "postcss-preset-mantine": "^1.18.0",
26
+ "react": "^18.0.0",
27
+ "react-dom": "^18.0.0",
28
+ "react-syntax-highlighter": "^16.1.0",
29
+ "sqlite3": "^5.1.6",
30
+ "tarn": "^3.0.0",
31
+ "tedious": "^16.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.0.0",
35
+ "@types/react-dom": "^18.0.0",
36
+ "@types/sqlite3": "^3.1.11",
37
+ "@vitejs/plugin-react": "^4.0.0",
38
+ "@vitest/ui": "^4.0.14",
39
+ "tsconfig-paths": "^4.2.0",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.0.0",
42
+ "vite": "^5.0.0",
43
+ "vitest": "^4.0.14"
44
+ }
45
+ }
@@ -0,0 +1,94 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { SqliteClient } from '../../src/playground/features/playground/clients/SqliteClient';
3
+ import { SCENARIOS } from '../../src/playground/features/playground/data/scenarios';
4
+ import { QueryExecutionService } from '../../src/playground/features/playground/services/QueryExecutionService';
5
+ import type { ApiStatusResponse } from '../../src/playground/features/playground/api/types';
6
+
7
+ export const PLAYGROUND_API_PREFIX = '/api/playground';
8
+
9
+ const sqliteClient = new SqliteClient();
10
+ const queryExecutionService = new QueryExecutionService(sqliteClient);
11
+
12
+ const sendJson = (res: ServerResponse, payload: unknown, status = 200) => {
13
+ res.statusCode = status;
14
+ res.setHeader('Content-Type', 'application/json');
15
+ res.end(JSON.stringify(payload));
16
+ };
17
+
18
+ const parseJsonBody = (req: IncomingMessage) =>
19
+ new Promise<Record<string, unknown>>((resolve, reject) => {
20
+ const chunks: Buffer[] = [];
21
+ req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
22
+ req.on('end', () => {
23
+ const body = Buffer.concat(chunks).toString('utf8');
24
+ if (!body) {
25
+ resolve({});
26
+ return;
27
+ }
28
+
29
+ try {
30
+ resolve(JSON.parse(body));
31
+ } catch (error) {
32
+ reject(error);
33
+ }
34
+ });
35
+ req.on('error', reject);
36
+ });
37
+
38
+ const handleStatus = (_req: IncomingMessage, res: ServerResponse) => {
39
+ const body: ApiStatusResponse = {
40
+ ready: sqliteClient.isReady,
41
+ error: sqliteClient.error
42
+ };
43
+ sendJson(res, body);
44
+ };
45
+
46
+ const handleExecute = async (req: IncomingMessage, res: ServerResponse) => {
47
+ try {
48
+ const { scenarioId } = await parseJsonBody(req);
49
+ if (!scenarioId || typeof scenarioId !== 'string') {
50
+ sendJson(res, { error: 'Missing scenarioId' }, 400);
51
+ return;
52
+ }
53
+
54
+ const scenario = SCENARIOS.find(s => s.id === scenarioId);
55
+ if (!scenario) {
56
+ sendJson(res, { error: `Scenario '${scenarioId}' not found` }, 404);
57
+ return;
58
+ }
59
+
60
+ const result = await queryExecutionService.executeScenario(scenario);
61
+ sendJson(res, result);
62
+ } catch (error) {
63
+ const message = error instanceof Error ? error.message : 'Unknown error';
64
+ sendJson(res, { error: message }, 500);
65
+ }
66
+ };
67
+
68
+ export const createPlaygroundApiMiddleware = () => {
69
+ return async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
70
+ const pathname = req.url ? req.url.split('?')[0] : '';
71
+
72
+ if (req.method === 'GET' && pathname === '/status') {
73
+ handleStatus(req, res);
74
+ return;
75
+ }
76
+
77
+ if (req.method === 'POST' && pathname === '/execute') {
78
+ await handleExecute(req, res);
79
+ return;
80
+ }
81
+
82
+ next();
83
+ };
84
+ };
85
+
86
+ export const playgroundApiPlugin = () => ({
87
+ name: 'metal-orm-playground-api',
88
+ configureServer(server) {
89
+ server.middlewares.use(PLAYGROUND_API_PREFIX, createPlaygroundApiMiddleware());
90
+ },
91
+ configurePreviewServer(server) {
92
+ server.middlewares.use(PLAYGROUND_API_PREFIX, createPlaygroundApiMiddleware());
93
+ },
94
+ });
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Metal ORM Playground</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+
15
+ </html>
@@ -0,0 +1 @@
1
+ /* App.css is no longer needed as we are using Mantine UI */
@@ -0,0 +1,114 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { MantineProvider, AppShell, Burger, Group, Title, Text, ActionIcon, useMantineTheme } from '@mantine/core';
3
+ import { useDisclosure } from '@mantine/hooks';
4
+ import { SCENARIOS, type Scenario } from './data/scenarios';
5
+ import { PlaygroundApiService } from './services/PlaygroundApiService';
6
+ import { ScenarioList } from './components/ScenarioList';
7
+ import { QueryExecutor } from './components/QueryExecutor';
8
+ import '@mantine/core/styles.css';
9
+ import './App.css'; // Keeping for custom overrides if needed, but will likely remove
10
+
11
+ function App() {
12
+ const [selectedScenario, setSelectedScenario] = useState<Scenario | null>(null);
13
+ const [apiReady, setApiReady] = useState(false);
14
+ const [statusMessage, setStatusMessage] = useState<string | null>(null);
15
+ const [opened, { toggle }] = useDisclosure();
16
+
17
+ const queryService = useMemo(() => new PlaygroundApiService(), []);
18
+
19
+ useEffect(() => {
20
+ let isMounted = true;
21
+ let timerId: ReturnType<typeof setTimeout> | null = null;
22
+
23
+ const pollStatus = async () => {
24
+ const status = await queryService.getStatus();
25
+ if (!isMounted) return;
26
+
27
+ if (status.error) {
28
+ setStatusMessage(status.error);
29
+ } else {
30
+ setStatusMessage(null);
31
+ }
32
+
33
+ if (status.ready) {
34
+ setApiReady(true);
35
+ timerId && clearTimeout(timerId);
36
+ return;
37
+ }
38
+
39
+ timerId = setTimeout(pollStatus, 250);
40
+ };
41
+
42
+ pollStatus();
43
+
44
+ return () => {
45
+ isMounted = false;
46
+ if (timerId) {
47
+ clearTimeout(timerId);
48
+ }
49
+ };
50
+ }, [queryService]);
51
+
52
+ // Auto-select first scenario when API is ready
53
+ useEffect(() => {
54
+ if (apiReady && !selectedScenario && SCENARIOS.length > 0) {
55
+ setSelectedScenario(SCENARIOS[0]);
56
+ }
57
+ }, [apiReady, selectedScenario]);
58
+
59
+ const handleScenarioSelect = (scenario: Scenario) => {
60
+ setSelectedScenario(scenario);
61
+ if (window.innerWidth < 768) {
62
+ toggle(); // Close sidebar on mobile selection
63
+ }
64
+ };
65
+
66
+ if (!apiReady) {
67
+ return (
68
+ <MantineProvider defaultColorScheme="dark">
69
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column' }}>
70
+ <Title order={2}>Initializing Metal ORM Playground</Title>
71
+ <Text c="dimmed">Waiting for the playground API to become ready...</Text>
72
+ {statusMessage && <Text c="red">Error: {statusMessage}</Text>}
73
+ </div>
74
+ </MantineProvider>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <MantineProvider defaultColorScheme="dark">
80
+ <AppShell
81
+ header={{ height: 60 }}
82
+ navbar={{ width: 300, breakpoint: 'sm', collapsed: { mobile: !opened } }}
83
+ padding="md"
84
+ >
85
+ <AppShell.Header>
86
+ <Group h="100%" px="md">
87
+ <Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
88
+ <Group justify="space-between" style={{ flex: 1 }}>
89
+ <Title order={3}>⚡ Metal ORM Playground</Title>
90
+ <Text size="sm" c="dimmed" visibleFrom="sm">Explore and test ORM query scenarios</Text>
91
+ </Group>
92
+ </Group>
93
+ </AppShell.Header>
94
+
95
+ <AppShell.Navbar p="md">
96
+ <ScenarioList
97
+ scenarios={SCENARIOS}
98
+ selectedId={selectedScenario?.id || null}
99
+ onSelect={handleScenarioSelect}
100
+ />
101
+ </AppShell.Navbar>
102
+
103
+ <AppShell.Main>
104
+ <QueryExecutor
105
+ scenario={selectedScenario}
106
+ queryService={queryService}
107
+ />
108
+ </AppShell.Main>
109
+ </AppShell>
110
+ </MantineProvider>
111
+ );
112
+ }
113
+
114
+ export default App;
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ import { ScrollArea } from '@mantine/core';
5
+
6
+ interface CodeDisplayProps {
7
+ code: string;
8
+ language?: 'sql' | 'typescript';
9
+ title?: string;
10
+ }
11
+
12
+ /**
13
+ * Component responsible for displaying syntax-highlighted code
14
+ * Follows SRP by handling only code display formatting
15
+ */
16
+ export const CodeDisplay: React.FC<CodeDisplayProps> = ({
17
+ code,
18
+ language = 'sql',
19
+ title
20
+ }) => {
21
+ return (
22
+ <ScrollArea bg="#282c34">
23
+ <SyntaxHighlighter
24
+ language={language}
25
+ style={oneDark}
26
+ customStyle={{
27
+ margin: 0,
28
+ padding: '1.25rem',
29
+ background: 'transparent',
30
+ fontSize: '0.9rem',
31
+ lineHeight: '1.6'
32
+ }}
33
+ codeTagProps={{
34
+ style: {
35
+ fontFamily: 'JetBrains Mono, Fira Code, monospace'
36
+ }
37
+ }}
38
+ >
39
+ {code}
40
+ </SyntaxHighlighter>
41
+ </ScrollArea>
42
+ );
43
+ };
@@ -0,0 +1,189 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Card, Button, Title, Text, Tabs, Loader, Group, Stack, Badge, ActionIcon, CopyButton, Tooltip } from '@mantine/core';
3
+ import { IconPlayerPlay, IconCode, IconDatabase, IconCheck, IconCopy } from '@tabler/icons-react'; // Assuming tabler icons are available or we use text
4
+ import type { Scenario } from '../data/scenarios';
5
+ import type { QueryExecutionResult } from '@orm/playground/features/playground/api/types';
6
+ import { CodeDisplay } from './CodeDisplay';
7
+ import { ResultsTabs } from './ResultsTabs';
8
+ import { PlaygroundApiService } from '../services/PlaygroundApiService';
9
+
10
+ const describeBinding = (value: unknown): { display: string; type: string } => {
11
+ if (value === null) {
12
+ return { display: 'null', type: 'null' };
13
+ }
14
+ if (typeof value === 'undefined') {
15
+ return { display: 'undefined', type: 'undefined' };
16
+ }
17
+ if (typeof value === 'string') {
18
+ return { display: `"${value}"`, type: 'string' };
19
+ }
20
+ if (typeof value === 'number') {
21
+ return { display: value.toString(), type: Number.isInteger(value) ? 'integer' : 'number' };
22
+ }
23
+ if (typeof value === 'boolean') {
24
+ return { display: value ? 'true' : 'false', type: 'boolean' };
25
+ }
26
+ if (Array.isArray(value)) {
27
+ return { display: JSON.stringify(value), type: 'array' };
28
+ }
29
+ return { display: JSON.stringify(value), type: typeof value };
30
+ };
31
+
32
+ const BindingsDisplay: React.FC<{ params: unknown[] }> = ({ params }) => {
33
+ const hasParams = Array.isArray(params) && params.length > 0;
34
+
35
+ return (
36
+ <div
37
+ style={{
38
+ border: '1px solid var(--mantine-color-dark-5)',
39
+ borderRadius: 'var(--mantine-radius-md)',
40
+ padding: 'var(--mantine-spacing-md)',
41
+ background: 'var(--mantine-color-dark-8)'
42
+ }}
43
+ >
44
+ <Group justify="space-between" mb="sm">
45
+ <Text fw={500}>Bindings</Text>
46
+ {hasParams && (
47
+ <CopyButton value={JSON.stringify(params, null, 2)}>
48
+ {({ copied, copy }) => (
49
+ <Tooltip label={copied ? 'Copied' : 'Copy JSON'} withArrow>
50
+ <ActionIcon variant="subtle" color={copied ? 'teal' : 'blue'} onClick={copy}>
51
+ {copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
52
+ </ActionIcon>
53
+ </Tooltip>
54
+ )}
55
+ </CopyButton>
56
+ )}
57
+ </Group>
58
+ {!hasParams && <Text c="dimmed" fz="sm">Query has no parameter bindings.</Text>}
59
+ {hasParams && (
60
+ <Stack gap="xs">
61
+ {params.map((value, index) => {
62
+ const { display, type } = describeBinding(value);
63
+ return (
64
+ <Group
65
+ key={`${index}-${String(value)}`}
66
+ justify="space-between"
67
+ style={{
68
+ border: '1px solid var(--mantine-color-dark-5)',
69
+ borderRadius: 'var(--mantine-radius-sm)',
70
+ padding: 'var(--mantine-spacing-xs)'
71
+ }}
72
+ >
73
+ <Badge variant="light" color="grape">#{index + 1}</Badge>
74
+ <div style={{ textAlign: 'right' }}>
75
+ <Text style={{ fontFamily: 'var(--mantine-font-family-monospace)' }}>
76
+ {display}
77
+ </Text>
78
+ <Text fz="xs" c="dimmed">
79
+ {type.toUpperCase()}
80
+ </Text>
81
+ </div>
82
+ </Group>
83
+ );
84
+ })}
85
+ </Stack>
86
+ )}
87
+ </div>
88
+ );
89
+ };
90
+
91
+ interface QueryExecutorProps {
92
+ scenario: Scenario | null;
93
+ queryService: PlaygroundApiService;
94
+ }
95
+
96
+ /**
97
+ * Component responsible for executing queries and displaying results
98
+ * Follows SRP by coordinating query execution and result display
99
+ */
100
+ export const QueryExecutor: React.FC<QueryExecutorProps> = ({
101
+ scenario,
102
+ queryService
103
+ }) => {
104
+ const [result, setResult] = useState<QueryExecutionResult | null>(null);
105
+ const [isLoading, setIsLoading] = useState(false);
106
+
107
+ useEffect(() => {
108
+ if (scenario) {
109
+ executeQuery();
110
+ }
111
+ }, [scenario]);
112
+
113
+ const executeQuery = async () => {
114
+ if (!scenario) return;
115
+
116
+ setIsLoading(true);
117
+ const executionResult = await queryService.executeScenario(scenario.id);
118
+ setResult(executionResult);
119
+ setIsLoading(false);
120
+ };
121
+
122
+ if (!scenario) {
123
+ return (
124
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--mantine-color-dimmed)' }}>
125
+ <Text>Select a scenario from the list to execute</Text>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ return (
131
+ <Stack gap="lg">
132
+ <div>
133
+ <Group justify="space-between" align="start" mb="md">
134
+ <div>
135
+ <Title order={2}>{scenario.title}</Title>
136
+ <Text c="dimmed" mt="xs">{scenario.description}</Text>
137
+ </div>
138
+ <Button
139
+ onClick={executeQuery}
140
+ loading={isLoading}
141
+ leftSection={<span>▶</span>}
142
+ variant="gradient"
143
+ gradient={{ from: 'indigo', to: 'cyan' }}
144
+ >
145
+ Re-execute Query
146
+ </Button>
147
+ </Group>
148
+ </div>
149
+
150
+ {result && (
151
+ <>
152
+ <Card withBorder shadow="sm" radius="md" p={0}>
153
+ <Tabs defaultValue="sql">
154
+ <Tabs.List>
155
+ <Tabs.Tab value="sql" leftSection={<span>SQL</span>}>Generated SQL</Tabs.Tab>
156
+ <Tabs.Tab value="typescript" leftSection={<span>TS</span>}>TypeScript</Tabs.Tab>
157
+ </Tabs.List>
158
+
159
+ <Tabs.Panel value="sql">
160
+ <Stack gap="sm" p="md">
161
+ <CodeDisplay code={result.sql} language="sql" />
162
+ <BindingsDisplay params={result.params} />
163
+ </Stack>
164
+ </Tabs.Panel>
165
+
166
+ <Tabs.Panel value="typescript">
167
+ <CodeDisplay code={result.typescriptCode} language="typescript" />
168
+ </Tabs.Panel>
169
+ </Tabs>
170
+ </Card>
171
+
172
+ <ResultsTabs
173
+ results={result.results}
174
+ hydratedResults={result.hydratedResults}
175
+ executionTime={result.executionTime}
176
+ error={result.error}
177
+ />
178
+ </>
179
+ )}
180
+
181
+ {isLoading && (
182
+ <Group justify="center" p="xl">
183
+ <Loader type="dots" />
184
+ <Text>Executing query...</Text>
185
+ </Group>
186
+ )}
187
+ </Stack>
188
+ );
189
+ };