From d61e54865be23ef772c01a4d39492395a318cff7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 13:01:36 +0000 Subject: [PATCH 1/3] Initial plan for issue From d7e754295a73af8235ae7269ea0b4a1a6ad2d1a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 13:06:06 +0000 Subject: [PATCH 2/3] Initial plan for implementing CTEInjector with fixture support Co-authored-by: mk3008 <7686540+mk3008@users.noreply.github.com> --- package-lock.json | 4 ++-- tsconfig.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7d72f8..d845a5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rawsql-ts", - "version": "0.6.0-beta", + "version": "0.8.3-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rawsql-ts", - "version": "0.6.0-beta", + "version": "0.8.3-beta", "license": "MIT", "devDependencies": { "@types/benchmark": "^2.1.5", diff --git a/tsconfig.json b/tsconfig.json index b48d54b..a5b4d4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ From d468f8b715d289d34b2b6061249d6dd097321e2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 13:11:17 +0000 Subject: [PATCH 3/3] Implement CTEInjector with fixture support Co-authored-by: mk3008 <7686540+mk3008@users.noreply.github.com> --- README.md | 68 ++ package-lock.json | 667 +++++++++++++++++- package.json | 3 +- src/index.ts | 1 + src/transformers/CTEInjector.ts | 149 ++++ .../transformers/CTEInjector.fixtures.test.ts | 238 +++++++ 6 files changed, 1108 insertions(+), 18 deletions(-) create mode 100644 tests/transformers/CTEInjector.fixtures.test.ts diff --git a/README.md b/README.md index 693f591..b524335 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,13 @@ A suite of utilities for transforming and analyzing SQL ASTs. Collects all CTEs from WITH clauses, subqueries, and UNION queries. - **UpstreamSelectQueryFinder** Finds upstream SELECT queries that provide specific columns by traversing CTEs, subqueries, and UNION branches. +- **CTEInjector** + Inserts Common Table Expressions into queries. + - `inject(query, commonTables)`: Injects CTEs into AST query objects. + - `withFixtures(sql, deps, fixtures, options)`: Injects test fixtures as CTEs with explicit VALUES rows. + - `withNullScaffolding(sql, deps, options)`: Injects NULL-based scaffolding CTEs. + This is useful for testing a query's calculation logic without relying on a live database. + - **CTENormalizer** Consolidates all CTEs into a single root-level WITH clause. Throws an error if duplicate CTE names with different definitions are found. - **QueryNormalizer** @@ -567,6 +574,67 @@ console.log(formattedSql); --- +## Testing SQL with CTEInjector and Fixtures + +The `CTEInjector` class can be used to inject test data into a SQL query for testing purposes. This allows you to validate your SQL query's logic without needing a live database. + +```typescript +import { SelectQueryParser } from 'rawsql-ts'; +import { SchemaCollector } from 'rawsql-ts'; +import { CTEInjector, Fixtures } from 'rawsql-ts'; + +// SQL query under test +const sql = ` + SELECT u.name, SUM(o.total) AS sum_total + FROM users u + JOIN orders o ON o.user_id = u.id + GROUP BY u.name +`; + +// Parse the query and collect schema dependencies +const query = SelectQueryParser.parse(sql); +const schemaCollector = new SchemaCollector(); +const deps = schemaCollector.collect(query); + +// Provide test fixtures +const fixtures: Fixtures = { + users: [ + { id: 1, name: 'mike' }, + { id: 2, name: 'ken' }, + ], + orders: [ + { user_id: 1, total: 100 }, + { user_id: 1, total: 500 }, + { user_id: 2, total: 300 }, + ], +}; + +// Create the test SQL with injected fixtures +const injector = new CTEInjector(); +const testSQL = injector.withFixtures(sql, deps, fixtures); + +console.log(testSQL); +// Output: +// WITH +// users(id, name) AS ( +// VALUES +// (1, 'mike'), +// (2, 'ken') +// ), +// orders(total, user_id) AS ( +// VALUES +// (100, 1), +// (500, 1), +// (300, 2) +// ) +// SELECT u.name, SUM(o.total) AS sum_total +// FROM users u +// JOIN orders o ON o.user_id = u.id +// GROUP BY u.name +``` + +You can then run this SQL against any database engine (SQLite, DuckDB, PostgreSQL, etc.) for deterministic unit tests. + ## Benchmarks This project includes a comprehensive benchmark suite to evaluate the performance of `rawsql-ts` in comparison with other popular libraries such as `node-sql-parser` and `sql-formatter`. diff --git a/package-lock.json b/package-lock.json index d845a5c..5dbd750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "@vitest/coverage-v8": "^3.1.4", "benchmark": "^2.1.4", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.1", @@ -28,6 +29,91 @@ "vitest": "^1.5.2" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -671,6 +757,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -684,6 +798,32 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -694,6 +834,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -750,6 +900,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", @@ -1309,6 +1470,39 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -1465,6 +1659,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1753,6 +1960,20 @@ "dev": true, "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2236,6 +2457,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2274,6 +2512,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2317,6 +2576,13 @@ "node": ">=8" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -2374,6 +2640,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2417,6 +2693,87 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -2567,6 +2924,13 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -2577,6 +2941,34 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2659,6 +3051,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mlly": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", @@ -2870,6 +3272,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2903,6 +3312,23 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -3005,23 +3431,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", @@ -3325,6 +3734,110 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -3394,6 +3907,21 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3411,6 +3939,16 @@ "node": ">=14.0.0" } }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinyspy": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", @@ -3758,6 +4296,101 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 70385f8..ef7504c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", + "@vitest/coverage-v8": "^3.1.4", "benchmark": "^2.1.4", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.1", @@ -53,4 +54,4 @@ "files": [ "dist" ] -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index d314fc7..1a32e4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './models/ValueComponent'; export * from './models/ValuesQuery'; export * from './transformers/CTECollector'; +export * from './transformers/CTEInjector'; export * from './transformers/CTENormalizer'; export * from './transformers/Formatter'; export * from './transformers/SqlFormatter'; diff --git a/src/transformers/CTEInjector.ts b/src/transformers/CTEInjector.ts index 6caa203..456431b 100644 --- a/src/transformers/CTEInjector.ts +++ b/src/transformers/CTEInjector.ts @@ -2,6 +2,15 @@ import { CommonTable, WithClause } from "../models/Clause"; import { BinarySelectQuery, SelectQuery, SimpleSelectQuery, ValuesQuery } from "../models/SelectQuery"; import { CTECollector } from "./CTECollector"; import { CTEBuilder } from "./CTEBuilder"; +import { TableSchema } from "./SchemaCollector"; +import { SelectQueryParser } from "../parsers/SelectQueryParser"; +import { SchemaCollector } from "./SchemaCollector"; + +/** + * Represents a collection of fixtures (test data) to be injected into a SQL query. + * Each key is a table name, and the value is an array of objects representing rows. + */ +export type Fixtures = Record>>; /** * CTEInjector accepts a SelectQuery object and an array of CommonTables, @@ -49,6 +58,146 @@ export class CTEInjector { throw new Error("Unsupported query type"); } + /** + * Injects fixture-based Common Table Expressions into a SQL query. + * This allows testing queries with deterministic test data. + * + * @param sql The SQL query to inject fixtures into + * @param deps Schema information collected from the query + * @param fixtures Test data to be injected as CTEs + * @param options Options for CTE injection + * @returns A SQL string with injected fixture CTEs + */ + public withFixtures( + sql: string, + deps: TableSchema[], + fixtures: Fixtures, + options: { quoteIdentifiers?: boolean } = {} + ): string { + // If no fixtures provided, return the original SQL + if (!fixtures || Object.keys(fixtures).length === 0) { + return sql; + } + + const quoteIdentifiers = options.quoteIdentifiers ?? false; + const cteStatements: string[] = []; + + // For each table in the schema that has fixtures + for (const schema of deps) { + const tableName = schema.name; + const fixtureData = fixtures[tableName]; + + // Skip if no fixture data for this table + if (!fixtureData || fixtureData.length === 0) { + continue; + } + + // Get column names from the schema + const columns = schema.columns; + if (columns.length === 0) { + continue; + } + + // Build the CTE header with table and column names + const quotedTableName = this.quoteIdentifier(tableName, quoteIdentifiers); + const quotedColumns = columns.map(col => this.quoteIdentifier(col, quoteIdentifiers)); + let cte = `${quotedTableName}(${quotedColumns.join(', ')}) AS (\n VALUES\n`; + + // Process each row in the fixture data + const valueRows: string[] = fixtureData.map(row => { + const rowValues = columns.map(col => { + const value = col in row ? row[col] : null; + return this.formatValue(value); + }); + return ` (${rowValues.join(', ')})`; + }); + + cte += valueRows.join(',\n'); + cte += '\n)'; + cteStatements.push(cte); + } + + // If no CTEs were generated, return the original SQL + if (cteStatements.length === 0) { + return sql; + } + + // Combine the CTE statements with the original query + return `WITH\n${cteStatements.join(',\n')}\n${sql}`; + } + + /** + * Injects NULL-based scaffolding Common Table Expressions into a SQL query. + * This version creates CTEs with NULL values for all columns. + * + * @param sql The SQL query to inject NULL scaffolding into + * @param deps Schema information collected from the query + * @param options Options for CTE injection + * @returns A SQL string with injected scaffolding CTEs + */ + public withNullScaffolding( + sql: string, + deps: TableSchema[], + options: { quoteIdentifiers?: boolean } = {} + ): string { + // Create empty fixtures with one NULL row per table + const nullFixtures: Fixtures = {}; + + for (const schema of deps) { + const emptyRow: Record = {}; + // Initialize all columns to NULL + for (const col of schema.columns) { + emptyRow[col] = null; + } + nullFixtures[schema.name] = [emptyRow]; + } + + // Use the withFixtures method with our NULL fixtures + return this.withFixtures(sql, deps, nullFixtures, options); + } + + /** + * Formats a value as a SQL literal based on its JavaScript type. + * + * @param value The value to format + * @returns A SQL-formatted string representation of the value + */ + private formatValue(value: unknown): string { + if (value === null || value === undefined) { + return 'NULL'; + } + + // Handle different types + switch (typeof value) { + case 'string': + // Escape single quotes by doubling them + return `'${(value as string).replace(/'/g, "''")}'`; + case 'number': + return value.toString(); + case 'boolean': + return value ? 'TRUE' : 'FALSE'; + case 'object': + if (value instanceof Date) { + return `'${value.toISOString()}'`; + } + // For other objects, convert to JSON string + return `'${JSON.stringify(value).replace(/'/g, "''")}'`; + default: + return 'NULL'; + } + } + + /** + * Quotes an identifier if the quoteIdentifiers option is enabled. + * + * @param identifier The identifier to quote + * @param quoteIdentifiers Whether to quote identifiers + * @returns The quoted or unquoted identifier + */ + private quoteIdentifier(identifier: string, quoteIdentifiers: boolean): string { + return quoteIdentifiers ? `"${identifier}"` : identifier; + } + /** * Inserts Common Table Expressions into a SimpleSelectQuery. * diff --git a/tests/transformers/CTEInjector.fixtures.test.ts b/tests/transformers/CTEInjector.fixtures.test.ts new file mode 100644 index 0000000..3ec7459 --- /dev/null +++ b/tests/transformers/CTEInjector.fixtures.test.ts @@ -0,0 +1,238 @@ +import { describe, expect, test } from 'vitest'; +import { CTEInjector, Fixtures } from '../../src/transformers/CTEInjector'; +import { TableSchema } from '../../src/transformers/SchemaCollector'; +import { SelectQueryParser } from '../../src/parsers/SelectQueryParser'; +import { SchemaCollector } from '../../src/transformers/SchemaCollector'; + +describe('CTEInjector (Fixtures)', () => { + test('withFixtures returns original SQL when fixtures are empty', () => { + // Arrange + const sql = 'SELECT * FROM users'; + const deps = [new TableSchema('users', ['id', 'name'])]; + const fixtures: Fixtures = {}; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toBe(sql); + }); + + test('withFixtures injects simple fixture data correctly', () => { + // Arrange + const sql = 'SELECT name, age FROM users WHERE age > 21'; + const deps = [new TableSchema('users', ['id', 'name', 'age'])]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'Alice', age: 30 }, + { id: 2, name: 'Bob', age: 25 } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain('WITH'); + expect(result).toContain('users(id, name, age) AS ('); + expect(result).toContain('VALUES'); + expect(result).toContain('(1, \'Alice\', 30)'); + expect(result).toContain('(2, \'Bob\', 25)'); + expect(result).toContain('SELECT name, age FROM users WHERE age > 21'); + }); + + test('withFixtures handles NULL values correctly', () => { + // Arrange + const sql = 'SELECT name, email FROM users'; + const deps = [new TableSchema('users', ['id', 'name', 'email'])]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'Alice', email: null }, + { id: 2, name: 'Bob', email: 'bob@example.com' } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain('(1, \'Alice\', NULL)'); + expect(result).toContain('(2, \'Bob\', \'bob@example.com\')'); + }); + + test('withFixtures handles missing columns by filling with NULL', () => { + // Arrange + const sql = 'SELECT name, age FROM users'; + const deps = [new TableSchema('users', ['id', 'name', 'age', 'email'])]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'Alice' }, // age and email missing + { id: 2, name: 'Bob', age: 25 } // email missing + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain('(1, \'Alice\', NULL, NULL)'); + expect(result).toContain('(2, \'Bob\', 25, NULL)'); + }); + + test('withFixtures handles different value types correctly', () => { + // Arrange + const sql = 'SELECT * FROM data'; + const deps = [new TableSchema('data', ['id', 'name', 'active', 'created_at', 'score', 'metadata'])]; + const now = new Date('2023-01-01T12:00:00Z'); + const fixtures: Fixtures = { + data: [ + { + id: 1, + name: 'Item 1', + active: true, + created_at: now, + score: 9.5, + metadata: { tags: ['test', 'sample'] } + } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain(`(1, 'Item 1', TRUE, '${now.toISOString()}', 9.5, '{\"tags\":[\"test\",\"sample\"]}')`); + }); + + test('withFixtures quotes identifiers when quoteIdentifiers is true', () => { + // Arrange + const sql = 'SELECT name FROM users'; + const deps = [new TableSchema('users', ['id', 'name'])]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'Alice' } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures, { quoteIdentifiers: true }); + + // Assert + expect(result).toContain('"users"("id", "name") AS ('); + }); + + test('withFixtures handles multiple tables correctly', () => { + // Arrange + const sql = 'SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id'; + const deps = [ + new TableSchema('users', ['id', 'name']), + new TableSchema('orders', ['id', 'user_id', 'product']) + ]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' } + ], + orders: [ + { id: 101, user_id: 1, product: 'Widget' }, + { id: 102, user_id: 1, product: 'Gadget' }, + { id: 103, user_id: 2, product: 'Thingamajig' } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain('users(id, name) AS ('); + expect(result).toContain('orders(id, user_id, product) AS ('); + expect(result).toContain('(1, \'Alice\')'); + expect(result).toContain('(101, 1, \'Widget\')'); + }); + + test('withFixtures properly escapes single quotes in string values', () => { + // Arrange + const sql = 'SELECT name FROM users'; + const deps = [new TableSchema('users', ['id', 'name'])]; + const fixtures: Fixtures = { + users: [ + { id: 1, name: "O'Brien" }, + { id: 2, name: "User's data" } + ] + }; + const injector = new CTEInjector(); + + // Act + const result = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(result).toContain('(1, \'O\'\'Brien\')'); + expect(result).toContain('(2, \'User\'\'s data\')'); + }); + + test('withNullScaffolding creates CTEs with NULL values', () => { + // Arrange + const sql = 'SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id'; + const deps = [ + new TableSchema('users', ['id', 'name']), + new TableSchema('orders', ['id', 'user_id', 'product']) + ]; + const injector = new CTEInjector(); + + // Act + const result = injector.withNullScaffolding(sql, deps); + + // Assert + expect(result).toContain('users(id, name) AS ('); + expect(result).toContain('orders(id, user_id, product) AS ('); + expect(result).toContain('(NULL, NULL)'); + expect(result).toContain('(NULL, NULL, NULL)'); + }); + + test('end-to-end test with a real SQL query and fixtures', () => { + // Arrange + const sql = ` + SELECT u.name, SUM(o.total) AS sum_total + FROM users u + JOIN orders o ON o.user_id = u.id + GROUP BY u.name + `; + + // Parse and collect schema + const query = SelectQueryParser.parse(sql); + const schemaCollector = new SchemaCollector(); + const deps = schemaCollector.collect(query); + + // Create fixtures + const fixtures: Fixtures = { + users: [ + { id: 1, name: 'mike' }, + { id: 2, name: 'ken' }, + ], + orders: [ + { user_id: 1, total: 100 }, + { user_id: 1, total: 500 }, + { user_id: 2, total: 300 }, + ], + }; + + const injector = new CTEInjector(); + + // Act + const testSQL = injector.withFixtures(sql, deps, fixtures); + + // Assert + expect(testSQL).toContain('WITH'); + expect(testSQL).toContain('users(id, name) AS ('); + expect(testSQL).toContain('orders(total, user_id) AS ('); // Note: SchemaCollector sorts columns alphabetically + expect(testSQL).toContain('(1, \'mike\')'); + expect(testSQL).toContain('(100, 1)'); + }); +}); \ No newline at end of file