From 420013079c7d1c570521c09ab1637e561acc217b Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:20:48 +0000 Subject: [PATCH 01/13] Add relational algebra mode with RA-to-SQL transpiler --- README.md | 2 +- package-lock.json | 353 ++++++- package.json | 3 +- src/App.tsx | 145 ++- src/ra-engine/LICENSE.md | 51 + src/ra-engine/relationalAlgebra.test.ts | 693 +++++++++++++ src/ra-engine/relationalAlgebra.ts | 1178 +++++++++++++++++++++++ 7 files changed, 2387 insertions(+), 38 deletions(-) create mode 100644 src/ra-engine/LICENSE.md create mode 100644 src/ra-engine/relationalAlgebra.test.ts create mode 100644 src/ra-engine/relationalAlgebra.ts diff --git a/README.md b/README.md index ba09e07..47f63ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SQL Validator -SQL Validator is a fully client-side web application powered by sql.js. Designed for the database course at Stockholm University, it provides an interface for students to write, format, and run SQL queries directly in their browser without the need for a database server. Results can be compared to expected results based on a question bank, and views can be created and managed using the browser's local storage. +SQL Validator is a fully client-side web application powered by sql.js. It provides an interface for writing, formatting, and running SQL queries directly in the browser without the need for a database server. Results can be compared to expected results based on a question bank, and views can be created and managed using the browser's local storage. ## Features - **Fully Client-Side Execution**: All SQL validation and execution is performed in the browser using sql.js SQLite. diff --git a/package-lock.json b/package-lock.json index 47ccd8e..c1dd6e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,8 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.15.0", "vite": "^7.1.10", - "vite-plugin-node-polyfills": "^0.24.0" + "vite-plugin-node-polyfills": "^0.24.0", + "vitest": "^4.1.0" } }, "node_modules/@acemir/cssom": { @@ -2817,6 +2818,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", @@ -3008,12 +3016,30 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/emscripten": { "version": "1.41.5", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", @@ -3405,6 +3431,129 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3713,6 +3862,16 @@ "util": "^0.12.5" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4123,6 +4282,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -5041,6 +5210,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5522,6 +5698,16 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8019,6 +8205,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8235,6 +8432,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -9518,6 +9722,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -9630,6 +9841,20 @@ "obliterator": "^2.0.1" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -10146,6 +10371,23 @@ "node": ">=0.6.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -10162,6 +10404,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "7.0.25", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", @@ -10709,6 +10961,88 @@ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -10879,6 +11213,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 74989cb..06ab4eb 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.15.0", "vite": "^7.1.10", - "vite-plugin-node-polyfills": "^0.24.0" + "vite-plugin-node-polyfills": "^0.24.0", + "vitest": "^4.1.0" } } diff --git a/src/App.tsx b/src/App.tsx index eb2170d..8a1b21d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,7 @@ import "prismjs/themes/prism.css"; import { format } from "sql-formatter"; import initSqlJs from "sql.js"; import ViewsTable, { View } from "./ViewsTable"; +import { raToSQL, RAError } from "./ra-engine/relationalAlgebra"; import { Info, Settings, XCircle, CheckCircle2, Eye, EyeOff } from "lucide-react"; import ChangelogDialog from "./ChangelogDialog"; @@ -78,6 +79,15 @@ function App() { const [exportQuery, setExportQuery] = useState(); const [exportingStatus, setExportingStatus] = useState(0); const [loadedQuestionCorrect, setLoadedQuestionCorrect] = useState(false); + const [editorMode, setEditorMode] = useState<"sql" | "ra">("sql"); + const [generatedSQL, setGeneratedSQL] = useState(null); + + const switchEditorMode = useCallback((mode: "sql" | "ra") => { + setEditorMode(mode); + setGeneratedSQL(null); + setError(null); + resetResult(); + }, [resetResult]); const exportRendererRef = useRef(null); const editorRef = useRef(null); @@ -239,23 +249,38 @@ function App() { setWrittenQuestions(wq); } - try { - // Check for multiple statements - let stmtCount = 0; - for (const stmt of database.iterateStatements(query)) { - stmtCount++; - stmt.free(); - if (stmtCount > 1) { - setError(t("multipleStatements")); - return; + if (editorMode === "sql") { + try { + // Check for multiple statements + let stmtCount = 0; + for (const stmt of database.iterateStatements(query)) { + stmtCount++; + stmt.free(); + if (stmtCount > 1) { + setError(t("multipleStatements")); + return; + } + } + setError(null); + } catch (e) { + // @ts-expect-error - Error.message is a string + setError(e.message); + } + } else { + // In RA mode, try to parse to check for errors + try { + raToSQL(query); + setError(null); + } catch (e) { + if (e instanceof RAError) { + setError(t("raParseError", { message: e.message })); + } else { + // @ts-expect-error - Error.message is a string + setError(e.message); } } - setError(null); - } catch (e) { - // @ts-expect-error - Error.message is a string - setError(e.message); } - }, [database, query, question, lang, defaultQuery, t]); + }, [database, query, question, lang, defaultQuery, t, editorMode]); const refreshViews = useCallback((upsert: boolean) => { @@ -307,7 +332,24 @@ function App() { return; } try { - const res = database.exec(query); + let sqlToRun = query; + if (editorMode === "ra") { + try { + sqlToRun = raToSQL(query, database); + setGeneratedSQL(sqlToRun); + } catch (e) { + if (e instanceof RAError) { + setError(t("raParseError", { message: e.message })); + } else { + // @ts-expect-error - Error.message is a string + setError(e.message); + } + return; + } + } else { + setGeneratedSQL(null); + } + const res = database.exec(sqlToRun); setIsViewResult(false); setQueryedView(null); setEvaluatedQuery(query); @@ -322,7 +364,7 @@ function App() { // @ts-expect-error - Error.message is a string setError(e.message); } - }, [database, query, refreshViews]); + }, [database, query, refreshViews, editorMode, t]); const evalSql = useCallback((sql: string): Result => { if (!database) { @@ -429,7 +471,7 @@ function App() { // Update mismatch & loadedQuestionCorrect flags when query is changed useEffect(() => { - if (!database || !question || query === undefined) { + if (!database || !question || query === undefined || editorMode === "ra") { return; } @@ -817,7 +859,23 @@ function App() { {/* Query Section with Header */}
- {t("query")} +
+ {t("query")} +
+ + +
+
)} - + {editorMode === "sql" && ( + + )}
+ {editorMode === "ra" && generatedSQL && ( +
+ {t("generatedSQL")} + { try { return format(generatedSQL, { language: "sqlite", tabWidth: 2, useTabs: false, keywordCase: "upper", dataTypeCase: "upper", functionCase: "upper" }); } catch { return generatedSQL; } })()} + onValueChange={() => null} + highlight={code => highlight(code, languages.sql)} + padding={10} + tabSize={2} + className="font-mono text-sm w-full dark:bg-slate-800/50 bg-slate-50 border dark:border-slate-700 border-gray-200 rounded-md mt-1" + /> +
+ )} + exportData({include})} ref={exportModalRef} /> +- **Licensed Work:** ra-engine + The Licensed Work is (c) 2026 E.SU. IT AB and Edwin Sundberg. +- **Change Date:** 2035-03-20 +- **Change License:** GNU General Public License v3.0 or later + +**Terms** + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. diff --git a/src/ra-engine/relationalAlgebra.test.ts b/src/ra-engine/relationalAlgebra.test.ts new file mode 100644 index 0000000..46f42c7 --- /dev/null +++ b/src/ra-engine/relationalAlgebra.test.ts @@ -0,0 +1,693 @@ +import { describe, it, expect } from "vitest"; +import { raToSQL, RAError } from "./relationalAlgebra"; + +// ─── Helper ───────────────────────────────────────────────────────────────── + +/** Normalize whitespace for comparison */ +function norm(s: string): string { + return s.replace(/\s+/g, " ").trim(); +} + +// ─── Basic table references ───────────────────────────────────────────────── + +describe("table references", () => { + it("should return a bare table name", () => { + expect(raToSQL("Person")).toBe("Person"); + }); + + it("should handle parenthesised table name", () => { + expect(raToSQL("(Person)")).toBe("Person"); + }); + + it("should handle trailing semicolon", () => { + expect(raToSQL("Person;")).toBe("Person"); + }); +}); + +// ─── Selection (σ) ────────────────────────────────────────────────────────── + +describe("selection (σ)", () => { + it("should transpile σ with Unicode symbol", () => { + expect(norm(raToSQL("σ[age > 20](Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should transpile sigma keyword", () => { + expect(norm(raToSQL("sigma[age > 20](Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should transpile select keyword", () => { + expect(norm(raToSQL("select[age > 20](Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should handle string comparison", () => { + expect(norm(raToSQL("σ[name = 'Alice'](Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE name = 'Alice'") + ); + }); + + it("should handle compound conditions with AND/OR", () => { + const sql = raToSQL("σ[age > 20 and name = 'Alice'](Person)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (Person) WHERE (age > 20 AND name = 'Alice')") + ); + }); + + it("should handle NOT condition", () => { + const sql = raToSQL("σ[not age > 20](Person)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (Person) WHERE NOT (age > 20)") + ); + }); + + it("should handle table.column references in conditions", () => { + const sql = raToSQL("σ[Person.age > 20](Person)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (Person) WHERE Person.age > 20") + ); + }); + + it("should handle nested OR and AND", () => { + const sql = raToSQL("σ[age > 20 or (name = 'Alice' and city = 'Stockholm')](Person)"); + expect(norm(sql)).toContain("OR"); + expect(norm(sql)).toContain("AND"); + }); + + it("should handle all comparison operators", () => { + expect(norm(raToSQL("σ[a = 1](T)"))).toContain("= 1"); + expect(norm(raToSQL("σ[a <> 1](T)"))).toContain("<> 1"); + expect(norm(raToSQL("σ[a != 1](T)"))).toContain("<> 1"); + expect(norm(raToSQL("σ[a < 1](T)"))).toContain("< 1"); + expect(norm(raToSQL("σ[a > 1](T)"))).toContain("> 1"); + expect(norm(raToSQL("σ[a <= 1](T)"))).toContain("<= 1"); + expect(norm(raToSQL("σ[a >= 1](T)"))).toContain(">= 1"); + }); + + it("should handle function calls in conditions", () => { + const sql = raToSQL("σ[YEAR(startDate) = 2024](CourseInstance)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (CourseInstance) WHERE YEAR(startDate) = 2024") + ); + }); +}); + +// ─── Projection (π) ───────────────────────────────────────────────────────── + +describe("projection (π)", () => { + it("should transpile π with Unicode", () => { + expect(norm(raToSQL("π[name, age](Person)"))).toBe( + norm("SELECT name, age FROM (Person)") + ); + }); + + it("should transpile pi keyword", () => { + expect(norm(raToSQL("pi[name](Person)"))).toBe( + norm("SELECT name FROM (Person)") + ); + }); + + it("should transpile project keyword", () => { + expect(norm(raToSQL("project[name, city](Person)"))).toBe( + norm("SELECT name, city FROM (Person)") + ); + }); + + it("should handle table.column references", () => { + expect(norm(raToSQL("π[Person.name](Person)"))).toBe( + norm("SELECT Person.name FROM (Person)") + ); + }); +}); + +// ─── Rename (ρ) ───────────────────────────────────────────────────────────── + +describe("rename (ρ)", () => { + it("should transpile ρ with Unicode arrow", () => { + expect(norm(raToSQL("ρ[name→fullName](Person)"))).toBe( + norm("SELECT name AS fullName FROM (Person)") + ); + }); + + it("should transpile rho with ASCII arrow", () => { + expect(norm(raToSQL("rho[name->fullName](Person)"))).toBe( + norm("SELECT name AS fullName FROM (Person)") + ); + }); + + it("should handle multiple rename mappings", () => { + const sql = raToSQL("ρ[name→fullName, age→years](Person)"); + expect(norm(sql)).toBe( + norm("SELECT name AS fullName, age AS years FROM (Person)") + ); + }); +}); + +// ─── Cross product (×) ───────────────────────────────────────────────────── + +describe("cross product (×)", () => { + it("should transpile × operator", () => { + const sql = raToSQL("Person × Course"); + expect(norm(sql)).toContain("CROSS JOIN"); + }); + + it("should transpile cross keyword", () => { + const sql = raToSQL("Person cross Course"); + expect(norm(sql)).toContain("CROSS JOIN"); + }); +}); + +// ─── Natural join (⋈) ────────────────────────────────────────────────────── + +describe("natural join (⋈)", () => { + it("should transpile ⋈ operator", () => { + const sql = raToSQL("Person ⋈ Student"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should transpile natjoin keyword", () => { + const sql = raToSQL("Person natjoin Student"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should error on impossible natural join when database is provided", () => { + // Mock database that returns different columns for two tables + const mockDb = { + exec: (sql: string) => { + if (sql.includes("TableA")) { + return [{ columns: ["id", "name"], values: [] }]; + } + if (sql.includes("TableB")) { + return [{ columns: ["code", "description"], values: [] }]; + } + return [{ columns: [], values: [] }]; + }, + }; + + expect(() => raToSQL("TableA ⋈ TableB", mockDb)).toThrow(RAError); + expect(() => raToSQL("TableA ⋈ TableB", mockDb)).toThrow(/no common columns/i); + }); + + it("should not error on valid natural join when database is provided", () => { + const mockDb = { + exec: (sql: string) => { + if (sql.includes("Person")) { + return [{ columns: ["id", "name", "city"], values: [] }]; + } + if (sql.includes("Student")) { + return [{ columns: ["id", "hasDisability"], values: [] }]; + } + return [{ columns: [], values: [] }]; + }, + }; + + expect(() => raToSQL("Person ⋈ Student", mockDb)).not.toThrow(); + }); + + it("should not error when no database is provided (parse-only mode)", () => { + expect(() => raToSQL("TableA ⋈ TableB")).not.toThrow(); + }); +}); + +// ─── Theta join (⋈[cond]) ────────────────────────────────────────────────── + +describe("theta join (⋈[cond])", () => { + it("should transpile ⋈ with condition", () => { + const sql = raToSQL("Person ⋈[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("JOIN"); + expect(norm(sql)).toContain("ON Person.id = Student.id"); + expect(norm(sql)).not.toContain("NATURAL"); + }); + + it("should transpile join keyword with condition", () => { + const sql = raToSQL("Person join[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("JOIN"); + expect(norm(sql)).toContain("ON Person.id = Student.id"); + }); +}); + +// ─── Set operations ───────────────────────────────────────────────────────── + +describe("set operations", () => { + it("should transpile union with ∪", () => { + const sql = raToSQL("π[name](Person) ∪ π[name](Teacher)"); + expect(norm(sql)).toContain("UNION"); + }); + + it("should transpile union keyword", () => { + const sql = raToSQL("π[name](Person) union π[name](Teacher)"); + expect(norm(sql)).toContain("UNION"); + }); + + it("should transpile intersect with ∩", () => { + const sql = raToSQL("π[name](Person) ∩ π[name](Teacher)"); + expect(norm(sql)).toContain("INTERSECT"); + }); + + it("should transpile difference with −", () => { + const sql = raToSQL("π[name](Person) − π[name](Teacher)"); + expect(norm(sql)).toContain("EXCEPT"); + }); + + it("should transpile minus keyword", () => { + const sql = raToSQL("π[name](Person) minus π[name](Teacher)"); + expect(norm(sql)).toContain("EXCEPT"); + }); + + it("should transpile backslash as set difference", () => { + const sql = raToSQL("A \\ B"); + expect(norm(sql)).toContain("EXCEPT"); + }); +}); + +// ─── Outer joins ──────────────────────────────────────────────────────────── + +describe("outer joins", () => { + it("should transpile left outer join", () => { + const sql = raToSQL("Person leftjoin[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("LEFT JOIN"); + expect(norm(sql)).toContain("ON Person.id = Student.id"); + }); + + it("should transpile right outer join", () => { + const sql = raToSQL("Person rightjoin[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("RIGHT JOIN"); + }); + + it("should transpile full outer join", () => { + const sql = raToSQL("Person fulljoin[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("FULL OUTER JOIN"); + }); + + it("should transpile left outer join with Unicode ⟕", () => { + const sql = raToSQL("Person ⟕[Person.id = Student.id] Student"); + expect(norm(sql)).toContain("LEFT JOIN"); + }); +}); + +// ─── Semi-joins and anti-join ─────────────────────────────────────────────── + +describe("semi-joins and anti-join", () => { + it("should transpile left semi-join", () => { + const sql = raToSQL("Person leftsemijoin Student"); + expect(norm(sql)).toContain("WHERE EXISTS"); + }); + + it("should transpile left semi-join with ⋉", () => { + const sql = raToSQL("Person ⋉ Student"); + expect(norm(sql)).toContain("WHERE EXISTS"); + }); + + it("should transpile right semi-join", () => { + const sql = raToSQL("Person rightsemijoin Student"); + expect(norm(sql)).toContain("WHERE EXISTS"); + }); + + it("should transpile anti-join", () => { + const sql = raToSQL("Person antijoin Student"); + expect(norm(sql)).toContain("WHERE NOT EXISTS"); + }); + + it("should transpile anti-join with ▷", () => { + const sql = raToSQL("Person ▷ Student"); + expect(norm(sql)).toContain("WHERE NOT EXISTS"); + }); +}); + +// ─── Division (÷) ────────────────────────────────────────────────────────── + +describe("division (÷)", () => { + it("should transpile ÷ operator", () => { + const sql = raToSQL("A ÷ B"); + expect(norm(sql)).toContain("NOT EXISTS"); + }); + + it("should transpile divide keyword", () => { + const sql = raToSQL("A divide B"); + expect(norm(sql)).toContain("NOT EXISTS"); + }); +}); + +// ─── Distinct (δ) ────────────────────────────────────────────────────────── + +describe("distinct (δ)", () => { + it("should transpile δ", () => { + expect(norm(raToSQL("δ(Person)"))).toBe( + norm("SELECT DISTINCT * FROM (Person)") + ); + }); + + it("should transpile delta keyword", () => { + expect(norm(raToSQL("delta(Person)"))).toBe( + norm("SELECT DISTINCT * FROM (Person)") + ); + }); + + it("should transpile distinct keyword", () => { + expect(norm(raToSQL("distinct(Person)"))).toBe( + norm("SELECT DISTINCT * FROM (Person)") + ); + }); +}); + +// ─── Sorting (τ) ──────────────────────────────────────────────────────────── + +describe("sorting (τ)", () => { + it("should transpile τ with single column", () => { + const sql = raToSQL("τ[name](Person)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (Person) ORDER BY name") + ); + }); + + it("should transpile τ with DESC", () => { + const sql = raToSQL("τ[age DESC](Person)"); + expect(norm(sql)).toBe( + norm("SELECT * FROM (Person) ORDER BY age DESC") + ); + }); + + it("should transpile sort keyword", () => { + const sql = raToSQL("sort[name](Person)"); + expect(norm(sql)).toContain("ORDER BY name"); + }); + + it("should handle multiple sort columns", () => { + const sql = raToSQL("τ[name, age DESC](Person)"); + expect(norm(sql)).toContain("ORDER BY name, age DESC"); + }); +}); + +// ─── Grouping/Aggregation (γ) ────────────────────────────────────────────── + +describe("grouping/aggregation (γ)", () => { + it("should transpile γ with group-by and aggregate", () => { + const sql = raToSQL("γ[city; COUNT(id)](Person)"); + expect(norm(sql)).toContain("COUNT(id)"); + expect(norm(sql)).toContain("GROUP BY city"); + }); + + it("should handle aggregate with alias", () => { + const sql = raToSQL("γ[city; COUNT(id) AS total](Person)"); + expect(norm(sql)).toContain("COUNT(id) AS total"); + expect(norm(sql)).toContain("GROUP BY city"); + }); + + it("should transpile gamma keyword", () => { + const sql = raToSQL("gamma[city; SUM(price) AS totalPrice](Course)"); + expect(norm(sql)).toContain("SUM(price) AS totalPrice"); + expect(norm(sql)).toContain("GROUP BY city"); + }); +}); + +// ─── Composition / nesting ────────────────────────────────────────────────── + +describe("composition and nesting", () => { + it("should handle projection of selection", () => { + const sql = raToSQL("π[name](σ[age > 20](Person))"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE age > 20"); + }); + + it("should handle selection of join", () => { + const sql = raToSQL("σ[age > 20](Person ⋈ Student)"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should handle complex nested expression", () => { + const sql = raToSQL("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE city = 'Stockholm'"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should handle parenthesised subexpressions", () => { + const sql = raToSQL("(Person ⋈ Student) ∪ (Person ⋈ Teacher)"); + expect(norm(sql)).toContain("UNION"); + expect(norm(sql).match(/NATURAL JOIN/g)?.length).toBe(2); + }); + + it("should handle deeply nested expressions", () => { + const sql = raToSQL("π[name](σ[age > 20](ρ[id→personId](Person)))"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("id AS personId"); + }); +}); + +// ─── Operator precedence ─────────────────────────────────────────────────── + +describe("operator precedence", () => { + it("should bind join tighter than union", () => { + // A ⋈ B ∪ C should be (A ⋈ B) ∪ C, not A ⋈ (B ∪ C) + const sql = raToSQL("A ⋈ B ∪ C"); + // The NATURAL JOIN should come before UNION in the SQL + expect(norm(sql)).toMatch(/NATURAL JOIN.*UNION/); + }); + + it("should bind intersect tighter than union", () => { + const sql = raToSQL("A ∪ B ∩ C"); + // B ∩ C should be grouped together + expect(norm(sql)).toContain("INTERSECT"); + expect(norm(sql)).toContain("UNION"); + }); +}); + +// ─── Error handling ───────────────────────────────────────────────────────── + +describe("error handling", () => { + it("should throw RAError on empty input", () => { + expect(() => raToSQL("")).toThrow(RAError); + }); + + it("should throw RAError on invalid syntax", () => { + expect(() => raToSQL("σ[](Person)")).toThrow(RAError); + }); + + it("should throw RAError on unclosed bracket", () => { + expect(() => raToSQL("σ[age > 20(Person)")).toThrow(RAError); + }); + + it("should throw RAError on unexpected token after expression", () => { + expect(() => raToSQL("Person Student")).toThrow(RAError); + }); + + it("should throw RAError on unterminated string", () => { + expect(() => raToSQL("σ[name = 'Alice](Person)")).toThrow(RAError); + }); + + it("should throw RAError on missing condition in selection", () => { + expect(() => raToSQL("σ(Person)")).toThrow(RAError); + }); + + it("should throw RAError on missing relation for selection", () => { + expect(() => raToSQL("σ[age > 20]")).toThrow(RAError); + }); +}); + +// ─── Multi-line / assignments (←) ────────────────────────────────────────── + +describe("multi-line and assignments", () => { + it("should handle simple assignment with ←", () => { + const sql = raToSQL("A ← Person\nA"); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + }); + + it("should handle simple assignment with <-", () => { + const sql = raToSQL("A <- Person\nA"); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + }); + + it("should handle assignment with complex expression", () => { + const sql = raToSQL("Students ← σ[age > 20](Person)\nπ[name](Students)"); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("Students AS"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("SELECT name FROM"); + }); + + it("should handle multiple assignments", () => { + const input = [ + "A ← σ[city = 'Stockholm'](Person)", + "B ← π[name](A)", + "B", + ].join("\n"); + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + expect(norm(sql)).toContain("B AS"); + expect(norm(sql)).toContain("WHERE city = 'Stockholm'"); + expect(norm(sql)).toContain("SELECT name FROM"); + }); + + it("should handle assignment chaining with joins", () => { + const input = [ + "PS ← Person ⋈ Student", + "Result ← π[name](PS)", + "Result", + ].join("\n"); + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("PS AS"); + expect(norm(sql)).toContain("NATURAL JOIN"); + expect(norm(sql)).toContain("Result AS"); + }); + + it("should handle semicolons as statement separators", () => { + const sql = raToSQL("A ← Person; π[name](A)"); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + expect(norm(sql)).toContain("SELECT name FROM"); + }); + + it("should handle mixed newlines and semicolons", () => { + const input = "A ← Person;\nB ← Student\nA ⋈ B"; + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + expect(norm(sql)).toContain("B AS"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should still work with single-line expressions (no assignments)", () => { + // No regression — single-line without assignment should work as before + expect(raToSQL("Person")).toBe("Person"); + expect(norm(raToSQL("π[name](Person)"))).toBe( + norm("SELECT name FROM (Person)") + ); + }); + + it("should handle -- comments in multi-line input", () => { + const input = [ + "-- First get students", + "A ← σ[age > 20](Person)", + "-- Then project names", + "π[name](A)", + ].join("\n"); + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("SELECT name FROM"); + }); + + it("should handle trailing newlines", () => { + const sql = raToSQL("Person\n\n\n"); + expect(sql).toBe("Person"); + }); + + it("should handle leading newlines", () => { + const sql = raToSQL("\n\n\nPerson"); + expect(sql).toBe("Person"); + }); + + it("should handle the student example: A <- pi[student](sigma[name='peter'](P))", () => { + const input = "A <- π[student](σ[name = 'Peter'](Participation))\nA"; + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("A AS"); + expect(norm(sql)).toContain("SELECT student FROM"); + expect(norm(sql)).toContain("WHERE name = 'Peter'"); + }); +}); + +// ─── Alternative notation styles ──────────────────────────────────────────── + +describe("curly braces (LaTeX-style)", () => { + it("should support σ_{condition}(R)", () => { + expect(norm(raToSQL("σ_{age > 20}(Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should support π_{cols}(R)", () => { + expect(norm(raToSQL("π_{name, city}(Person)"))).toBe( + norm("SELECT name, city FROM (Person)") + ); + }); + + it("should support ρ_{old→new}(R)", () => { + expect(norm(raToSQL("ρ_{name→fullName}(Person)"))).toBe( + norm("SELECT name AS fullName FROM (Person)") + ); + }); + + it("should support curly braces without underscore", () => { + expect(norm(raToSQL("σ{age > 20}(Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should support theta join with curly braces", () => { + const sql = raToSQL("Person ⋈{Person.id = Student.id} Student"); + expect(norm(sql)).toContain("JOIN"); + expect(norm(sql)).toContain("ON Person.id = Student.id"); + }); +}); + +describe("implicit subscripts (no brackets)", () => { + it("should support σ condition (R)", () => { + expect(norm(raToSQL("σ age > 20 (Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should support π cols (R)", () => { + expect(norm(raToSQL("π name, city (Person)"))).toBe( + norm("SELECT name, city FROM (Person)") + ); + }); + + it("should support sigma condition (R) with keyword", () => { + expect(norm(raToSQL("sigma age > 20 (Person)"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should support pi cols (R) with keyword", () => { + expect(norm(raToSQL("pi name (Person)"))).toBe( + norm("SELECT name FROM (Person)") + ); + }); + + it("should support ρ old→new (R) implicit", () => { + expect(norm(raToSQL("ρ name→fullName (Person)"))).toBe( + norm("SELECT name AS fullName FROM (Person)") + ); + }); + + it("should support τ col (R) implicit", () => { + expect(norm(raToSQL("τ name (Person)"))).toContain("ORDER BY name"); + }); + + it("should support nested implicit subscripts", () => { + const sql = raToSQL("π name (σ age > 20 (Person))"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE age > 20"); + }); + + it("should support compound implicit condition with AND", () => { + const sql = raToSQL("σ age > 20 and city = 'Stockholm' (Person)"); + expect(norm(sql)).toContain("WHERE"); + expect(norm(sql)).toContain("AND"); + expect(norm(sql)).toContain("age > 20"); + }); + + it("should work with multi-line and implicit subscripts", () => { + const input = [ + "A <- σ age > 20 (Person)", + "π name (A)", + ].join("\n"); + const sql = raToSQL(input); + expect(norm(sql)).toContain("WITH"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("SELECT name FROM"); + }); +}); diff --git a/src/ra-engine/relationalAlgebra.ts b/src/ra-engine/relationalAlgebra.ts new file mode 100644 index 0000000..865070d --- /dev/null +++ b/src/ra-engine/relationalAlgebra.ts @@ -0,0 +1,1178 @@ +/* +Relational Algebra engine for sql-validator +Copyright (C) 2026 E.SU. IT AB (Org.no 559484-0505) and Edwin Sundberg + +Licensed under the Business Source License 1.1 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License in the LICENSE.md file in this repository. +*/ + +// ─── Token Types ──────────────────────────────────────────────────────────── + +enum TokenType { + // Unary operators + SIGMA = "SIGMA", // σ or sigma + PI = "PI", // π or pi + RHO = "RHO", // ρ or rho + GAMMA = "GAMMA", // γ or gamma (grouping/aggregation) + TAU = "TAU", // τ or tau (sorting) + DELTA = "DELTA", // δ or delta (duplicate elimination / DISTINCT) + // Binary operators + CROSS = "CROSS", // × or cross + NATJOIN = "NATJOIN", // ⋈ or natjoin + JOIN = "JOIN", // join (theta join with condition) + UNION = "UNION", // ∪ or union + INTERSECT = "INTERSECT", // ∩ or intersect + MINUS = "MINUS", // − or minus or \ + DIVIDE = "DIVIDE", // ÷ or divide + LEFTJOIN = "LEFTJOIN", // ⟕ or leftjoin + RIGHTJOIN = "RIGHTJOIN", // ⟖ or rightjoin + FULLJOIN = "FULLJOIN", // ⟗ or fulljoin + LEFTSEMIJOIN = "LEFTSEMIJOIN", // ⋉ or leftsemijoin + RIGHTSEMIJOIN = "RIGHTSEMIJOIN", // ⋊ or rightsemijoin + ANTIJOIN = "ANTIJOIN", // ▷ or antijoin + // Delimiters + LPAREN = "LPAREN", // ( + RPAREN = "RPAREN", // ) + LBRACKET = "LBRACKET", // [ + RBRACKET = "RBRACKET", // ] + COMMA = "COMMA", // , + // Identifiers, literals + IDENTIFIER = "IDENTIFIER", + NUMBER = "NUMBER", + STRING = "STRING", // 'value' + // Condition tokens (passed through inside brackets) + CONDITION = "CONDITION", + // Operators within conditions + EQ = "EQ", // = + NEQ = "NEQ", // <> or != + LT = "LT", // < + GT = "GT", // > + LTE = "LTE", // <= + GTE = "GTE", // >= + AND = "AND", + OR = "OR", + NOT = "NOT", + DOT = "DOT", // . + ARROW = "ARROW", // → or -> + ASSIGN = "ASSIGN", // ← or <- + SEMICOLON = "SEMICOLON", + NEWLINE = "NEWLINE", // line break (statement separator) + EOF = "EOF", +} + +interface Token { + type: TokenType; + value: string; + pos: number; +} + +// ─── Tokenizer ────────────────────────────────────────────────────────────── + +// Unicode symbol → TokenType map +const UNICODE_OPS: Record = { + "σ": TokenType.SIGMA, + "π": TokenType.PI, + "ρ": TokenType.RHO, + "γ": TokenType.GAMMA, + "τ": TokenType.TAU, + "δ": TokenType.DELTA, + "×": TokenType.CROSS, + "⋈": TokenType.NATJOIN, + "∪": TokenType.UNION, + "∩": TokenType.INTERSECT, + "−": TokenType.MINUS, + "÷": TokenType.DIVIDE, + "⟕": TokenType.LEFTJOIN, + "⟖": TokenType.RIGHTJOIN, + "⟗": TokenType.FULLJOIN, + "⋉": TokenType.LEFTSEMIJOIN, + "⋊": TokenType.RIGHTSEMIJOIN, + "▷": TokenType.ANTIJOIN, + "←": TokenType.ASSIGN, +}; + +// Keyword → TokenType map (case-insensitive) +const KEYWORD_OPS: Record = { + "sigma": TokenType.SIGMA, + "select": TokenType.SIGMA, + "pi": TokenType.PI, + "project": TokenType.PI, + "rho": TokenType.RHO, + "rename": TokenType.RHO, + "gamma": TokenType.GAMMA, + "tau": TokenType.TAU, + "sort": TokenType.TAU, + "delta": TokenType.DELTA, + "distinct": TokenType.DELTA, + "cross": TokenType.CROSS, + "natjoin": TokenType.NATJOIN, + "join": TokenType.JOIN, + "union": TokenType.UNION, + "intersect": TokenType.INTERSECT, + "minus": TokenType.MINUS, + "divide": TokenType.DIVIDE, + "leftjoin": TokenType.LEFTJOIN, + "rightjoin": TokenType.RIGHTJOIN, + "fulljoin": TokenType.FULLJOIN, + "leftsemijoin": TokenType.LEFTSEMIJOIN, + "rightsemijoin": TokenType.RIGHTSEMIJOIN, + "antijoin": TokenType.ANTIJOIN, + "and": TokenType.AND, + "or": TokenType.OR, + "not": TokenType.NOT, +}; + +function tokenize(input: string): Token[] { + const tokens: Token[] = []; + let i = 0; + + while (i < input.length) { + // Skip comments (lines starting with --) + if (input[i] === "-" && i + 1 < input.length && input[i + 1] === "-") { + while (i < input.length && input[i] !== "\n") i++; + continue; + } + + // Emit newlines as statement separators, skip other whitespace + if (input[i] === "\n" || input[i] === "\r") { + // Skip consecutive newlines and emit at most one NEWLINE token + while (i < input.length && (input[i] === "\n" || input[i] === "\r")) i++; + // Only emit if there are tokens before this (not leading newlines) + // and the last token isn't already a newline/semicolon + if (tokens.length > 0 && + tokens[tokens.length - 1].type !== TokenType.NEWLINE && + tokens[tokens.length - 1].type !== TokenType.SEMICOLON) { + tokens.push({ type: TokenType.NEWLINE, value: "\\n", pos: i }); + } + continue; + } + if (/\s/.test(input[i])) { + i++; + continue; + } + + const pos = i; + const ch = input[i]; + + // Check for Unicode operators + if (UNICODE_OPS[ch]) { + tokens.push({ type: UNICODE_OPS[ch], value: ch, pos }); + i++; + continue; + } + + // Single-char tokens + if (ch === "(") { tokens.push({ type: TokenType.LPAREN, value: ch, pos }); i++; continue; } + if (ch === ")") { tokens.push({ type: TokenType.RPAREN, value: ch, pos }); i++; continue; } + if (ch === "[") { tokens.push({ type: TokenType.LBRACKET, value: ch, pos }); i++; continue; } + if (ch === "]") { tokens.push({ type: TokenType.RBRACKET, value: ch, pos }); i++; continue; } + // Curly braces as alternative to square brackets (LaTeX-style σ_{cond}) + if (ch === "{") { tokens.push({ type: TokenType.LBRACKET, value: ch, pos }); i++; continue; } + if (ch === "}") { tokens.push({ type: TokenType.RBRACKET, value: ch, pos }); i++; continue; } + // Underscore: skip silently (allows LaTeX-style σ_{cond} — the _ is just decoration) + if (ch === "_") { i++; continue; } + if (ch === ",") { tokens.push({ type: TokenType.COMMA, value: ch, pos }); i++; continue; } + if (ch === ".") { tokens.push({ type: TokenType.DOT, value: ch, pos }); i++; continue; } + if (ch === ";") { tokens.push({ type: TokenType.SEMICOLON, value: ch, pos }); i++; continue; } + if (ch === "\\") { tokens.push({ type: TokenType.MINUS, value: ch, pos }); i++; continue; } + + // Arrow: → or -> + if (ch === "→") { tokens.push({ type: TokenType.ARROW, value: ch, pos }); i++; continue; } + if (ch === "-" && i + 1 < input.length && input[i + 1] === ">") { + tokens.push({ type: TokenType.ARROW, value: "->", pos }); + i += 2; + continue; + } + + // Assignment: <- (must be checked before comparison operators) + if (ch === "<" && i + 1 < input.length && input[i + 1] === "-") { + tokens.push({ type: TokenType.ASSIGN, value: "<-", pos }); i += 2; continue; + } + + // Comparison operators + if (ch === "=" ) { tokens.push({ type: TokenType.EQ, value: "=", pos }); i++; continue; } + if (ch === "<" && i + 1 < input.length && input[i + 1] === ">") { + tokens.push({ type: TokenType.NEQ, value: "<>", pos }); i += 2; continue; + } + if (ch === "!" && i + 1 < input.length && input[i + 1] === "=") { + tokens.push({ type: TokenType.NEQ, value: "!=", pos }); i += 2; continue; + } + if (ch === "<" && i + 1 < input.length && input[i + 1] === "=") { + tokens.push({ type: TokenType.LTE, value: "<=", pos }); i += 2; continue; + } + if (ch === ">" && i + 1 < input.length && input[i + 1] === "=") { + tokens.push({ type: TokenType.GTE, value: ">=", pos }); i += 2; continue; + } + if (ch === "<") { tokens.push({ type: TokenType.LT, value: "<", pos }); i++; continue; } + if (ch === ">") { tokens.push({ type: TokenType.GT, value: ">", pos }); i++; continue; } + + // Minus (as set difference) when not part of -> + if (ch === "-") { tokens.push({ type: TokenType.MINUS, value: "-", pos }); i++; continue; } + + // String literals + if (ch === "'") { + let str = ""; + i++; // skip opening quote + while (i < input.length && input[i] !== "'") { + if (input[i] === "\\" && i + 1 < input.length) { + str += input[i + 1]; + i += 2; + } else { + str += input[i]; + i++; + } + } + if (i >= input.length) throw new RAError(`Unterminated string literal at position ${pos}`); + i++; // skip closing quote + tokens.push({ type: TokenType.STRING, value: str, pos }); + continue; + } + + // Numbers + if (/\d/.test(ch)) { + let num = ""; + while (i < input.length && /[\d.]/.test(input[i])) { + num += input[i]; + i++; + } + tokens.push({ type: TokenType.NUMBER, value: num, pos }); + continue; + } + + // Identifiers and keywords + if (/[a-zA-Z_\u00C0-\u024F]/.test(ch)) { + let ident = ""; + while (i < input.length && /[a-zA-Z0-9_\u00C0-\u024F]/.test(input[i])) { + ident += input[i]; + i++; + } + const lower = ident.toLowerCase(); + if (KEYWORD_OPS[lower]) { + tokens.push({ type: KEYWORD_OPS[lower], value: ident, pos }); + } else { + tokens.push({ type: TokenType.IDENTIFIER, value: ident, pos }); + } + continue; + } + + throw new RAError(`Unexpected character '${ch}' at position ${pos}`); + } + + tokens.push({ type: TokenType.EOF, value: "", pos: i }); + return tokens; +} + +// ─── AST Node Types ───────────────────────────────────────────────────────── + +type RANode = + | TableNode + | SelectionNode + | ProjectionNode + | RenameNode + | GroupNode + | SortNode + | DistinctNode + | CrossProductNode + | NaturalJoinNode + | ThetaJoinNode + | LeftJoinNode + | RightJoinNode + | FullJoinNode + | LeftSemiJoinNode + | RightSemiJoinNode + | AntiJoinNode + | UnionNode + | IntersectNode + | DifferenceNode + | DivisionNode; + +interface TableNode { type: "table"; name: string } +interface SelectionNode { type: "selection"; condition: ConditionNode; relation: RANode } +interface ProjectionNode { type: "projection"; columns: ColumnRef[]; relation: RANode } +interface RenameNode { type: "rename"; mappings: RenameMapping[]; relation: RANode } +interface GroupNode { type: "group"; groupBy: ColumnRef[]; aggregates: AggregateExpr[]; relation: RANode } +interface SortNode { type: "sort"; columns: SortColumn[]; relation: RANode } +interface DistinctNode { type: "distinct"; relation: RANode } +interface CrossProductNode { type: "crossProduct"; left: RANode; right: RANode } +interface NaturalJoinNode { type: "naturalJoin"; left: RANode; right: RANode } +interface ThetaJoinNode { type: "thetaJoin"; condition: ConditionNode; left: RANode; right: RANode } +interface LeftJoinNode { type: "leftJoin"; condition: ConditionNode; left: RANode; right: RANode } +interface RightJoinNode { type: "rightJoin"; condition: ConditionNode; left: RANode; right: RANode } +interface FullJoinNode { type: "fullJoin"; condition: ConditionNode; left: RANode; right: RANode } +interface LeftSemiJoinNode { type: "leftSemiJoin"; left: RANode; right: RANode } +interface RightSemiJoinNode { type: "rightSemiJoin"; left: RANode; right: RANode } +interface AntiJoinNode { type: "antiJoin"; left: RANode; right: RANode } +interface UnionNode { type: "union"; left: RANode; right: RANode } +interface IntersectNode { type: "intersect"; left: RANode; right: RANode } +interface DifferenceNode { type: "difference"; left: RANode; right: RANode } +interface DivisionNode { type: "division"; left: RANode; right: RANode } + +interface ColumnRef { table?: string; column: string } +interface RenameMapping { from: string; to: string } +interface SortColumn { column: ColumnRef; desc: boolean } +interface AggregateExpr { func: string; column: ColumnRef; alias?: string } + +type ConditionNode = + | ComparisonNode + | AndNode + | OrNode + | NotNode; + +interface ComparisonNode { type: "comparison"; left: ValueExpr; op: string; right: ValueExpr } +interface AndNode { type: "and"; left: ConditionNode; right: ConditionNode } +interface OrNode { type: "or"; left: ConditionNode; right: ConditionNode } +interface NotNode { type: "not"; operand: ConditionNode } + +type ValueExpr = + | ColumnRefExpr + | StringLiteral + | NumberLiteral + | FunctionCallExpr; + +interface ColumnRefExpr { type: "columnRef"; ref: ColumnRef } +interface StringLiteral { type: "string"; value: string } +interface NumberLiteral { type: "number"; value: string } +interface FunctionCallExpr { type: "functionCall"; name: string; args: ValueExpr[] } + +// ─── Error Type ───────────────────────────────────────────────────────────── + +export class RAError extends Error { + constructor(message: string) { + super(message); + this.name = "RAError"; + } +} + +// ─── Parser ───────────────────────────────────────────────────────────────── + +interface RAProgram { + assignments: { name: string; expr: RANode }[]; + result: RANode; +} + +class Parser { + private tokens: Token[]; + private pos: number; + + constructor(tokens: Token[]) { + this.tokens = tokens; + this.pos = 0; + } + + private peek(): Token { + return this.tokens[this.pos]; + } + + private advance(): Token { + const token = this.tokens[this.pos]; + this.pos++; + return token; + } + + private expect(type: TokenType): Token { + const token = this.peek(); + if (token.type !== type) { + throw new RAError(`Expected ${type} but got ${token.type} ('${token.value}') at position ${token.pos}`); + } + return this.advance(); + } + + private match(type: TokenType): boolean { + if (this.peek().type === type) { + this.advance(); + return true; + } + return false; + } + + // Grammar (precedence low→high): + // expr = union_expr + // union_expr = intersect_expr (( ∪ | − ) intersect_expr)* + // intersect_expr = join_expr ( ∩ join_expr)* + // join_expr = unary_expr (( × | ⋈ | ⋈[cond] | join[cond] | ÷ | ⟕[cond] | ⟖[cond] | ⟗[cond] | ⋉ | ⋊ | ▷ ) unary_expr)* + // unary_expr = (σ[cond] | π[cols] | ρ[mappings] | γ[...] | τ[...] | δ) unary_expr | primary + // primary = IDENTIFIER | '(' expr ')' + + parse(): RAProgram { + const assignments: { name: string; expr: RANode }[] = []; + + // Skip leading newlines + while (this.peek().type === TokenType.NEWLINE) this.advance(); + + while (this.peek().type !== TokenType.EOF) { + // Try to detect assignment: IDENTIFIER (← | <-) expr + if (this.peek().type === TokenType.IDENTIFIER) { + const saved = this.pos; + const name = this.advance().value; + if (this.peek().type === TokenType.ASSIGN) { + this.advance(); // consume ← / <- + const expr = this.parseUnionExpr(); + assignments.push({ name, expr }); + // Consume statement separator (newline, semicolon, or EOF) + while (this.peek().type === TokenType.SEMICOLON || this.peek().type === TokenType.NEWLINE) { + this.advance(); + } + continue; + } + // Not an assignment — backtrack and parse as expression + this.pos = saved; + } + + // Parse the final result expression + const result = this.parseUnionExpr(); + // Consume trailing separators + while (this.peek().type === TokenType.SEMICOLON || this.peek().type === TokenType.NEWLINE) { + this.advance(); + } + if (this.peek().type !== TokenType.EOF) { + throw new RAError(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`); + } + return { assignments, result }; + } + + throw new RAError("Empty expression"); + } + + private parseUnionExpr(): RANode { + let left = this.parseIntersectExpr(); + while (this.peek().type === TokenType.UNION || this.peek().type === TokenType.MINUS) { + const op = this.advance(); + const right = this.parseIntersectExpr(); + if (op.type === TokenType.UNION) { + left = { type: "union", left, right }; + } else { + left = { type: "difference", left, right }; + } + } + return left; + } + + private parseIntersectExpr(): RANode { + let left = this.parseJoinExpr(); + while (this.peek().type === TokenType.INTERSECT) { + this.advance(); + const right = this.parseJoinExpr(); + left = { type: "intersect", left, right }; + } + return left; + } + + private parseJoinExpr(): RANode { + let left = this.parseUnaryExpr(); + // eslint-disable-next-line no-constant-condition + while (true) { + const t = this.peek().type; + if (t === TokenType.CROSS) { + this.advance(); + const right = this.parseUnaryExpr(); + left = { type: "crossProduct", left, right }; + } else if (t === TokenType.NATJOIN) { + this.advance(); + // Check if there's a condition: ⋈[cond] + if (this.peek().type === TokenType.LBRACKET) { + const condition = this.parseBracketedCondition(); + const right = this.parseUnaryExpr(); + left = { type: "thetaJoin", condition, left, right }; + } else { + const right = this.parseUnaryExpr(); + left = { type: "naturalJoin", left, right }; + } + } else if (t === TokenType.JOIN) { + this.advance(); + if (this.peek().type === TokenType.LBRACKET) { + const condition = this.parseBracketedCondition(); + const right = this.parseUnaryExpr(); + left = { type: "thetaJoin", condition, left, right }; + } else { + // join without condition = natural join + const right = this.parseUnaryExpr(); + left = { type: "naturalJoin", left, right }; + } + } else if (t === TokenType.DIVIDE) { + this.advance(); + const right = this.parseUnaryExpr(); + left = { type: "division", left, right }; + } else if (t === TokenType.LEFTJOIN) { + this.advance(); + const condition = this.parseBracketedCondition(); + const right = this.parseUnaryExpr(); + left = { type: "leftJoin", condition, left, right }; + } else if (t === TokenType.RIGHTJOIN) { + this.advance(); + const condition = this.parseBracketedCondition(); + const right = this.parseUnaryExpr(); + left = { type: "rightJoin", condition, left, right }; + } else if (t === TokenType.FULLJOIN) { + this.advance(); + const condition = this.parseBracketedCondition(); + const right = this.parseUnaryExpr(); + left = { type: "fullJoin", condition, left, right }; + } else if (t === TokenType.LEFTSEMIJOIN) { + this.advance(); + const right = this.parseUnaryExpr(); + left = { type: "leftSemiJoin", left, right }; + } else if (t === TokenType.RIGHTSEMIJOIN) { + this.advance(); + const right = this.parseUnaryExpr(); + left = { type: "rightSemiJoin", left, right }; + } else if (t === TokenType.ANTIJOIN) { + this.advance(); + const right = this.parseUnaryExpr(); + left = { type: "antiJoin", left, right }; + } else { + break; + } + } + return left; + } + + /** + * Check if the next token starts a subscript (bracket or implicit). + * Returns true if it's `[`, `{`, or any non-`(` token that could begin + * an implicit subscript (i.e., the subscript content before `(`). + */ + private hasSubscript(): boolean { + return this.peek().type === TokenType.LBRACKET || this.peek().type !== TokenType.LPAREN; + } + + /** + * Parse a subscript that may be: + * 1. Bracketed: `[...]` or `{...}` + * 2. Implicit: tokens up to the next unmatched `(` + * + * For implicit subscripts, we temporarily inject LBRACKET/RBRACKET + * around the subscript tokens so the same parsing logic can be reused. + */ + private parseSubscriptCondition(): ConditionNode { + if (this.peek().type === TokenType.LBRACKET) { + return this.parseBracketedCondition(); + } + // Implicit subscript: parse condition tokens until we hit LPAREN + // We parse directly — the condition ends when we encounter LPAREN at depth 0 + const cond = this.parseOrCondition(); + return cond; + } + + private parseSubscriptColumns(): ColumnRef[] { + if (this.peek().type === TokenType.LBRACKET) { + this.expect(TokenType.LBRACKET); + const cols = this.parseColumnList(); + this.expect(TokenType.RBRACKET); + return cols; + } + // Implicit: parse column list until LPAREN + return this.parseColumnList(); + } + + private parseSubscriptRenameMappings(): RenameMapping[] { + if (this.peek().type === TokenType.LBRACKET) { + this.expect(TokenType.LBRACKET); + const mappings = this.parseRenameMappings(); + this.expect(TokenType.RBRACKET); + return mappings; + } + return this.parseRenameMappings(); + } + + private parseSubscriptGroupSpec(): { groupBy: ColumnRef[]; aggregates: AggregateExpr[] } { + if (this.peek().type === TokenType.LBRACKET) { + this.expect(TokenType.LBRACKET); + const spec = this.parseGroupSpec(); + this.expect(TokenType.RBRACKET); + return spec; + } + return this.parseGroupSpec(); + } + + private parseSubscriptSortColumns(): SortColumn[] { + if (this.peek().type === TokenType.LBRACKET) { + this.expect(TokenType.LBRACKET); + const cols = this.parseSortColumns(); + this.expect(TokenType.RBRACKET); + return cols; + } + return this.parseSortColumns(); + } + + private parseUnaryExpr(): RANode { + const t = this.peek().type; + + if (t === TokenType.SIGMA) { + this.advance(); + if (this.hasSubscript()) { + const condition = this.parseSubscriptCondition(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "selection", condition, relation }; + } + // σ(R) with no subscript — error + throw new RAError("Selection (σ) requires a condition — use σ[condition](R) or σ condition (R)"); + } + + if (t === TokenType.PI) { + this.advance(); + if (this.hasSubscript()) { + const columns = this.parseSubscriptColumns(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "projection", columns, relation }; + } + throw new RAError("Projection (π) requires column list — use π[cols](R) or π cols (R)"); + } + + if (t === TokenType.RHO) { + this.advance(); + if (this.hasSubscript()) { + const mappings = this.parseSubscriptRenameMappings(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "rename", mappings, relation }; + } + throw new RAError("Rename (ρ) requires mappings — use ρ[old→new](R) or ρ old→new (R)"); + } + + if (t === TokenType.GAMMA) { + this.advance(); + if (this.hasSubscript()) { + const { groupBy, aggregates } = this.parseSubscriptGroupSpec(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "group", groupBy, aggregates, relation }; + } + throw new RAError("Grouping (γ) requires specification — use γ[groupCols; AGG(col)](R)"); + } + + if (t === TokenType.TAU) { + this.advance(); + if (this.hasSubscript()) { + const columns = this.parseSubscriptSortColumns(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "sort", columns, relation }; + } + throw new RAError("Sort (τ) requires column list — use τ[col](R) or τ col (R)"); + } + + if (t === TokenType.DELTA) { + this.advance(); + this.expect(TokenType.LPAREN); + const relation = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return { type: "distinct", relation }; + } + + return this.parsePrimary(); + } + + private parsePrimary(): RANode { + if (this.peek().type === TokenType.LPAREN) { + this.advance(); + const expr = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return expr; + } + + if (this.peek().type === TokenType.IDENTIFIER) { + const name = this.advance().value; + return { type: "table", name }; + } + + throw new RAError(`Expected table name or '(' at position ${this.peek().pos}, got '${this.peek().value}'`); + } + + // ─── Condition parsing ────────────────────────────────────── + + private parseBracketedCondition(): ConditionNode { + this.expect(TokenType.LBRACKET); + const cond = this.parseOrCondition(); + this.expect(TokenType.RBRACKET); + return cond; + } + + private parseOrCondition(): ConditionNode { + let left = this.parseAndCondition(); + while (this.peek().type === TokenType.OR) { + this.advance(); + const right = this.parseAndCondition(); + left = { type: "or", left, right }; + } + return left; + } + + private parseAndCondition(): ConditionNode { + let left = this.parseNotCondition(); + while (this.peek().type === TokenType.AND) { + this.advance(); + const right = this.parseNotCondition(); + left = { type: "and", left, right }; + } + return left; + } + + private parseNotCondition(): ConditionNode { + if (this.peek().type === TokenType.NOT) { + this.advance(); + const operand = this.parseNotCondition(); + return { type: "not", operand }; + } + return this.parseComparisonCondition(); + } + + private parseComparisonCondition(): ConditionNode { + if (this.peek().type === TokenType.LPAREN) { + this.advance(); + const cond = this.parseOrCondition(); + this.expect(TokenType.RPAREN); + return cond; + } + + const left = this.parseValueExpr(); + const opToken = this.peek(); + let op: string; + if (opToken.type === TokenType.EQ) { op = "="; this.advance(); } + else if (opToken.type === TokenType.NEQ) { op = "<>"; this.advance(); } + else if (opToken.type === TokenType.LT) { op = "<"; this.advance(); } + else if (opToken.type === TokenType.GT) { op = ">"; this.advance(); } + else if (opToken.type === TokenType.LTE) { op = "<="; this.advance(); } + else if (opToken.type === TokenType.GTE) { op = ">="; this.advance(); } + else { + throw new RAError(`Expected comparison operator at position ${opToken.pos}, got '${opToken.value}'`); + } + const right = this.parseValueExpr(); + return { type: "comparison", left, op, right }; + } + + private parseValueExpr(): ValueExpr { + if (this.peek().type === TokenType.STRING) { + const token = this.advance(); + return { type: "string", value: token.value }; + } + if (this.peek().type === TokenType.NUMBER) { + const token = this.advance(); + return { type: "number", value: token.value }; + } + if (this.peek().type === TokenType.IDENTIFIER) { + const name = this.advance().value; + + // Check if it's a function call: NAME(args) + if (this.peek().type === TokenType.LPAREN) { + this.advance(); + const args: ValueExpr[] = []; + if (this.peek().type !== TokenType.RPAREN) { + args.push(this.parseValueExpr()); + while (this.match(TokenType.COMMA)) { + args.push(this.parseValueExpr()); + } + } + this.expect(TokenType.RPAREN); + return { type: "functionCall", name, args }; + } + + // Check for table.column + if (this.peek().type === TokenType.DOT) { + this.advance(); + const col = this.expect(TokenType.IDENTIFIER).value; + return { type: "columnRef", ref: { table: name, column: col } }; + } + return { type: "columnRef", ref: { column: name } }; + } + throw new RAError(`Expected value expression at position ${this.peek().pos}, got '${this.peek().value}'`); + } + + // ─── Column list parsing ──────────────────────────────────── + + private parseColumnList(): ColumnRef[] { + const columns: ColumnRef[] = []; + columns.push(this.parseColumnRef()); + while (this.match(TokenType.COMMA)) { + columns.push(this.parseColumnRef()); + } + return columns; + } + + private parseColumnRef(): ColumnRef { + const name = this.expect(TokenType.IDENTIFIER).value; + if (this.peek().type === TokenType.DOT) { + this.advance(); + const col = this.expect(TokenType.IDENTIFIER).value; + return { table: name, column: col }; + } + return { column: name }; + } + + // ─── Rename mappings ─────────────────────────────────────── + + private parseRenameMappings(): RenameMapping[] { + const mappings: RenameMapping[] = []; + mappings.push(this.parseRenameMapping()); + while (this.match(TokenType.COMMA)) { + mappings.push(this.parseRenameMapping()); + } + return mappings; + } + + private parseRenameMapping(): RenameMapping { + const from = this.expect(TokenType.IDENTIFIER).value; + this.expect(TokenType.ARROW); + const to = this.expect(TokenType.IDENTIFIER).value; + return { from, to }; + } + + // ─── Group/aggregate spec ────────────────────────────────── + // Format: γ[groupCol1, groupCol2; COUNT(col) AS alias, SUM(col2)] + // The semicolon separates group-by columns from aggregates + + private parseGroupSpec(): { groupBy: ColumnRef[]; aggregates: AggregateExpr[] } { + const groupBy: ColumnRef[] = []; + const aggregates: AggregateExpr[] = []; + + // Parse group-by columns (before semicolon) + if (this.peek().type === TokenType.IDENTIFIER) { + // Check if this is a group-by column or an aggregate function + const saved = this.pos; + const name = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + // It's an aggregate function, backtrack + this.pos = saved; + } else if (this.peek().type === TokenType.DOT) { + // It's a table.column group-by + this.pos = saved; + groupBy.push(this.parseColumnRef()); + while (this.peek().type === TokenType.COMMA) { + this.advance(); + // Check if next item is aggregate or column + const saved2 = this.pos; + this.expect(TokenType.IDENTIFIER); + if (this.peek().type === TokenType.LPAREN) { + this.pos = saved2; + break; + } + this.pos = saved2; + groupBy.push(this.parseColumnRef()); + } + } else if (this.peek().type === TokenType.SEMICOLON) { + // Single group-by column followed by semicolon + groupBy.push({ column: name }); + this.advance(); // consume semicolon + } else if (this.peek().type === TokenType.COMMA) { + groupBy.push({ column: name }); + while (this.peek().type === TokenType.COMMA) { + this.advance(); + if (this.peek().type === TokenType.IDENTIFIER) { + const saved3 = this.pos; + const nextName = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + this.pos = saved3; + break; + } + if (this.peek().type === TokenType.SEMICOLON) { + groupBy.push({ column: nextName }); + this.advance(); + break; + } + this.pos = saved3; + groupBy.push(this.parseColumnRef()); + } + } + } else { + groupBy.push({ column: name }); + } + } + + // Handle semicolon separator if not yet consumed + if (this.peek().type === TokenType.SEMICOLON) { + this.advance(); + } + + // Parse aggregates + while (this.peek().type === TokenType.IDENTIFIER) { + const funcName = this.advance().value.toUpperCase(); + this.expect(TokenType.LPAREN); + const col = this.parseColumnRef(); + this.expect(TokenType.RPAREN); + let alias: string | undefined; + // Check for AS alias + if (this.peek().type === TokenType.IDENTIFIER && this.peek().value.toUpperCase() === "AS") { + this.advance(); + alias = this.expect(TokenType.IDENTIFIER).value; + } + aggregates.push({ func: funcName, column: col, alias }); + if (!this.match(TokenType.COMMA)) break; + } + + return { groupBy, aggregates }; + } + + // ─── Sort columns ────────────────────────────────────────── + // Format: τ[col1, col2 DESC] + + private parseSortColumns(): SortColumn[] { + const columns: SortColumn[] = []; + const col = this.parseColumnRef(); + let desc = false; + if (this.peek().type === TokenType.IDENTIFIER && this.peek().value.toUpperCase() === "DESC") { + this.advance(); + desc = true; + } else if (this.peek().type === TokenType.IDENTIFIER && this.peek().value.toUpperCase() === "ASC") { + this.advance(); + } + columns.push({ column: col, desc }); + + while (this.match(TokenType.COMMA)) { + const c = this.parseColumnRef(); + let d = false; + if (this.peek().type === TokenType.IDENTIFIER && this.peek().value.toUpperCase() === "DESC") { + this.advance(); + d = true; + } else if (this.peek().type === TokenType.IDENTIFIER && this.peek().value.toUpperCase() === "ASC") { + this.advance(); + } + columns.push({ column: c, desc: d }); + } + + return columns; + } +} + +// ─── SQL Code Generator ───────────────────────────────────────────────────── + +function conditionToSQL(cond: ConditionNode): string { + switch (cond.type) { + case "comparison": + return `${valueExprToSQL(cond.left)} ${cond.op} ${valueExprToSQL(cond.right)}`; + case "and": + return `(${conditionToSQL(cond.left)} AND ${conditionToSQL(cond.right)})`; + case "or": + return `(${conditionToSQL(cond.left)} OR ${conditionToSQL(cond.right)})`; + case "not": + return `NOT (${conditionToSQL(cond.operand)})`; + } +} + +function valueExprToSQL(expr: ValueExpr): string { + switch (expr.type) { + case "columnRef": + return columnRefToSQL(expr.ref); + case "string": + return `'${expr.value.replace(/'/g, "''")}'`; + case "number": + return expr.value; + case "functionCall": + return `${expr.name}(${expr.args.map(valueExprToSQL).join(", ")})`; + } +} + +function columnRefToSQL(ref: ColumnRef): string { + if (ref.table) { + return `${ref.table}.${ref.column}`; + } + return ref.column; +} + +let subqueryCounter = 0; + +/** + * Interface for a minimal database handle used to resolve column names. + * Compatible with sql.js Database. + */ +interface DatabaseHandle { + exec(sql: string): { columns: string[]; values: unknown[][] }[]; +} + +/** + * Resolve the column names produced by a SQL expression using the database. + * Executes a LIMIT 0 query to get column metadata without fetching data. + */ +function resolveColumns(sql: string, db: DatabaseHandle): string[] { + const res = db.exec(`SELECT * FROM (${sql}) LIMIT 0`); + if (res.length === 0) return []; + return res[0].columns; +} + +function nodeToSQL(node: RANode, db?: DatabaseHandle): string { + switch (node.type) { + case "table": + return node.name; + + case "selection": + return `SELECT * FROM (${nodeToSQL(node.relation, db)}) WHERE ${conditionToSQL(node.condition)}`; + + case "projection": + return `SELECT ${node.columns.map(columnRefToSQL).join(", ")} FROM (${nodeToSQL(node.relation, db)})`; + + case "rename": { + const inner = nodeToSQL(node.relation, db); + // Simple case: rename columns + const colMappings = node.mappings.map(m => `${m.from} AS ${m.to}`).join(", "); + return `SELECT ${colMappings} FROM (${inner})`; + } + + case "group": { + const inner = nodeToSQL(node.relation, db); + const groupCols = node.groupBy.map(columnRefToSQL); + const aggExprs = node.aggregates.map(a => { + const expr = `${a.func}(${columnRefToSQL(a.column)})`; + return a.alias ? `${expr} AS ${a.alias}` : expr; + }); + const selectParts = [...groupCols, ...aggExprs].join(", "); + const groupByClause = groupCols.length > 0 ? ` GROUP BY ${groupCols.join(", ")}` : ""; + return `SELECT ${selectParts} FROM (${inner})${groupByClause}`; + } + + case "sort": { + const inner = nodeToSQL(node.relation, db); + const orderParts = node.columns.map(c => + `${columnRefToSQL(c.column)}${c.desc ? " DESC" : ""}` + ).join(", "); + return `SELECT * FROM (${inner}) ORDER BY ${orderParts}`; + } + + case "distinct": + return `SELECT DISTINCT * FROM (${nodeToSQL(node.relation, db)})`; + + case "crossProduct": + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} CROSS JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++}`; + + case "naturalJoin": { + const leftSQL = nodeToSQL(node.left, db); + const rightSQL = nodeToSQL(node.right, db); + // Validate that there are common columns when a database is available + if (db) { + const leftCols = resolveColumns(leftSQL, db); + const rightCols = resolveColumns(rightSQL, db); + const common = leftCols.filter(c => rightCols.includes(c)); + if (common.length === 0) { + throw new RAError( + "Natural join has no common columns between the two relations. " + + "Left columns: [" + leftCols.join(", ") + "], Right columns: [" + rightCols.join(", ") + "]. " + + "Use a cross product (×) if a cartesian product is intended, or a theta join (⋈[condition]) to specify the join condition." + ); + } + } + return `SELECT * FROM (${leftSQL}) AS _ra${subqueryCounter++} NATURAL JOIN (${rightSQL}) AS _ra${subqueryCounter++}`; + } + + case "thetaJoin": + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + + case "leftJoin": + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} LEFT JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + + case "rightJoin": + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} RIGHT JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + + case "fullJoin": + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} FULL OUTER JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + + case "leftSemiJoin": { + const lAlias = `_ra${subqueryCounter++}`; + const rAlias = `_ra${subqueryCounter++}`; + return `SELECT ${lAlias}.* FROM (${nodeToSQL(node.left, db)}) AS ${lAlias} WHERE EXISTS (SELECT 1 FROM (${nodeToSQL(node.right, db)}) AS ${rAlias})`; + } + + case "rightSemiJoin": { + const lAlias = `_ra${subqueryCounter++}`; + const rAlias = `_ra${subqueryCounter++}`; + return `SELECT ${rAlias}.* FROM (${nodeToSQL(node.right, db)}) AS ${rAlias} WHERE EXISTS (SELECT 1 FROM (${nodeToSQL(node.left, db)}) AS ${lAlias})`; + } + + case "antiJoin": { + const lAlias = `_ra${subqueryCounter++}`; + const rAlias = `_ra${subqueryCounter++}`; + return `SELECT ${lAlias}.* FROM (${nodeToSQL(node.left, db)}) AS ${lAlias} WHERE NOT EXISTS (SELECT 1 FROM (${nodeToSQL(node.right, db)}) AS ${rAlias})`; + } + + case "union": + return `${nodeToSQL(node.left, db)} UNION ${nodeToSQL(node.right, db)}`; + + case "intersect": + return `${nodeToSQL(node.left, db)} INTERSECT ${nodeToSQL(node.right, db)}`; + + case "difference": + return `${nodeToSQL(node.left, db)} EXCEPT ${nodeToSQL(node.right, db)}`; + + case "division": { + const lAlias = `_ra${subqueryCounter++}`; + const rAlias = `_ra${subqueryCounter++}`; + const crossAlias = `_ra${subqueryCounter++}`; + const innerAlias = `_ra${subqueryCounter++}`; + const leftSQL = nodeToSQL(node.left, db); + const rightSQL = nodeToSQL(node.right, db); + return `SELECT * FROM (SELECT DISTINCT * FROM (${leftSQL}) AS ${lAlias}) AS ${innerAlias} WHERE NOT EXISTS (SELECT * FROM (${rightSQL}) AS ${rAlias} WHERE NOT EXISTS (SELECT * FROM (${leftSQL}) AS ${crossAlias}))`; + } + } +} + +function programToSQL(program: RAProgram, db?: DatabaseHandle): string { + if (program.assignments.length === 0) { + return nodeToSQL(program.result, db); + } + + // Use CTEs (WITH clauses) for assignments + const ctes = program.assignments.map(a => { + const sql = nodeToSQL(a.expr, db); + // Wrap non-table expressions in SELECT * FROM (...) for CTE compatibility + const wrappedSQL = /^\w+$/.test(sql) ? `SELECT * FROM ${sql}` : sql; + return `${a.name} AS (${wrappedSQL})`; + }); + + const resultSQL = nodeToSQL(program.result, db); + // Wrap bare table reference in SELECT for the final expression + const wrappedResult = /^\w+$/.test(resultSQL) ? `SELECT * FROM ${resultSQL}` : resultSQL; + + return `WITH ${ctes.join(", ")} ${wrappedResult}`; +} + +// ─── Public API ───────────────────────────────────────────────────────────── + +/** + * Parse a relational algebra expression and transpile it to SQL. + * + * Supported syntax: + * + * **Unary operators** (prefix, with condition/columns in brackets): + * - `σ[condition](R)` or `sigma[condition](R)` — Selection (WHERE) + * - `π[col1, col2](R)` or `pi[col1, col2](R)` — Projection (SELECT) + * - `ρ[old→new](R)` or `rho[old->new](R)` — Rename + * - `γ[groupCol; COUNT(col) AS alias](R)` or `gamma[...]` — Grouping/Aggregation + * - `τ[col DESC](R)` or `tau[col](R)` — Sorting (ORDER BY) + * - `δ(R)` or `delta(R)` — Duplicate elimination (DISTINCT) + * + * **Binary operators** (infix): + * - `R × S` or `R cross S` — Cross product + * - `R ⋈ S` or `R natjoin S` — Natural join + * - `R ⋈[cond] S` or `R join[cond] S` — Theta join + * - `R ⟕[cond] S` or `R leftjoin[cond] S` — Left outer join + * - `R ⟖[cond] S` or `R rightjoin[cond] S` — Right outer join + * - `R ⟗[cond] S` or `R fulljoin[cond] S` — Full outer join + * - `R ⋉ S` or `R leftsemijoin S` — Left semi-join + * - `R ⋊ S` or `R rightsemijoin S` — Right semi-join + * - `R ▷ S` or `R antijoin S` — Anti-join + * - `R ∪ S` or `R union S` — Union + * - `R ∩ S` or `R intersect S` — Intersection + * - `R − S` or `R minus S` — Set difference + * - `R ÷ S` or `R divide S` — Division + * + * **Conditions** support: `=`, `<>`, `!=`, `<`, `>`, `<=`, `>=`, `AND`, `OR`, `NOT` + * + * @param input The relational algebra expression + * @param db Optional database handle for validating natural joins (checks for common columns) + * @returns The equivalent SQL query string + * @throws RAError if the expression cannot be parsed or a natural join has no common columns + */ +export function raToSQL(input: string, db?: DatabaseHandle): string { + subqueryCounter = 0; + const tokens = tokenize(input); + const parser = new Parser(tokens); + const program = parser.parse(); + return programToSQL(program, db); +} + +// Re-export for potential future use (e.g., AST visualization) +export type { RANode, ConditionNode, ColumnRef }; From ee46e8bd4f65de79a887440820bfaee2b385bc84 Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:34:55 +0000 Subject: [PATCH 02/13] Add RA editor UI: highlighter, reference dialog, mode switching --- src/App.tsx | 142 +++++----- src/ra-engine/RAReference.tsx | 172 ++++++++++++ src/ra-engine/raHighlight.test.ts | 82 ++++++ src/ra-engine/raHighlight.ts | 344 ++++++++++++++++++++++++ src/ra-engine/relationalAlgebra.test.ts | 92 +++++-- src/ra-engine/relationalAlgebra.ts | 81 +++--- 6 files changed, 798 insertions(+), 115 deletions(-) create mode 100644 src/ra-engine/RAReference.tsx create mode 100644 src/ra-engine/raHighlight.test.ts create mode 100644 src/ra-engine/raHighlight.ts diff --git a/src/App.tsx b/src/App.tsx index 8a1b21d..6829e04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,8 @@ import { format } from "sql-formatter"; import initSqlJs from "sql.js"; import ViewsTable, { View } from "./ViewsTable"; import { raToSQL, RAError } from "./ra-engine/relationalAlgebra"; +import RAReference from "./ra-engine/RAReference"; +import { highlightRA } from "./ra-engine/raHighlight"; import { Info, Settings, XCircle, CheckCircle2, Eye, EyeOff } from "lucide-react"; import ChangelogDialog from "./ChangelogDialog"; @@ -56,6 +58,11 @@ import { useLanguage, langKey, getUrlParam, setUrlParam } from "./i18n/context"; import LanguageSelector from "./LanguageSelector"; import { getQuestion } from "./QuestionSelector"; +/** Storage key for a question's query, namespaced by editor mode */ +function queryKey(questionId: number, mode: "sql" | "ra"): string { + return mode === "ra" ? `ra-questionId-${questionId}` : `questionId-${questionId}`; +} + function App() { const { lang, t, questions, dbArrayBuffer, defaultQuery } = useLanguage(); const [question, setQuestion] = useState(); @@ -80,14 +87,7 @@ function App() { const [exportingStatus, setExportingStatus] = useState(0); const [loadedQuestionCorrect, setLoadedQuestionCorrect] = useState(false); const [editorMode, setEditorMode] = useState<"sql" | "ra">("sql"); - const [generatedSQL, setGeneratedSQL] = useState(null); - const switchEditorMode = useCallback((mode: "sql" | "ra") => { - setEditorMode(mode); - setGeneratedSQL(null); - setError(null); - resetResult(); - }, [resetResult]); const exportRendererRef = useRef(null); const editorRef = useRef(null); @@ -154,6 +154,27 @@ function App() { setQueryedView(null); }, []); + const switchEditorMode = useCallback((mode: "sql" | "ra") => { + // Save current query before switching + if (question && query !== undefined) { + const currentKey = queryKey(question.id, editorMode); + if (query === defaultQuery || query === "") { + localStorage.removeItem(langKey(lang, currentKey)); + } else { + localStorage.setItem(langKey(lang, currentKey), query); + } + } + setEditorMode(mode); + + setError(null); + resetResult(); + // Load query for the new mode + if (question) { + const newKey = queryKey(question.id, mode); + setQuery(localStorage.getItem(langKey(lang, newKey)) || (mode === "ra" ? "" : defaultQuery)); + } + }, [resetResult, question, query, editorMode, lang, defaultQuery]); + const initDb = useCallback(async () => { if (!dbArrayBuffer) return; resetResult(); @@ -200,7 +221,7 @@ function App() { const resolved = getQuestion(q.id, questions); if (resolved) { setQuestion(resolved); - setQuery(localStorage.getItem(langKey(lang, "questionId-" + resolved.id)) || defaultQuery); + setQuery(localStorage.getItem(langKey(lang, queryKey(resolved.id, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); } } } @@ -232,15 +253,18 @@ function App() { } let wq = JSON.parse(localStorage.getItem(langKey(lang, "writtenQuestions")) || "[]"); const initialLength = wq.length; + const storageKey = queryKey(question.id, editorMode); if (query === defaultQuery || query === "") { - localStorage.removeItem(langKey(lang, "questionId-" + question.id)); - // remove from writtenQuestions if it exists there as well - const filtered = wq.filter((id: number) => id !== question.id); - wq = filtered; + localStorage.removeItem(langKey(lang, storageKey)); + // remove from writtenQuestions if it exists there as well (only for SQL mode) + if (editorMode === "sql") { + const filtered = wq.filter((id: number) => id !== question.id); + wq = filtered; + } } else { - localStorage.setItem(langKey(lang, "questionId-" + question.id), query); - // ensure that questionid is in localstorage writtenQuestions - if (!wq.includes(question.id)) { + localStorage.setItem(langKey(lang, storageKey), query); + // ensure that questionid is in localstorage writtenQuestions (only for SQL mode) + if (editorMode === "sql" && !wq.includes(question.id)) { wq.push(question.id); } } @@ -336,7 +360,6 @@ function App() { if (editorMode === "ra") { try { sqlToRun = raToSQL(query, database); - setGeneratedSQL(sqlToRun); } catch (e) { if (e instanceof RAError) { setError(t("raParseError", { message: e.message })); @@ -346,8 +369,6 @@ function App() { } return; } - } else { - setGeneratedSQL(null); } const res = database.exec(sqlToRun); setIsViewResult(false); @@ -448,7 +469,8 @@ function App() { setIsCorrect(true); setMatchedResult(matchedAlt ?? question.evaluable_result); - localStorage.setItem(langKey(lang, `correctQuestionId-${question.id}`), query); + const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; + localStorage.setItem(langKey(lang, correctKey), query); setCorrectQueryMismatch(false); setLoadedQuestionCorrect(true); @@ -462,20 +484,22 @@ function App() { // Save query based on question const loadQuery = useCallback((_oldQuestion: Question | undefined, newQuestion: Question) => { - setQuery(localStorage.getItem(langKey(lang, "questionId-" + newQuestion.id)) || defaultQuery); + const key = queryKey(newQuestion.id, editorMode); + setQuery(localStorage.getItem(langKey(lang, key)) || (editorMode === "ra" ? "" : defaultQuery)); // This prevents user from ctrl-z'ing to a different question if (editorRef.current) { editorRef.current!.session = {history: { stack: [], offset: 0 }}; } - }, [setQuery, lang, defaultQuery]); + }, [setQuery, lang, defaultQuery, editorMode]); // Update mismatch & loadedQuestionCorrect flags when query is changed useEffect(() => { - if (!database || !question || query === undefined || editorMode === "ra") { + if (!database || !question || query === undefined) { return; } - const correctQuery = localStorage.getItem(langKey(lang, `correctQuestionId-${question.id}`)); + const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; + const correctQuery = localStorage.getItem(langKey(lang, correctKey)); if (!correctQuery) { setCorrectQueryMismatch(false); setLoadedQuestionCorrect(false); @@ -484,9 +508,25 @@ function App() { setLoadedQuestionCorrect(true); - let currentQuery = ""; - try { - currentQuery = format(query + (query.endsWith(";") ? "" : ";"), { + if (editorMode === "ra") { + // For RA, compare raw text (trimmed) + setCorrectQueryMismatch(query.trim() !== correctQuery.trim()); + } else { + let currentQuery = ""; + try { + currentQuery = format(query + (query.endsWith(";") ? "" : ";"), { + language: "sqlite", + tabWidth: 2, + useTabs: false, + keywordCase: "upper", + dataTypeCase: "upper", + functionCase: "upper", + }); + } catch { + setCorrectQueryMismatch(true); + return; + } + const correctQueryFormatted = format(correctQuery + (correctQuery?.endsWith(";") ? "" : ";"), { language: "sqlite", tabWidth: 2, useTabs: false, @@ -494,20 +534,9 @@ function App() { dataTypeCase: "upper", functionCase: "upper", }); - } catch { - setCorrectQueryMismatch(true); - return; + setCorrectQueryMismatch(currentQuery !== correctQueryFormatted); } - const correctQueryFormatted = format(correctQuery + (correctQuery?.endsWith(";") ? "" : ";"), { - language: "sqlite", - tabWidth: 2, - useTabs: false, - keywordCase: "upper", - dataTypeCase: "upper", - functionCase: "upper", - }); - setCorrectQueryMismatch(currentQuery !== correctQueryFormatted); - }, [database, question, query, lang]); + }, [database, question, query, lang, editorMode]); const exportData = useCallback((options?: { include?: number[]}) => { if (!database) { @@ -861,16 +890,17 @@ function App() {
{t("query")} -
+
+ / @@ -902,10 +932,10 @@ function App() { null} - highlight={code => highlight(code, languages.sql)} + highlight={code => editorMode === "ra" ? highlightRA(code) : highlight(code, languages.sql)} padding={10} tabSize={2} className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 min-h-32 rounded-md" @@ -917,7 +947,7 @@ function App() { itemID="editor" value={query} onValueChange={code => setQuery(code)} - highlight={code => highlight(code, languages.sql)} + highlight={code => editorMode === "ra" ? highlightRA(code) : highlight(code, languages.sql)} padding={10} tabSize={2} className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 border dark:border-slate-600 border-gray-300 min-h-32 rounded-md" @@ -942,7 +972,10 @@ function App() { {t("queryMismatch")}
)} - {t("sqlReference")} + {editorMode === "ra" + ? + : {t("sqlReference")} + }
{/* Right side - Buttons */} @@ -952,7 +985,8 @@ function App() { variant="outline" onClick={() => { if (!question) return; - setQuery(localStorage.getItem(langKey(lang, `correctQuestionId-${question.id}`)) || defaultQuery); + const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; + setQuery(localStorage.getItem(langKey(lang, correctKey)) || (editorMode === "ra" ? "" : defaultQuery)); }} className="border-yellow-500 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-50 dark:hover:bg-yellow-900/20" > @@ -988,20 +1022,6 @@ function App() {
- {editorMode === "ra" && generatedSQL && ( -
- {t("generatedSQL")} - { try { return format(generatedSQL, { language: "sqlite", tabWidth: 2, useTabs: false, keywordCase: "upper", dataTypeCase: "upper", functionCase: "upper" }); } catch { return generatedSQL; } })()} - onValueChange={() => null} - highlight={code => highlight(code, languages.sql)} - padding={10} - tabSize={2} - className="font-mono text-sm w-full dark:bg-slate-800/50 bg-slate-50 border dark:border-slate-700 border-gray-200 rounded-md mt-1" - /> -
- )} exportData({include})} ref={exportModalRef} /> + +Licensed under the Business Source License 1.1 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License in the LICENSE.md file in this repository. +*/ + +import { useState } from "react"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { X } from "lucide-react"; +import { useLanguage } from "../i18n/context"; + +const RAReference = () => { + const { t } = useLanguage(); + const [open, setOpen] = useState(false); + + return ( + <> + + + +
+

{t("raReference")}

+ +
+
+ {/* Unary Operators */} +
+

{t("raUnaryOps")}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("raColOp")}{t("raColSyntax")}{t("raColDesc")}
σ / sigmaσ[age > 20](R){t("raDescSelection")}
π / piπ[name, city](R){t("raDescProjection")}
ρ / rhoρ[old→new](R){t("raDescRename")}
γ / gammaγ[city; COUNT(id)](R){t("raDescGroup")}
τ / tauτ[name](R){t("raDescSort")}
δ / deltaδ(R){t("raDescDistinct")}
+
+ + {/* Binary Operators */} +
+

{t("raBinaryOps")}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("raColOp")}{t("raColSyntax")}{t("raColDesc")}
× / crossR × S{t("raDescCross")}
⋈ / natjoinR ⋈ S{t("raDescNatJoin")}
⋈ / joinR ⋈[R.id = S.id] S{t("raDescThetaJoin")}
∪ / unionR ∪ S{t("raDescUnion")}
∩ / intersectR ∩ S{t("raDescIntersect")}
− / minusR − S{t("raDescMinus")}
÷ / divideR ÷ S{t("raDescDivide")}
+
+ + {/* Notation */} +
+

{t("raNotation")}

+
+

σ[cond](R) {t("raNoteBrackets")}

+

σ{"{cond}"}(R) {t("raNoteCurly")}

+

σ{"_{cond}"}(R) {t("raNoteLaTeX")}

+

σ cond (R) {t("raNoteImplicit")}

+

π[cols] σ[cond] R {t("raNoteChain")}

+
+
+ + {/* Assignment */} +
+

{t("raAssignment")}

+
+

A <- σ[age > 20](Person)

+

B <- π[name](A)

+

{t("raNoteAssignment")}

+
+
+ + {/* Conditions */} +
+

{t("raConditions")}

+
+

age > 20 AND city = 'York'

+

age > 20 OR age < 10

+

NOT active = 1

+

{t("raNoteComparison")}

+
+
+
+
+
+ + ); +}; + +export default RAReference; diff --git a/src/ra-engine/raHighlight.test.ts b/src/ra-engine/raHighlight.test.ts new file mode 100644 index 0000000..5585296 --- /dev/null +++ b/src/ra-engine/raHighlight.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from "vitest"; +import { highlightRA } from "./raHighlight"; + +describe("RA highlighter", () => { + it("should highlight σ operator in purple", () => { + const html = highlightRA("σ[age > 20](Person)"); + expect(html).toContain("color: #7c3aed"); // operator color + expect(html).toContain("σ"); + }); + + it("should render brackets faintly", () => { + const html = highlightRA("σ[age > 20](Person)"); + expect(html).toContain("opacity: 0.5"); // faint brackets + }); + + it("should render subscript content in light purple", () => { + const html = highlightRA("π[name](Person)"); + expect(html).toContain("color: #c084fc"); // subscript styling + }); + + it("should highlight string literals in green", () => { + const html = highlightRA("σ[name = 'Alice'](Person)"); + expect(html).toContain("color: #059669"); // string color + expect(html).toContain("Alice"); + }); + + it("should highlight numbers in amber", () => { + const html = highlightRA("σ[age > 20](Person)"); + expect(html).toContain("color: #d97706"); // number color + }); + + it("should highlight AND/OR in blue", () => { + const html = highlightRA("σ[a > 1 and b < 2](T)"); + expect(html).toContain("color: #2563eb"); // logic color + expect(html).toContain("and"); + }); + + it("should highlight comments in gray italic", () => { + const html = highlightRA("-- this is a comment\nPerson"); + expect(html).toContain("font-style: italic"); + expect(html).toContain("this is a comment"); + }); + + it("should render <- with assignment styling", () => { + const html = highlightRA("A <- Person"); + expect(html).toContain("<-"); + expect(html).toContain("font-weight: bold"); + }); + + it("should render -> with operator styling", () => { + const html = highlightRA("ρ[name->fullName](Person)"); + expect(html).toContain("->"); + }); + + it("should handle LaTeX-style _{} notation", () => { + const html = highlightRA("σ_{age > 20}(Person)"); + expect(html).toContain("opacity: 0.5"); // faint brackets + expect(html).toContain("color: #c084fc"); // subscript content + }); + + it("should highlight keyword operators", () => { + const html = highlightRA("sigma[age > 20](Person)"); + expect(html).toContain("color: #7c3aed"); // operator color + expect(html).toContain("sigma"); + }); + + it("should highlight binary keyword operators", () => { + const html = highlightRA("A cross B"); + expect(html).toContain("color: #7c3aed"); + expect(html).toContain("cross"); + }); + + it("should preserve newlines", () => { + const html = highlightRA("A <- Person\nA"); + expect(html).toContain("\n"); + }); + + it("should handle implicit subscripts with whitespace", () => { + const html = highlightRA("σ age > 20 (Person)"); + expect(html).toContain("color: #c084fc"); // subscript styling for implicit content + }); +}); diff --git a/src/ra-engine/raHighlight.ts b/src/ra-engine/raHighlight.ts new file mode 100644 index 0000000..32343d6 --- /dev/null +++ b/src/ra-engine/raHighlight.ts @@ -0,0 +1,344 @@ +/* +Relational Algebra engine for sql-validator +Copyright (C) 2026 E.SU. IT AB (Org.no 559484-0505) and Edwin Sundberg + +Licensed under the Business Source License 1.1 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License in the LICENSE.md file in this repository. +*/ + +/** + * Custom syntax highlighter for relational algebra expressions. + * Returns HTML with subscript rendering for bracket content and + * colored tokens for operators, keywords, strings, etc. + */ + +// Escape HTML special characters +function esc(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +// RA operator symbols +const UNARY_SYMBOLS = new Set(["σ", "π", "ρ", "γ", "τ", "δ"]); +const BINARY_SYMBOLS = new Set(["×", "⋈", "∪", "∩", "−", "÷", "⟕", "⟖", "⟗", "⋉", "⋊", "▷", "←"]); + +const UNARY_KEYWORDS = new Set([ + "sigma", "select", "pi", "project", "rho", "rename", + "gamma", "tau", "sort", "delta", "distinct", +]); +const BINARY_KEYWORDS = new Set([ + "cross", "natjoin", "join", "union", "intersect", "minus", + "divide", "leftjoin", "rightjoin", "fulljoin", + "leftsemijoin", "rightsemijoin", "antijoin", +]); +const LOGIC_KEYWORDS = new Set(["and", "or", "not"]); + +// CSS classes (inline styles for portability with the code editor) +const S = { + op: "color: #7c3aed; font-weight: bold;", // purple - operators + kw: "color: #7c3aed; font-weight: bold;", // purple - keyword operators + logic: "color: #2563eb; font-weight: bold;", // blue - AND/OR/NOT + str: "color: #059669;", // green - strings + num: "color: #d97706;", // amber - numbers + comment: "color: #9ca3af; font-style: italic;", // gray - comments + bracket: "color: #a1a1aa; opacity: 0.5;", // zinc, faint - brackets (subscript delimiters) + assign: "color: #7c3aed; font-weight: bold;", // purple - assignment arrow + sub: "color: #c084fc; font-weight: 500;", // light purple - subscript content (readable, distinct from plain text) +}; + +/** + * Highlight a relational algebra expression, rendering bracket content + * as subscripts and coloring operators/keywords. + */ +export function highlightRA(code: string): string { + const result: string[] = []; + let i = 0; + + while (i < code.length) { + // Comments: -- + if (code[i] === "-" && i + 1 < code.length && code[i + 1] === "-") { + let comment = ""; + while (i < code.length && code[i] !== "\n") { + comment += code[i]; + i++; + } + result.push(`${esc(comment)}`); + continue; + } + + // Newlines — preserve them + if (code[i] === "\n") { + result.push("\n"); + i++; + continue; + } + + // Unicode RA operators (single char) + if (UNARY_SYMBOLS.has(code[i])) { + result.push(`${esc(code[i])}`); + i++; + // Check for subscript: _{ }, [ ], { }, or implicit (tokens until '(') + i = renderSubscript(code, i, result); + continue; + } + + // |X| or |><| — natural join + if (code[i] === "|") { + if (i + 2 < code.length && (code[i + 1] === "X" || code[i + 1] === "x") && code[i + 2] === "|") { + result.push(`${esc("|X|")}`); + i += 3; + continue; + } + if (i + 3 < code.length && code[i + 1] === ">" && code[i + 2] === "<" && code[i + 3] === "|") { + result.push(`${esc("|><|")}`); + i += 4; + continue; + } + } + + if (BINARY_SYMBOLS.has(code[i])) { + result.push(`${esc(code[i])}`); + i++; + // Binary ops can have bracket conditions: ⋈[cond] or ⋈{cond} + if (i < code.length && (code[i] === "[" || code[i] === "{" || code[i] === "_")) { + i = renderSubscript(code, i, result); + } + continue; + } + + // Assignment arrow: ← (already handled above as binary symbol) + // Arrow: <- (assignment) — keep both chars for editor alignment, tight letter-spacing + if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { + result.push(`<-`); + i += 2; + continue; + } + + // Arrow: -> (rename) — keep both chars for editor alignment + if (code[i] === "-" && i + 1 < code.length && code[i + 1] === ">") { + result.push(`->`); + i += 2; + continue; + } + + // String literals + if (code[i] === "'") { + let str = "'"; + i++; + while (i < code.length && code[i] !== "'") { + str += code[i]; + i++; + } + if (i < code.length) { str += "'"; i++; } + result.push(`${esc(str)}`); + continue; + } + + // Numbers + if (/\d/.test(code[i])) { + let num = ""; + while (i < code.length && /[\d.]/.test(code[i])) { + num += code[i]; + i++; + } + result.push(`${esc(num)}`); + continue; + } + + // Identifiers and keywords + if (/[a-zA-Z_\u00C0-\u024F]/.test(code[i])) { + let ident = ""; + while (i < code.length && /[a-zA-Z0-9_\u00C0-\u024F]/.test(code[i])) { + ident += code[i]; + i++; + } + const lower = ident.toLowerCase(); + if (UNARY_KEYWORDS.has(lower)) { + result.push(`${esc(ident)}`); + // Check for subscript after keyword + i = renderSubscript(code, i, result); + } else if (BINARY_KEYWORDS.has(lower)) { + result.push(`${esc(ident)}`); + // Binary keyword may have bracket conditions + const j = skipWhitespace(code, i); + if (j < code.length && (code[j] === "[" || code[j] === "{" || code[j] === "_")) { + // Include the whitespace before the bracket + if (j > i) result.push(esc(code.slice(i, j))); + i = renderSubscript(code, j, result); + } + } else if (LOGIC_KEYWORDS.has(lower)) { + result.push(`${esc(ident)}`); + } else { + result.push(esc(ident)); + } + continue; + } + + // Comparison operators + if (code[i] === "<" || code[i] === ">" || code[i] === "!" || code[i] === "=") { + let op = code[i]; + i++; + if (i < code.length && (code[i] === "=" || code[i] === ">")) { + op += code[i]; + i++; + } + result.push(`${esc(op)}`); + continue; + } + + // Everything else (whitespace, parens, etc.) — pass through + result.push(esc(code[i])); + i++; + } + + return result.join(""); +} + +function skipWhitespace(code: string, i: number): number { + while (i < code.length && code[i] === " ") i++; + return i; +} + +/** + * Render a subscript section after a unary/binary operator. + * Handles: _{ }, [ ], { }, or implicit (content until '('). + * Returns the new index position. + */ +function renderSubscript(code: string, i: number, result: string[]): number { + // Skip whitespace + const beforeWs = i; + while (i < code.length && code[i] === " ") i++; + + // Check what follows + if (i >= code.length) { + if (i > beforeWs) result.push(code.slice(beforeWs, i)); + return i; + } + + // Handle _{ or _[ (LaTeX-style) + if (code[i] === "_") { + result.push(`${esc("_")}`); + i++; + while (i < code.length && code[i] === " ") i++; + } + + if (code[i] === "[" || code[i] === "{") { + // Bracketed subscript — render brackets faintly, content as subscript + const openBracket = code[i]; + result.push(`${esc(openBracket)}`); + i++; // past opening bracket + let depth = 1; + let content = ""; + while (i < code.length && depth > 0) { + if (code[i] === "[" || code[i] === "{") depth++; + if (code[i] === "]" || code[i] === "}") depth--; + if (depth > 0) { + content += code[i]; + } + i++; + } + // Render inner content with subscript styling + result.push(`${highlightSubscriptContent(content)}`); + // Render closing bracket faintly (the character at i-1 was the closing bracket) + if (depth === 0) { + const closeBracket = code[i - 1]; + result.push(`${esc(closeBracket)}`); + } + return i; + } + + // Not a bracket — check for implicit subscript (content until '(') + // Only apply for unary operators where we expect a subscript + if (code[i] !== "(" && i > beforeWs) { + // Emit the whitespace between operator and subscript content + result.push(code.slice(beforeWs, i)); + // Collect content until '(' as implicit subscript + let content = ""; + while (i < code.length && code[i] !== "(" && code[i] !== "\n") { + content += code[i]; + i++; + } + // Trim trailing whitespace from content but keep the position + const trimmed = content.trimEnd(); + const trimDiff = content.length - trimmed.length; + if (trimDiff > 0) i -= trimDiff; + if (trimmed.length > 0) { + result.push(`${highlightSubscriptContent(trimmed)}`); + } + return i; + } + + // No subscript found — restore whitespace + if (i > beforeWs) result.push(code.slice(beforeWs, i)); + return i; +} + +/** + * Highlight content inside a subscript (conditions, column lists, etc.) + */ +function highlightSubscriptContent(content: string): string { + const parts: string[] = []; + let i = 0; + + while (i < content.length) { + // String literals + if (content[i] === "'") { + let str = "'"; + i++; + while (i < content.length && content[i] !== "'") { str += content[i]; i++; } + if (i < content.length) { str += "'"; i++; } + parts.push(`${esc(str)}`); + continue; + } + // Numbers + if (/\d/.test(content[i])) { + let num = ""; + while (i < content.length && /[\d.]/.test(content[i])) { num += content[i]; i++; } + parts.push(`${esc(num)}`); + continue; + } + // Keywords and identifiers + if (/[a-zA-Z_\u00C0-\u024F]/.test(content[i])) { + let ident = ""; + while (i < content.length && /[a-zA-Z0-9_\u00C0-\u024F]/.test(content[i])) { ident += content[i]; i++; } + const lower = ident.toLowerCase(); + if (LOGIC_KEYWORDS.has(lower)) { + parts.push(`${esc(ident)}`); + } else if (lower === "desc" || lower === "asc" || lower === "as") { + parts.push(`${esc(ident)}`); + } else if (["count", "sum", "avg", "min", "max"].includes(lower)) { + parts.push(`${esc(ident)}`); + } else { + parts.push(esc(ident)); + } + continue; + } + // Arrow → or -> + if (content[i] === "→") { + parts.push(``); + i++; + continue; + } + if (content[i] === "-" && i + 1 < content.length && content[i + 1] === ">") { + parts.push(`->`); + i += 2; + continue; + } + // Comparison ops + if (content[i] === "<" || content[i] === ">" || content[i] === "!" || content[i] === "=") { + let op = content[i]; i++; + if (i < content.length && (content[i] === "=" || content[i] === ">")) { op += content[i]; i++; } + parts.push(`${esc(op)}`); + continue; + } + // Everything else + parts.push(esc(content[i])); + i++; + } + + return parts.join(""); +} diff --git a/src/ra-engine/relationalAlgebra.test.ts b/src/ra-engine/relationalAlgebra.test.ts index 46f42c7..16d5ffb 100644 --- a/src/ra-engine/relationalAlgebra.test.ts +++ b/src/ra-engine/relationalAlgebra.test.ts @@ -174,22 +174,9 @@ describe("natural join (⋈)", () => { expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should error on impossible natural join when database is provided", () => { - // Mock database that returns different columns for two tables - const mockDb = { - exec: (sql: string) => { - if (sql.includes("TableA")) { - return [{ columns: ["id", "name"], values: [] }]; - } - if (sql.includes("TableB")) { - return [{ columns: ["code", "description"], values: [] }]; - } - return [{ columns: [], values: [] }]; - }, - }; - - expect(() => raToSQL("TableA ⋈ TableB", mockDb)).toThrow(RAError); - expect(() => raToSQL("TableA ⋈ TableB", mockDb)).toThrow(/no common columns/i); + it("should produce NATURAL JOIN SQL even without common columns (let SQLite handle it)", () => { + const sql = raToSQL("TableA ⋈ TableB"); + expect(norm(sql)).toContain("NATURAL JOIN"); }); it("should not error on valid natural join when database is provided", () => { @@ -211,6 +198,16 @@ describe("natural join (⋈)", () => { it("should not error when no database is provided (parse-only mode)", () => { expect(() => raToSQL("TableA ⋈ TableB")).not.toThrow(); }); + + it("should support |X| as natural join", () => { + const sql = raToSQL("Person |X| Student"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); + + it("should support |><| as natural join", () => { + const sql = raToSQL("Person |><| Student"); + expect(norm(sql)).toContain("NATURAL JOIN"); + }); }); // ─── Theta join (⋈[cond]) ────────────────────────────────────────────────── @@ -691,3 +688,66 @@ describe("implicit subscripts (no brackets)", () => { expect(norm(sql)).toContain("SELECT name FROM"); }); }); + +describe("parenthesis-free syntax", () => { + it("should allow σ[cond] Table without parens", () => { + expect(norm(raToSQL("σ[age > 20] Person"))).toBe( + norm("SELECT * FROM (Person) WHERE age > 20") + ); + }); + + it("should allow π[cols] Table without parens", () => { + expect(norm(raToSQL("π[name] Person"))).toBe( + norm("SELECT name FROM (Person)") + ); + }); + + it("should allow chained unary ops without parens", () => { + const sql = raToSQL("π[name] σ[age > 20] Person"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE age > 20"); + }); + + it("should handle the user's example without excessive parens", () => { + const sql = raToSQL("π[person_id, name, city] σ[city = 'York' OR city = 'Bristol'] Person"); + expect(norm(sql)).toContain("SELECT person_id, name, city FROM"); + expect(norm(sql)).toContain("WHERE"); + expect(norm(sql)).toContain("city = 'York'"); + expect(norm(sql)).toContain("city = 'Bristol'"); + }); + + it("should still work with parens for grouping binary ops", () => { + const sql = raToSQL("π[name] (Person ∪ Student)"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("UNION"); + }); + + it("should allow δ Table without parens", () => { + expect(norm(raToSQL("δ Person"))).toContain("SELECT DISTINCT"); + }); + + it("should allow triple chain without parens", () => { + const sql = raToSQL("π[name] σ[age > 20] ρ[n→name] Person"); + expect(norm(sql)).toContain("SELECT name FROM"); + expect(norm(sql)).toContain("WHERE age > 20"); + expect(norm(sql)).toContain("n AS name"); + }); +}); + +describe("implicit return from last assignment", () => { + it("should return last assigned variable when no explicit result", () => { + const sql = raToSQL("A <- σ[age > 20](Person)"); + expect(norm(sql)).toContain("WITH A AS"); + expect(norm(sql)).toContain("SELECT * FROM A"); + }); + + it("should return last variable with multiple assignments", () => { + const sql = raToSQL("A <- σ[age > 20](Person)\nB <- π[name](A)"); + expect(norm(sql)).toContain("SELECT * FROM B"); + }); + + it("should still work when explicit result is given", () => { + const sql = raToSQL("A <- σ[age > 20](Person)\nA"); + expect(norm(sql)).toContain("SELECT * FROM A"); + }); +}); diff --git a/src/ra-engine/relationalAlgebra.ts b/src/ra-engine/relationalAlgebra.ts index 865070d..c81b609 100644 --- a/src/ra-engine/relationalAlgebra.ts +++ b/src/ra-engine/relationalAlgebra.ts @@ -177,6 +177,22 @@ function tokenize(input: string): Token[] { if (ch === ";") { tokens.push({ type: TokenType.SEMICOLON, value: ch, pos }); i++; continue; } if (ch === "\\") { tokens.push({ type: TokenType.MINUS, value: ch, pos }); i++; continue; } + // |X| or |><| — natural join + if (ch === "|") { + // Check for |X| + if (i + 2 < input.length && (input[i + 1] === "X" || input[i + 1] === "x") && input[i + 2] === "|") { + tokens.push({ type: TokenType.JOIN, value: "|X|", pos }); + i += 3; + continue; + } + // Check for |><| + if (i + 3 < input.length && input[i + 1] === ">" && input[i + 2] === "<" && input[i + 3] === "|") { + tokens.push({ type: TokenType.JOIN, value: "|><|", pos }); + i += 4; + continue; + } + } + // Arrow: → or -> if (ch === "→") { tokens.push({ type: TokenType.ARROW, value: ch, pos }); i++; continue; } if (ch === "-" && i + 1 < input.length && input[i + 1] === ">") { @@ -431,6 +447,12 @@ class Parser { return { assignments, result }; } + // If we only have assignments and no final expression, implicitly return the last assigned variable + if (assignments.length > 0) { + const lastName = assignments[assignments.length - 1].name; + return { assignments, result: { type: "table", name: lastName } }; + } + throw new RAError("Empty expression"); } @@ -595,6 +617,21 @@ class Parser { return this.parseSortColumns(); } + /** + * Parse the operand of a unary operator. + * If `(` follows, parse `(expr)`. Otherwise parse another unary or a table name. + * This allows `π[cols] σ[cond] Person` without mandatory parentheses. + */ + private parseUnaryOperand(): RANode { + if (this.peek().type === TokenType.LPAREN) { + this.advance(); + const expr = this.parseUnionExpr(); + this.expect(TokenType.RPAREN); + return expr; + } + return this.parseUnaryExpr(); + } + private parseUnaryExpr(): RANode { const t = this.peek().type; @@ -602,12 +639,9 @@ class Parser { this.advance(); if (this.hasSubscript()) { const condition = this.parseSubscriptCondition(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "selection", condition, relation }; } - // σ(R) with no subscript — error throw new RAError("Selection (σ) requires a condition — use σ[condition](R) or σ condition (R)"); } @@ -615,9 +649,7 @@ class Parser { this.advance(); if (this.hasSubscript()) { const columns = this.parseSubscriptColumns(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "projection", columns, relation }; } throw new RAError("Projection (π) requires column list — use π[cols](R) or π cols (R)"); @@ -627,9 +659,7 @@ class Parser { this.advance(); if (this.hasSubscript()) { const mappings = this.parseSubscriptRenameMappings(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "rename", mappings, relation }; } throw new RAError("Rename (ρ) requires mappings — use ρ[old→new](R) or ρ old→new (R)"); @@ -639,9 +669,7 @@ class Parser { this.advance(); if (this.hasSubscript()) { const { groupBy, aggregates } = this.parseSubscriptGroupSpec(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "group", groupBy, aggregates, relation }; } throw new RAError("Grouping (γ) requires specification — use γ[groupCols; AGG(col)](R)"); @@ -651,9 +679,7 @@ class Parser { this.advance(); if (this.hasSubscript()) { const columns = this.parseSubscriptSortColumns(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "sort", columns, relation }; } throw new RAError("Sort (τ) requires column list — use τ[col](R) or τ col (R)"); @@ -661,9 +687,7 @@ class Parser { if (t === TokenType.DELTA) { this.advance(); - this.expect(TokenType.LPAREN); - const relation = this.parseUnionExpr(); - this.expect(TokenType.RPAREN); + const relation = this.parseUnaryOperand(); return { type: "distinct", relation }; } @@ -989,12 +1013,6 @@ interface DatabaseHandle { * Resolve the column names produced by a SQL expression using the database. * Executes a LIMIT 0 query to get column metadata without fetching data. */ -function resolveColumns(sql: string, db: DatabaseHandle): string[] { - const res = db.exec(`SELECT * FROM (${sql}) LIMIT 0`); - if (res.length === 0) return []; - return res[0].columns; -} - function nodeToSQL(node: RANode, db?: DatabaseHandle): string { switch (node.type) { case "table": @@ -1042,19 +1060,6 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { case "naturalJoin": { const leftSQL = nodeToSQL(node.left, db); const rightSQL = nodeToSQL(node.right, db); - // Validate that there are common columns when a database is available - if (db) { - const leftCols = resolveColumns(leftSQL, db); - const rightCols = resolveColumns(rightSQL, db); - const common = leftCols.filter(c => rightCols.includes(c)); - if (common.length === 0) { - throw new RAError( - "Natural join has no common columns between the two relations. " + - "Left columns: [" + leftCols.join(", ") + "], Right columns: [" + rightCols.join(", ") + "]. " + - "Use a cross product (×) if a cartesian product is intended, or a theta join (⋈[condition]) to specify the join condition." - ); - } - } return `SELECT * FROM (${leftSQL}) AS _ra${subqueryCounter++} NATURAL JOIN (${rightSQL}) AS _ra${subqueryCounter++}`; } From 91c83a8d4d1d0ed32eb4add9d27329bad2afe19b Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:55:16 +0000 Subject: [PATCH 03/13] Add natural join and CTE validation with database reflection --- src/App.tsx | 4 +- src/ra-engine/raHighlight.ts | 4 +- src/ra-engine/relationalAlgebra.test.ts | 74 ++++++++++--- src/ra-engine/relationalAlgebra.ts | 132 ++++++++++++++++++++++-- 4 files changed, 185 insertions(+), 29 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6829e04..39a85a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -291,9 +291,9 @@ function App() { setError(e.message); } } else { - // In RA mode, try to parse to check for errors + // In RA mode, try to parse and validate against database try { - raToSQL(query); + raToSQL(query, database); setError(null); } catch (e) { if (e instanceof RAError) { diff --git a/src/ra-engine/raHighlight.ts b/src/ra-engine/raHighlight.ts index 32343d6..549e1a4 100644 --- a/src/ra-engine/raHighlight.ts +++ b/src/ra-engine/raHighlight.ts @@ -111,9 +111,9 @@ export function highlightRA(code: string): string { } // Assignment arrow: ← (already handled above as binary symbol) - // Arrow: <- (assignment) — keep both chars for editor alignment, tight letter-spacing + // Arrow: <- (assignment) — keep both chars for editor alignment if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { - result.push(`<-`); + result.push(`<-`); i += 2; continue; } diff --git a/src/ra-engine/relationalAlgebra.test.ts b/src/ra-engine/relationalAlgebra.test.ts index 16d5ffb..3b16610 100644 --- a/src/ra-engine/relationalAlgebra.test.ts +++ b/src/ra-engine/relationalAlgebra.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import initSqlJs from "sql.js"; import { raToSQL, RAError } from "./relationalAlgebra"; // ─── Helper ───────────────────────────────────────────────────────────────── @@ -174,25 +175,46 @@ describe("natural join (⋈)", () => { expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should produce NATURAL JOIN SQL even without common columns (let SQLite handle it)", () => { - const sql = raToSQL("TableA ⋈ TableB"); - expect(norm(sql)).toContain("NATURAL JOIN"); + it("should error on impossible natural join when database is provided", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE TableA (id INTEGER, name TEXT)"); + db.run("CREATE TABLE TableB (code TEXT, description TEXT)"); + + expect(() => raToSQL("TableA ⋈ TableB", db)).toThrow(RAError); + expect(() => raToSQL("TableA ⋈ TableB", db)).toThrow(/no common columns/i); + db.close(); + }); + + it("should not error on valid natural join when database is provided", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE Person (id INTEGER, name TEXT, city TEXT)"); + db.run("CREATE TABLE Student (id INTEGER, hasDisability INTEGER)"); + + expect(() => raToSQL("Person ⋈ Student", db)).not.toThrow(); + db.close(); + }); + + it("should error on natural join with non-existent table", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE Person (id INTEGER, name TEXT)"); + + expect(() => raToSQL("Person ⋈ Room", db)).toThrow(RAError); + expect(() => raToSQL("Person ⋈ Room", db)).toThrow(/Room/); + db.close(); }); - it("should not error on valid natural join when database is provided", () => { - const mockDb = { - exec: (sql: string) => { - if (sql.includes("Person")) { - return [{ columns: ["id", "name", "city"], values: [] }]; - } - if (sql.includes("Student")) { - return [{ columns: ["id", "hasDisability"], values: [] }]; - } - return [{ columns: [], values: [] }]; - }, - }; + it("should error on natural join with no common columns through subquery", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE Person (person_id INTEGER, name TEXT, city TEXT)"); + db.run("CREATE TABLE Room (room_id INTEGER, building TEXT, capacity INTEGER)"); - expect(() => raToSQL("Person ⋈ Student", mockDb)).not.toThrow(); + expect(() => raToSQL("σ[city = 'York'](Person) ⋈ Room", db)).toThrow(RAError); + expect(() => raToSQL("σ[city = 'York'](Person) ⋈ Room", db)).toThrow(/no common columns/i); + db.close(); }); it("should not error when no database is provided (parse-only mode)", () => { @@ -750,4 +772,24 @@ describe("implicit return from last assignment", () => { const sql = raToSQL("A <- σ[age > 20](Person)\nA"); expect(norm(sql)).toContain("SELECT * FROM A"); }); + + it("should handle self-referential A <- A as a no-op", () => { + const sql = raToSQL("A <- A"); + expect(norm(sql)).toBe(norm("SELECT * FROM A")); + }); + + it("should handle variable reassignment with versioned CTE names", () => { + const sql = raToSQL("A <- σ[age > 20](Person)\nA <- π[name](A)\nA"); + expect(norm(sql)).toContain("WITH A AS"); + expect(norm(sql)).toContain("A_v2 AS"); + expect(norm(sql)).toContain("SELECT * FROM A_v2"); + }); + + it("should handle triple reassignment", () => { + const sql = raToSQL("X <- Person\nX <- σ[age > 20](X)\nX <- π[name](X)"); + expect(norm(sql)).toContain("X AS"); + expect(norm(sql)).toContain("X_v2 AS"); + expect(norm(sql)).toContain("X_v3 AS"); + expect(norm(sql)).toContain("SELECT * FROM X_v3"); + }); }); diff --git a/src/ra-engine/relationalAlgebra.ts b/src/ra-engine/relationalAlgebra.ts index c81b609..5077118 100644 --- a/src/ra-engine/relationalAlgebra.ts +++ b/src/ra-engine/relationalAlgebra.ts @@ -1007,12 +1007,47 @@ let subqueryCounter = 0; */ interface DatabaseHandle { exec(sql: string): { columns: string[]; values: unknown[][] }[]; + prepare(sql: string): { step(): boolean; getColumnNames(): string[]; free(): void }; } /** * Resolve the column names produced by a SQL expression using the database. - * Executes a LIMIT 0 query to get column metadata without fetching data. + * Uses prepare() to get column metadata without executing the query. */ +function resolveColumns(sql: string, db: DatabaseHandle): string[] { + // For bare table names, use PRAGMA table_info which always works + if (/^\w+$/.test(sql.trim())) { + const tableName = sql.trim(); + try { + const res = db.exec(`PRAGMA table_info(${tableName})`); + if (res.length > 0 && res[0].values.length > 0) { + // PRAGMA table_info returns rows with [cid, name, type, notnull, dflt_value, pk] + return res[0].values.map((row: unknown[]) => String(row[1])); + } + } catch { + // Table doesn't exist + } + throw new RAError(`Table '${tableName}' does not exist`); + } + + // For complex expressions, use prepare to get column names + try { + const probeSQL = `SELECT * FROM (${sql}) LIMIT 0`; + const stmt = db.prepare(probeSQL); + // Step once to initialize column metadata (required by some sql.js versions) + stmt.step(); + const cols = stmt.getColumnNames(); + stmt.free(); + return cols; + } catch (e) { + const msg = (e as Error).message || String(e); + if (/no such table/i.test(msg)) { + const match = msg.match(/no such table:\s*(\S+)/i); + throw new RAError(match ? `Table '${match[1]}' does not exist` : msg); + } + throw new RAError(msg); + } +} function nodeToSQL(node: RANode, db?: DatabaseHandle): string { switch (node.type) { case "table": @@ -1060,6 +1095,20 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { case "naturalJoin": { const leftSQL = nodeToSQL(node.left, db); const rightSQL = nodeToSQL(node.right, db); + if (db) { + const leftCols = resolveColumns(leftSQL, db); + const rightCols = resolveColumns(rightSQL, db); + if (leftCols.length > 0 && rightCols.length > 0) { + const common = leftCols.filter(c => rightCols.includes(c)); + if (common.length === 0) { + throw new RAError( + "Natural join has no common columns between the two relations. " + + "Left columns: [" + leftCols.join(", ") + "], Right columns: [" + rightCols.join(", ") + "]. " + + "Use a cross product (×) if a cartesian product is intended, or a theta join (⋈[condition]) to specify the join condition." + ); + } + } + } return `SELECT * FROM (${leftSQL}) AS _ra${subqueryCounter++} NATURAL JOIN (${rightSQL}) AS _ra${subqueryCounter++}`; } @@ -1114,23 +1163,88 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { } } +/** + * Rewrite table references in an AST node using a name mapping. + * Used to handle variable reassignment (A <- ...; A <- ...) by pointing + * references to the correct versioned CTE name. + */ +function rewriteTableRefs(node: RANode, nameMap: Record): RANode { + switch (node.type) { + case "table": + return nameMap[node.name] ? { type: "table", name: nameMap[node.name] } : node; + case "selection": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "projection": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "rename": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "group": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "sort": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "distinct": + return { ...node, relation: rewriteTableRefs(node.relation, nameMap) }; + case "crossProduct": + case "naturalJoin": + case "union": + case "intersect": + case "difference": + case "division": + case "leftSemiJoin": + case "rightSemiJoin": + case "antiJoin": + return { ...node, left: rewriteTableRefs(node.left, nameMap), right: rewriteTableRefs(node.right, nameMap) }; + case "thetaJoin": + case "leftJoin": + case "rightJoin": + case "fullJoin": + return { ...node, left: rewriteTableRefs(node.left, nameMap), right: rewriteTableRefs(node.right, nameMap) }; + default: + return node; + } +} + function programToSQL(program: RAProgram, db?: DatabaseHandle): string { if (program.assignments.length === 0) { return nodeToSQL(program.result, db); } - // Use CTEs (WITH clauses) for assignments - const ctes = program.assignments.map(a => { - const sql = nodeToSQL(a.expr, db); - // Wrap non-table expressions in SELECT * FROM (...) for CTE compatibility + // Use CTEs (WITH clauses) for assignments. + // Handle reassignment (A <- ..., A <- ...) by versioning CTE names + // and rewriting references in subsequent expressions. + const ctes: string[] = []; + // Maps variable name -> current CTE name (may be versioned like A_v2) + const nameMap: Record = {}; + // Track how many times each name has been assigned + const assignCount: Record = {}; + + for (const a of program.assignments) { + if (a.expr.type === "table" && a.expr.name === a.name && !nameMap[a.name]) { + // Self-referential assignment (A <- A) where A is a real table — skip, it's a no-op + continue; + } + + // Rewrite the expression: replace table references with their current CTE aliases + const rewrittenExpr = rewriteTableRefs(a.expr, nameMap); + const sql = nodeToSQL(rewrittenExpr, db); const wrappedSQL = /^\w+$/.test(sql) ? `SELECT * FROM ${sql}` : sql; - return `${a.name} AS (${wrappedSQL})`; - }); - const resultSQL = nodeToSQL(program.result, db); - // Wrap bare table reference in SELECT for the final expression + // Determine the CTE name for this assignment + assignCount[a.name] = (assignCount[a.name] || 0) + 1; + const cteName = assignCount[a.name] > 1 ? `${a.name}_v${assignCount[a.name]}` : a.name; + nameMap[a.name] = cteName; + + ctes.push(`${cteName} AS (${wrappedSQL})`); + } + + // Rewrite the result expression with final name mappings + const rewrittenResult = rewriteTableRefs(program.result, nameMap); + const resultSQL = nodeToSQL(rewrittenResult, db); const wrappedResult = /^\w+$/.test(resultSQL) ? `SELECT * FROM ${resultSQL}` : resultSQL; + if (ctes.length === 0) { + return wrappedResult; + } return `WITH ${ctes.join(", ")} ${wrappedResult}`; } From 33dec9d1d1f99f105504e3163184c0ce0895fbde Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:21:07 +0000 Subject: [PATCH 04/13] Add mode-aware progress tracking, RA preview box, and RA PNG export --- src/App.css | 15 +++ src/App.tsx | 206 ++++++++++++++++++----------------- src/ExportRenderer.tsx | 43 +++++--- src/ra-engine/RAPreview.tsx | 204 ++++++++++++++++++++++++++++++++++ src/ra-engine/raHighlight.ts | 195 +++++++-------------------------- src/ra-engine/raShared.ts | 197 +++++++++++++++++++++++++++++++++ 6 files changed, 588 insertions(+), 272 deletions(-) create mode 100644 src/ra-engine/RAPreview.tsx create mode 100644 src/ra-engine/raShared.ts diff --git a/src/App.css b/src/App.css index 2b02d64..50a362b 100644 --- a/src/App.css +++ b/src/App.css @@ -45,3 +45,18 @@ body { .dark .diff-added-line { background-color: rgba(34, 197, 94, 0.15); } + +/* RA Preview styling */ +.ra-prev-op { color: #7c3aed; font-weight: bold; } +.ra-prev-assign { color: #7c3aed; font-weight: bold; } +.ra-prev-logic { color: #2563eb; font-weight: bold; } +.ra-prev-str { color: #059669; } +.ra-prev-num { color: #d97706; } +.ra-prev-comment { color: #9ca3af; font-style: italic; } +.ra-prev-sub { font-size: 0.8em; color: #6d28d9; } +.dark .ra-prev-op { color: #a78bfa; } +.dark .ra-prev-assign { color: #a78bfa; } +.dark .ra-prev-logic { color: #60a5fa; } +.dark .ra-prev-str { color: #34d399; } +.dark .ra-prev-num { color: #fbbf24; } +.dark .ra-prev-sub { color: #c4b5fd; } diff --git a/src/App.tsx b/src/App.tsx index 39a85a4..83c3554 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Editor from "react-simple-code-editor"; import "./App.css"; import ResultTable from "./ResultTable"; @@ -33,6 +33,7 @@ import ViewsTable, { View } from "./ViewsTable"; import { raToSQL, RAError } from "./ra-engine/relationalAlgebra"; import RAReference from "./ra-engine/RAReference"; import { highlightRA } from "./ra-engine/raHighlight"; +import RAPreview from "./ra-engine/RAPreview"; import { Info, Settings, XCircle, CheckCircle2, Eye, EyeOff } from "lucide-react"; import ChangelogDialog from "./ChangelogDialog"; @@ -58,9 +59,20 @@ import { useLanguage, langKey, getUrlParam, setUrlParam } from "./i18n/context"; import LanguageSelector from "./LanguageSelector"; import { getQuestion } from "./QuestionSelector"; -/** Storage key for a question's query, namespaced by editor mode */ -function queryKey(questionId: number, mode: "sql" | "ra"): string { - return mode === "ra" ? `ra-questionId-${questionId}` : `questionId-${questionId}`; +/** Storage key namespaced by editor mode */ +function modeKey(base: string, mode: "sql" | "ra"): string { + return mode === "ra" ? `ra-${base}` : base; +} + +/** Get a JSON-parsed list from localStorage, with language and mode namespacing */ +function getStoredList(lang: string, key: string): number[] { + const raw = localStorage.getItem(langKey(lang, key)); + return raw ? JSON.parse(raw) : []; +} + +/** Set a JSON list in localStorage, with language namespacing */ +function setStoredList(lang: string, key: string, value: number[]): void { + localStorage.setItem(langKey(lang, key), JSON.stringify(value)); } function App() { @@ -86,7 +98,10 @@ function App() { const [exportQuery, setExportQuery] = useState(); const [exportingStatus, setExportingStatus] = useState(0); const [loadedQuestionCorrect, setLoadedQuestionCorrect] = useState(false); - const [editorMode, setEditorMode] = useState<"sql" | "ra">("sql"); + const [editorMode, setEditorMode] = useState<"sql" | "ra">(() => { + const param = getUrlParam("mode"); + return param === "ra" ? "ra" : "sql"; + }); const exportRendererRef = useRef(null); @@ -96,11 +111,11 @@ function App() { const [pendingImportData, setPendingImportData] = useState(null); // QuestionSelector needs writtenQuestions and correctQuestions to be able to display the correct state - const [writtenQuestions, setWrittenQuestions] = useState( - localStorage.getItem(langKey(lang, "writtenQuestions")) ? JSON.parse(localStorage.getItem(langKey(lang, "writtenQuestions"))!) : [] + const [writtenQuestions, setWrittenQuestions] = useState(() => + getStoredList(lang, modeKey("writtenQuestions", editorMode)) ); - const [correctQuestions, setCorrectQuestions] = useState( - localStorage.getItem(langKey(lang, "correctQuestions")) ? JSON.parse(localStorage.getItem(langKey(lang, "correctQuestions"))!) : [] + const [correctQuestions, setCorrectQuestions] = useState(() => + getStoredList(lang, modeKey("correctQuestions", editorMode)) ); // One-time migration: copy old unnamespaced keys to sv: prefix @@ -131,19 +146,11 @@ function App() { localStorage.setItem("i18n-migrated", "1"); }, []); - // Reload written/correct questions when language changes + // Reload written/correct questions when language or mode changes useEffect(() => { - setWrittenQuestions( - localStorage.getItem(langKey(lang, "writtenQuestions")) - ? JSON.parse(localStorage.getItem(langKey(lang, "writtenQuestions"))!) - : [] - ); - setCorrectQuestions( - localStorage.getItem(langKey(lang, "correctQuestions")) - ? JSON.parse(localStorage.getItem(langKey(lang, "correctQuestions"))!) - : [] - ); - }, [lang]); + setWrittenQuestions(getStoredList(lang, modeKey("writtenQuestions", editorMode))); + setCorrectQuestions(getStoredList(lang, modeKey("correctQuestions", editorMode))); + }, [lang, editorMode]); const resetResult = useCallback(() => { setResult(undefined); @@ -157,7 +164,7 @@ function App() { const switchEditorMode = useCallback((mode: "sql" | "ra") => { // Save current query before switching if (question && query !== undefined) { - const currentKey = queryKey(question.id, editorMode); + const currentKey = modeKey(`questionId-${question.id}`, editorMode); if (query === defaultQuery || query === "") { localStorage.removeItem(langKey(lang, currentKey)); } else { @@ -170,9 +177,12 @@ function App() { resetResult(); // Load query for the new mode if (question) { - const newKey = queryKey(question.id, mode); + const newKey = modeKey(`questionId-${question.id}`, mode); setQuery(localStorage.getItem(langKey(lang, newKey)) || (mode === "ra" ? "" : defaultQuery)); } + // Reload progress for the new mode + setWrittenQuestions(getStoredList(lang, modeKey("writtenQuestions", mode))); + setCorrectQuestions(getStoredList(lang, modeKey("correctQuestions", mode))); }, [resetResult, question, query, editorMode, lang, defaultQuery]); const initDb = useCallback(async () => { @@ -221,7 +231,7 @@ function App() { const resolved = getQuestion(q.id, questions); if (resolved) { setQuestion(resolved); - setQuery(localStorage.getItem(langKey(lang, queryKey(resolved.id, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); + setQuery(localStorage.getItem(langKey(lang, modeKey(`questionId-${resolved.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); } } } @@ -237,6 +247,11 @@ function App() { } }, [question]); + // Sync editor mode to URL + useEffect(() => { + setUrlParam("mode", editorMode === "ra" ? "ra" : null); + }, [editorMode]); + // Track which language the current question was loaded in const questionLangRef = useRef(lang); useEffect(() => { @@ -251,25 +266,21 @@ function App() { if (questionLangRef.current !== lang) { return; } - let wq = JSON.parse(localStorage.getItem(langKey(lang, "writtenQuestions")) || "[]"); + const wqStorageKey = modeKey("writtenQuestions", editorMode); + let wq = getStoredList(lang, wqStorageKey); const initialLength = wq.length; - const storageKey = queryKey(question.id, editorMode); + const storageKey = modeKey(`questionId-${question.id}`, editorMode); if (query === defaultQuery || query === "") { localStorage.removeItem(langKey(lang, storageKey)); - // remove from writtenQuestions if it exists there as well (only for SQL mode) - if (editorMode === "sql") { - const filtered = wq.filter((id: number) => id !== question.id); - wq = filtered; - } + wq = wq.filter((id: number) => id !== question.id); } else { localStorage.setItem(langKey(lang, storageKey), query); - // ensure that questionid is in localstorage writtenQuestions (only for SQL mode) - if (editorMode === "sql" && !wq.includes(question.id)) { + if (!wq.includes(question.id)) { wq.push(question.id); } } if (wq.length !== initialLength) { - localStorage.setItem(langKey(lang, "writtenQuestions"), JSON.stringify(wq)); + setStoredList(lang, wqStorageKey, wq); setWrittenQuestions(wq); } @@ -469,22 +480,23 @@ function App() { setIsCorrect(true); setMatchedResult(matchedAlt ?? question.evaluable_result); - const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; - localStorage.setItem(langKey(lang, correctKey), query); + const cKey = modeKey(`correctQuestionId-${question.id}`, editorMode); + localStorage.setItem(langKey(lang, cKey), query); setCorrectQueryMismatch(false); setLoadedQuestionCorrect(true); - const cq = JSON.parse(localStorage.getItem(langKey(lang, "correctQuestions")) || "[]"); + const cqStorageKey = modeKey("correctQuestions", editorMode); + const cq = getStoredList(lang, cqStorageKey); if (!cq.includes(question.id)) { cq.push(question.id); - localStorage.setItem(langKey(lang, "correctQuestions"), JSON.stringify(cq)); + setStoredList(lang, cqStorageKey, cq); setCorrectQuestions(cq); } - }, [result, question, query, evaluatedQuery, exportingStatus, lang]); + }, [result, question, query, evaluatedQuery, exportingStatus, lang, editorMode]); // Save query based on question const loadQuery = useCallback((_oldQuestion: Question | undefined, newQuestion: Question) => { - const key = queryKey(newQuestion.id, editorMode); + const key = modeKey(`questionId-${newQuestion.id}`, editorMode); setQuery(localStorage.getItem(langKey(lang, key)) || (editorMode === "ra" ? "" : defaultQuery)); // This prevents user from ctrl-z'ing to a different question if (editorRef.current) { @@ -498,8 +510,8 @@ function App() { return; } - const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; - const correctQuery = localStorage.getItem(langKey(lang, correctKey)); + const cKey = modeKey(`correctQuestionId-${question.id}`, editorMode); + const correctQuery = localStorage.getItem(langKey(lang, cKey)); if (!correctQuery) { setCorrectQueryMismatch(false); setLoadedQuestionCorrect(false); @@ -563,15 +575,20 @@ function App() { output += "/* --- BEGIN Validation --- */\n"; - output += "/* --- BEGIN Submission Summary --- */\n"; - const writtenQueries = localStorage.getItem(langKey(lang, "correctQuestions")) || "[]"; - const parsed = JSON.parse(writtenQueries) as number[]; - const questionsString = parsed.filter((id) => options === undefined || (options.include && options.include.includes(id))).map((id) => { + // Cache localStorage reads used multiple times below + const storedCorrectIds = getStoredList(lang, "correctQuestions"); + const storedWrittenIds = getStoredList(lang, "writtenQuestions"); + + const formatQuestionIds = (ids: number[]) => ids.map((id) => { const category = questions.find(c => c.questions.some(q => q.id === id))!; const q = category.questions.find(q => q.id === id)!; return { formatted: `${category.display_number}${q.display_sequence}`, number: category.display_number, sequence: q.display_sequence }; }).sort((a, b) => a.sequence.localeCompare(b.sequence)).sort((a, b) => a.number - b.number).map(q => q.formatted).join(", "); - output += `-- Written Questions: ${questionsString}\n`; + + const filterIds = (ids: number[]) => ids.filter((id) => options === undefined || (options.include && options.include.includes(id))); + + output += "/* --- BEGIN Submission Summary --- */\n"; + output += `-- Written Questions: ${formatQuestionIds(filterIds(storedCorrectIds))}\n`; output += "/* --- END Submission Summary --- */\n"; if (views.length > 0) { output += "/* --- BEGIN Views --- */\n"; @@ -593,10 +610,8 @@ function App() { } output += "/* --- BEGIN Submission Queries --- */\n"; - const queriesStr = localStorage.getItem(langKey(lang, "correctQuestions")); - if (queriesStr) { - const parsed = JSON.parse(queriesStr) as number[]; - const sorted = parsed.filter((id) => options === undefined || (options.include && options.include.includes(id))).map((id) => { + if (storedCorrectIds.length > 0) { + const sorted = filterIds(storedCorrectIds).map((id) => { const category = questions.find(c => c.questions.some(q => q.id === id))!; const q = category.questions.find(q => q.id === id)!; return { category, question: q }; @@ -627,55 +642,30 @@ function App() { output += "/* --- END Submission Queries --- */\n"; output += "/* --- BEGIN Save Summary --- */\n"; - const existingQueries = localStorage.getItem(langKey(lang, "writtenQuestions")) || "[]"; - const existingParsed = JSON.parse(existingQueries) as number[]; - const existingQuestions = existingParsed.map((id) => { - const category = questions.find(c => c.questions.some(q => q.id === id))!; - const q = category.questions.find(q => q.id === id)!; - return { formatted: `${category.display_number}${q.display_sequence}`, number: category.display_number, sequence: q.display_sequence }; - }).sort((a, b) => a.sequence.localeCompare(b.sequence)).sort((a, b) => a.number - b.number).map(q => q.formatted).join(", "); - output += `-- Written Questions: ${existingQuestions}\n`; + output += `-- Written Questions: ${formatQuestionIds(storedWrittenIds)}\n`; output += "/* --- END Save Summary --- */\n"; - output += "/* --- BEGIN Raw Queries --- */\n"; - output += "/*\n"; - const allQueries = localStorage.getItem(langKey(lang, "writtenQuestions")); - if (allQueries) { - const parsed = JSON.parse(allQueries); + const collectQueries = (ids: number[], keyPrefix: string) => { const queries: { [key: number]: string } = {}; - for (const id of parsed) { - const activeQuery = localStorage.getItem(langKey(lang, "questionId-" + id)); - if (!activeQuery) { - continue; - } - queries[id] = activeQuery; + for (const id of ids) { + const q = localStorage.getItem(langKey(lang, keyPrefix + id)); + if (q) queries[id] = q; } - output += JSON.stringify(queries, null, 0).replace(/\*\//g, "\\*/"); - } + return queries; + }; + + output += "/* --- BEGIN Raw Queries --- */\n"; + output += "/*\n"; + output += JSON.stringify(collectQueries(storedWrittenIds, "questionId-"), null, 0).replace(/\*\//g, "\\*/"); output += "\n*/\n"; output += "/* --- END Raw Queries --- */\n"; output += "/* --- BEGIN Correct Raw Queries --- */\n"; output += "/*\n"; - const allCorrectQueries = localStorage.getItem(langKey(lang, "correctQuestions")); - if (allCorrectQueries) { - const parsed = JSON.parse(allCorrectQueries); - const queries: { [key: number]: string } = {}; - for (const id of parsed) { - const activeQuery = localStorage.getItem(langKey(lang, "correctQuestionId-" + id)); - if (!activeQuery) { - continue; - } - queries[id] = activeQuery; - } - output += JSON.stringify(queries, null, 0).replace(/\*\//g, "\\*/"); - } + output += JSON.stringify(collectQueries(storedCorrectIds, "correctQuestionId-"), null, 0).replace(/\*\//g, "\\*/"); output += "\n*/\n"; output += "/* --- END Correct Raw Queries --- */\n"; output += "/* --- BEGIN Raw List Dumps --- */\n"; - output += "-- " + (localStorage.getItem(langKey(lang, "writtenQuestions")) === null ? "[]" : localStorage.getItem(langKey(lang, "writtenQuestions"))) + "\n"; - output += "-- " + (localStorage.getItem(langKey(lang, "correctQuestions")) === null ? "[]" : - JSON.stringify((JSON.parse(localStorage.getItem(langKey(lang, "correctQuestions"))!) as number[]) - .filter((id) => options === undefined || (options.include && options.include.includes(id)))) - ) + "\n"; + output += "-- " + JSON.stringify(storedWrittenIds) + "\n"; + output += "-- " + JSON.stringify(filterIds(storedCorrectIds)) + "\n"; output += "/* --- END Raw List Dumps --- */\n"; output += "/* --- END Validation --- */\n"; @@ -700,10 +690,8 @@ function App() { const applyMergedData = useCallback((merged: ParsedSaveData) => { // Clear current data - const oldWritten: number[] = JSON.parse(localStorage.getItem(langKey(lang, "writtenQuestions")) || "[]"); - oldWritten.forEach(id => localStorage.removeItem(langKey(lang, `questionId-${id}`))); - const oldCorrect: number[] = JSON.parse(localStorage.getItem(langKey(lang, "correctQuestions")) || "[]"); - oldCorrect.forEach(id => localStorage.removeItem(langKey(lang, `correctQuestionId-${id}`))); + getStoredList(lang, "writtenQuestions").forEach(id => localStorage.removeItem(langKey(lang, `questionId-${id}`))); + getStoredList(lang, "correctQuestions").forEach(id => localStorage.removeItem(langKey(lang, `correctQuestionId-${id}`))); localStorage.removeItem(langKey(lang, "writtenQuestions")); localStorage.removeItem(langKey(lang, "correctQuestions")); @@ -720,8 +708,8 @@ function App() { setWrittenQuestions(merged.writtenQuestionIds); setCorrectQuestions(merged.correctQuestionIds); - localStorage.setItem(langKey(lang, "writtenQuestions"), JSON.stringify(merged.writtenQuestionIds)); - localStorage.setItem(langKey(lang, "correctQuestions"), JSON.stringify(merged.correctQuestionIds)); + setStoredList(lang, "writtenQuestions", merged.writtenQuestionIds); + setStoredList(lang, "correctQuestions", merged.correctQuestionIds); // Update views in database for (const view of views) { @@ -782,14 +770,14 @@ function App() { return; } - const toExportQuery = localStorage.getItem(langKey(lang, `correctQuestionId-${question.id}`)); + const toExportQuery = localStorage.getItem(langKey(lang, modeKey(`correctQuestionId-${question.id}`, editorMode))); if (!toExportQuery) { return; } setExportQuery(toExportQuery); setExportQuestion(question); - }, [exportView, loadedQuestionCorrect, question, lang]); + }, [exportView, loadedQuestionCorrect, question, lang, editorMode]); const exportImageView = useCallback((name: string) => { if (!database || exportQuery) { @@ -869,9 +857,25 @@ function App() { }); }, [evaluatedQuery, exportQuery, exportRendererRef, getTheme, isDarkMode, exportingStatus, question, resetResult, setTheme, exportQuestion, exportView]); + const exportRendererProps = useMemo(() => { + if (!exportQuestion || !exportQuery) return null; + let sqlForExport = exportQuery; + if (editorMode === "ra" && database) { + try { sqlForExport = raToSQL(exportQuery, database); } catch { /* use raw */ } + } + const exportResult = evalSql(sqlForExport); + return { + isCorrect: isCorrectResult(exportQuestion.evaluable_result, exportResult) || (exportQuestion.alternative_evaluable_results?.some(alt => isCorrectResult(alt, exportResult)) ?? false), + question: exportQuestion, + code: exportQuery, + result: exportResult, + mode: editorMode, + }; + }, [exportQuestion, exportQuery, editorMode, database, evalSql]); + return (
- {exportQuestion && exportQuery && isCorrectResult(alt, evalSql(exportQuery))) ?? false), question: exportQuestion, code: exportQuery, result: evalSql(exportQuery)}} ref={exportRendererRef} />} + {exportRendererProps && } {exportView && }
@@ -954,6 +958,7 @@ function App() { ref={editorRef} /> } + {editorMode === "ra" && query !== undefined && }
{/* Error/Warning and Action Buttons - Same Row */} @@ -985,8 +990,7 @@ function App() { variant="outline" onClick={() => { if (!question) return; - const correctKey = editorMode === "ra" ? `ra-correctQuestionId-${question.id}` : `correctQuestionId-${question.id}`; - setQuery(localStorage.getItem(langKey(lang, correctKey)) || (editorMode === "ra" ? "" : defaultQuery)); + setQuery(localStorage.getItem(langKey(lang, modeKey(`correctQuestionId-${question.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); }} className="border-yellow-500 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-50 dark:hover:bg-yellow-900/20" > diff --git a/src/ExportRenderer.tsx b/src/ExportRenderer.tsx index cfa48de..f80ba19 100644 --- a/src/ExportRenderer.tsx +++ b/src/ExportRenderer.tsx @@ -8,6 +8,7 @@ import ResultTable from "./ResultTable"; import { Question } from "./QuestionSelector"; import { View } from "./ViewsTable"; import { useLanguage } from "./i18n/context"; +import { renderRAPreview } from "./ra-engine/RAPreview"; interface ExportRendererProps { query?: { @@ -15,6 +16,7 @@ interface ExportRendererProps { isCorrect: boolean; code: string; result: Result; + mode?: "sql" | "ra"; }; view?: { view: View; @@ -55,23 +57,30 @@ const ExportRenderer = React.forwardRef(({

{t("exportViewCodeLabel", { name: view.view.name })}

} - null} - highlight={code => highlight(code, languages.sql)} - padding={10} - tabSize={4} - className="font-mono text-xl w-full bg-slate-200 border-2 max-w-4xl min-h-40 border-none my-2" - /> + {query?.mode === "ra" ? ( +
+ ) : ( + null} + highlight={code => highlight(code, languages.sql)} + padding={10} + tabSize={4} + className="font-mono text-xl w-full bg-slate-200 border-2 max-w-4xl min-h-40 border-none my-2" + /> + )} {query &&

{t("exportResultLabel")}

} diff --git a/src/ra-engine/RAPreview.tsx b/src/ra-engine/RAPreview.tsx new file mode 100644 index 0000000..6f59850 --- /dev/null +++ b/src/ra-engine/RAPreview.tsx @@ -0,0 +1,204 @@ +/* +Relational Algebra engine for sql-validator +Copyright (C) 2026 E.SU. IT AB (Org.no 559484-0505) and Edwin Sundberg + +Licensed under the Business Source License 1.1 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License in the LICENSE.md file in this repository. +*/ + +import { useMemo } from "react"; +import { + UNARY_SYMBOLS, BINARY_SYMBOLS, + LOGIC_KEYWORDS, UNARY_KEYWORD_SYMBOLS, BINARY_KEYWORD_SYMBOLS, + IDENT_START, IDENT_CHAR, + esc, skipWs, highlightSubContent, extractBracketContent, extractImplicitSubscript, +} from "./raShared"; + +/** + * Renders a relational algebra expression with proper formatting: + * - Keyword operators replaced with Unicode symbols + * - Bracket content rendered as subscripts using tags + * - Assignment arrows rendered as ← + */ + +const previewWrap = (type: string, text: string) => + `${text}`; + +function renderSubscriptHtml(content: string): string { + return `${highlightSubContent(content, previewWrap)}`; +} + +/** Consume a subscript and return [newIndex, html] */ +function consumeSubscript(code: string, i: number): [number, string] { + const beforeWs = i; + const wsI = skipWs(code, i); + + // Try bracket content + const [bracketI, bracketContent] = extractBracketContent(code, i); + if (bracketContent !== null) { + return [bracketI, renderSubscriptHtml(bracketContent)]; + } + + // Try implicit subscript + const [implI, implContent] = extractImplicitSubscript(code, wsI, beforeWs); + if (implContent !== null) { + return [implI, renderSubscriptHtml(implContent)]; + } + + return [beforeWs, ""]; +} + +export function renderRAPreview(code: string): string { + const result: string[] = []; + let i = 0; + + while (i < code.length) { + // Comments + if (code[i] === "-" && i + 1 < code.length && code[i + 1] === "-") { + let comment = ""; + while (i < code.length && code[i] !== "\n") { comment += code[i]; i++; } + result.push(`${esc(comment)}`); + continue; + } + + if (code[i] === "\n") { + result.push("
"); + i++; + continue; + } + + // Unicode unary operators + if (UNARY_SYMBOLS.has(code[i])) { + result.push(`${esc(code[i])}`); + i++; + const [newI, html] = consumeSubscript(code, i); + i = newI; + result.push(html); + continue; + } + + // |X| or |><| natural join + if (code[i] === "|") { + if (i + 2 < code.length && (code[i + 1] === "X" || code[i + 1] === "x") && code[i + 2] === "|") { + result.push(""); + i += 3; + continue; + } + if (i + 3 < code.length && code[i + 1] === ">" && code[i + 2] === "<" && code[i + 3] === "|") { + result.push(""); + i += 4; + continue; + } + } + + // Binary symbols + if (BINARY_SYMBOLS.has(code[i])) { + result.push(`${esc(code[i])}`); + i++; + if (i < code.length && (code[i] === "[" || code[i] === "{" || code[i] === "_")) { + const [newI, html] = consumeSubscript(code, i); + i = newI; + result.push(html); + } + continue; + } + + // <- assignment + if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { + result.push(""); + i += 2; + continue; + } + + // -> rename + if (code[i] === "-" && i + 1 < code.length && code[i + 1] === ">") { + result.push(""); + i += 2; + continue; + } + + // String literals + if (code[i] === "'") { + let str = "'"; + i++; + while (i < code.length && code[i] !== "'") { str += code[i]; i++; } + if (i < code.length) { str += "'"; i++; } + result.push(`${esc(str)}`); + continue; + } + + // Numbers + if (/\d/.test(code[i])) { + let num = ""; + while (i < code.length && /[\d.]/.test(code[i])) { num += code[i]; i++; } + result.push(`${esc(num)}`); + continue; + } + + // Identifiers and keywords + if (IDENT_START.test(code[i])) { + let ident = ""; + while (i < code.length && IDENT_CHAR.test(code[i])) { ident += code[i]; i++; } + const lower = ident.toLowerCase(); + if (UNARY_KEYWORD_SYMBOLS[lower]) { + result.push(`${UNARY_KEYWORD_SYMBOLS[lower]}`); + const [newI, html] = consumeSubscript(code, i); + i = newI; + result.push(html); + } else if (BINARY_KEYWORD_SYMBOLS[lower]) { + result.push(`${BINARY_KEYWORD_SYMBOLS[lower]}`); + const j = skipWs(code, i); + if (j < code.length && (code[j] === "[" || code[j] === "{" || code[j] === "_")) { + const [newI, html] = consumeSubscript(code, j); + i = newI; + result.push(html); + } + } else if (LOGIC_KEYWORDS.has(lower)) { + result.push(`${esc(ident)}`); + } else { + result.push(esc(ident)); + } + continue; + } + + // Comparison operators + if ("<>!=".includes(code[i])) { + let op = code[i]; i++; + if (i < code.length && (code[i] === "=" || code[i] === ">")) { op += code[i]; i++; } + result.push(`${esc(op)}`); + continue; + } + + // Everything else + result.push(esc(code[i])); + i++; + } + + return result.join(""); +} + +interface RAPreviewProps { + code: string; +} + +export default function RAPreview({ code }: RAPreviewProps) { + const html = useMemo(() => { + const trimmed = code.trim(); + if (!trimmed || trimmed.startsWith("--") && !trimmed.includes("\n")) { + return null; + } + return renderRAPreview(trimmed); + }, [code]); + + if (!html) return null; + + return ( +
+
+
+ ); +} diff --git a/src/ra-engine/raHighlight.ts b/src/ra-engine/raHighlight.ts index 549e1a4..5745a22 100644 --- a/src/ra-engine/raHighlight.ts +++ b/src/ra-engine/raHighlight.ts @@ -11,45 +11,33 @@ You may obtain a copy of the License in the LICENSE.md file in this repository. * Custom syntax highlighter for relational algebra expressions. * Returns HTML with subscript rendering for bracket content and * colored tokens for operators, keywords, strings, etc. + * + * IMPORTANT: This highlighter must preserve character count (each input char + * maps to exactly one output char) for editor overlay alignment. */ -// Escape HTML special characters -function esc(s: string): string { - return s - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} - -// RA operator symbols -const UNARY_SYMBOLS = new Set(["σ", "π", "ρ", "γ", "τ", "δ"]); -const BINARY_SYMBOLS = new Set(["×", "⋈", "∪", "∩", "−", "÷", "⟕", "⟖", "⟗", "⋉", "⋊", "▷", "←"]); - -const UNARY_KEYWORDS = new Set([ - "sigma", "select", "pi", "project", "rho", "rename", - "gamma", "tau", "sort", "delta", "distinct", -]); -const BINARY_KEYWORDS = new Set([ - "cross", "natjoin", "join", "union", "intersect", "minus", - "divide", "leftjoin", "rightjoin", "fulljoin", - "leftsemijoin", "rightsemijoin", "antijoin", -]); -const LOGIC_KEYWORDS = new Set(["and", "or", "not"]); +import { + UNARY_SYMBOLS, BINARY_SYMBOLS, UNARY_KEYWORDS, BINARY_KEYWORDS, + LOGIC_KEYWORDS, IDENT_START, IDENT_CHAR, + esc, skipWs, highlightSubContent, extractBracketContent, extractImplicitSubscript, +} from "./raShared"; -// CSS classes (inline styles for portability with the code editor) +// Inline styles for portability with the code editor const S = { - op: "color: #7c3aed; font-weight: bold;", // purple - operators - kw: "color: #7c3aed; font-weight: bold;", // purple - keyword operators - logic: "color: #2563eb; font-weight: bold;", // blue - AND/OR/NOT - str: "color: #059669;", // green - strings - num: "color: #d97706;", // amber - numbers - comment: "color: #9ca3af; font-style: italic;", // gray - comments - bracket: "color: #a1a1aa; opacity: 0.5;", // zinc, faint - brackets (subscript delimiters) - assign: "color: #7c3aed; font-weight: bold;", // purple - assignment arrow - sub: "color: #c084fc; font-weight: 500;", // light purple - subscript content (readable, distinct from plain text) + op: "color: #7c3aed; font-weight: bold;", + kw: "color: #7c3aed; font-weight: bold;", + logic: "color: #2563eb; font-weight: bold;", + str: "color: #059669;", + num: "color: #d97706;", + comment: "color: #9ca3af; font-style: italic;", + bracket: "color: #a1a1aa; opacity: 0.5;", + assign: "color: #7c3aed; font-weight: bold;", + sub: "color: #c084fc; font-weight: 500;", }; +const subWrap = (type: string, text: string) => + `${text}`; + /** * Highlight a relational algebra expression, rendering bracket content * as subscripts and coloring operators/keywords. @@ -81,7 +69,6 @@ export function highlightRA(code: string): string { if (UNARY_SYMBOLS.has(code[i])) { result.push(`${esc(code[i])}`); i++; - // Check for subscript: _{ }, [ ], { }, or implicit (tokens until '(') i = renderSubscript(code, i, result); continue; } @@ -103,14 +90,12 @@ export function highlightRA(code: string): string { if (BINARY_SYMBOLS.has(code[i])) { result.push(`${esc(code[i])}`); i++; - // Binary ops can have bracket conditions: ⋈[cond] or ⋈{cond} if (i < code.length && (code[i] === "[" || code[i] === "{" || code[i] === "_")) { i = renderSubscript(code, i, result); } continue; } - // Assignment arrow: ← (already handled above as binary symbol) // Arrow: <- (assignment) — keep both chars for editor alignment if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { result.push(`<-`); @@ -150,23 +135,20 @@ export function highlightRA(code: string): string { } // Identifiers and keywords - if (/[a-zA-Z_\u00C0-\u024F]/.test(code[i])) { + if (IDENT_START.test(code[i])) { let ident = ""; - while (i < code.length && /[a-zA-Z0-9_\u00C0-\u024F]/.test(code[i])) { + while (i < code.length && IDENT_CHAR.test(code[i])) { ident += code[i]; i++; } const lower = ident.toLowerCase(); if (UNARY_KEYWORDS.has(lower)) { result.push(`${esc(ident)}`); - // Check for subscript after keyword i = renderSubscript(code, i, result); } else if (BINARY_KEYWORDS.has(lower)) { result.push(`${esc(ident)}`); - // Binary keyword may have bracket conditions - const j = skipWhitespace(code, i); + const j = skipWs(code, i); if (j < code.length && (code[j] === "[" || code[j] === "{" || code[j] === "_")) { - // Include the whitespace before the bracket if (j > i) result.push(esc(code.slice(i, j))); i = renderSubscript(code, j, result); } @@ -198,22 +180,15 @@ export function highlightRA(code: string): string { return result.join(""); } -function skipWhitespace(code: string, i: number): number { - while (i < code.length && code[i] === " ") i++; - return i; -} - /** * Render a subscript section after a unary/binary operator. * Handles: _{ }, [ ], { }, or implicit (content until '('). * Returns the new index position. */ function renderSubscript(code: string, i: number, result: string[]): number { - // Skip whitespace const beforeWs = i; - while (i < code.length && code[i] === " ") i++; + i = skipWs(code, i); - // Check what follows if (i >= code.length) { if (i > beforeWs) result.push(code.slice(beforeWs, i)); return i; @@ -223,122 +198,34 @@ function renderSubscript(code: string, i: number, result: string[]): number { if (code[i] === "_") { result.push(`${esc("_")}`); i++; - while (i < code.length && code[i] === " ") i++; + i = skipWs(code, i); } + // Try bracket content if (code[i] === "[" || code[i] === "{") { - // Bracketed subscript — render brackets faintly, content as subscript const openBracket = code[i]; result.push(`${esc(openBracket)}`); - i++; // past opening bracket - let depth = 1; - let content = ""; - while (i < code.length && depth > 0) { - if (code[i] === "[" || code[i] === "{") depth++; - if (code[i] === "]" || code[i] === "}") depth--; - if (depth > 0) { - content += code[i]; + const [newI, content] = extractBracketContent(code, i); + if (content !== null) { + result.push(`${highlightSubContent(content, subWrap)}`); + // Render closing bracket + if (newI > i + 1) { + const closeBracket = code[newI - 1]; + result.push(`${esc(closeBracket)}`); } - i++; - } - // Render inner content with subscript styling - result.push(`${highlightSubscriptContent(content)}`); - // Render closing bracket faintly (the character at i-1 was the closing bracket) - if (depth === 0) { - const closeBracket = code[i - 1]; - result.push(`${esc(closeBracket)}`); + return newI; } - return i; } - // Not a bracket — check for implicit subscript (content until '(') - // Only apply for unary operators where we expect a subscript - if (code[i] !== "(" && i > beforeWs) { - // Emit the whitespace between operator and subscript content - result.push(code.slice(beforeWs, i)); - // Collect content until '(' as implicit subscript - let content = ""; - while (i < code.length && code[i] !== "(" && code[i] !== "\n") { - content += code[i]; - i++; - } - // Trim trailing whitespace from content but keep the position - const trimmed = content.trimEnd(); - const trimDiff = content.length - trimmed.length; - if (trimDiff > 0) i -= trimDiff; - if (trimmed.length > 0) { - result.push(`${highlightSubscriptContent(trimmed)}`); - } - return i; + // Implicit subscript + const [newI, content] = extractImplicitSubscript(code, i, beforeWs); + if (content !== null) { + result.push(code.slice(beforeWs, i)); // whitespace + result.push(`${highlightSubContent(content, subWrap)}`); + return newI; } // No subscript found — restore whitespace if (i > beforeWs) result.push(code.slice(beforeWs, i)); return i; } - -/** - * Highlight content inside a subscript (conditions, column lists, etc.) - */ -function highlightSubscriptContent(content: string): string { - const parts: string[] = []; - let i = 0; - - while (i < content.length) { - // String literals - if (content[i] === "'") { - let str = "'"; - i++; - while (i < content.length && content[i] !== "'") { str += content[i]; i++; } - if (i < content.length) { str += "'"; i++; } - parts.push(`${esc(str)}`); - continue; - } - // Numbers - if (/\d/.test(content[i])) { - let num = ""; - while (i < content.length && /[\d.]/.test(content[i])) { num += content[i]; i++; } - parts.push(`${esc(num)}`); - continue; - } - // Keywords and identifiers - if (/[a-zA-Z_\u00C0-\u024F]/.test(content[i])) { - let ident = ""; - while (i < content.length && /[a-zA-Z0-9_\u00C0-\u024F]/.test(content[i])) { ident += content[i]; i++; } - const lower = ident.toLowerCase(); - if (LOGIC_KEYWORDS.has(lower)) { - parts.push(`${esc(ident)}`); - } else if (lower === "desc" || lower === "asc" || lower === "as") { - parts.push(`${esc(ident)}`); - } else if (["count", "sum", "avg", "min", "max"].includes(lower)) { - parts.push(`${esc(ident)}`); - } else { - parts.push(esc(ident)); - } - continue; - } - // Arrow → or -> - if (content[i] === "→") { - parts.push(``); - i++; - continue; - } - if (content[i] === "-" && i + 1 < content.length && content[i + 1] === ">") { - parts.push(`->`); - i += 2; - continue; - } - // Comparison ops - if (content[i] === "<" || content[i] === ">" || content[i] === "!" || content[i] === "=") { - let op = content[i]; i++; - if (i < content.length && (content[i] === "=" || content[i] === ">")) { op += content[i]; i++; } - parts.push(`${esc(op)}`); - continue; - } - // Everything else - parts.push(esc(content[i])); - i++; - } - - return parts.join(""); -} diff --git a/src/ra-engine/raShared.ts b/src/ra-engine/raShared.ts new file mode 100644 index 0000000..c8215eb --- /dev/null +++ b/src/ra-engine/raShared.ts @@ -0,0 +1,197 @@ +/* +Relational Algebra engine for sql-validator +Copyright (C) 2026 E.SU. IT AB (Org.no 559484-0505) and Edwin Sundberg + +Licensed under the Business Source License 1.1 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License in the LICENSE.md file in this repository. +*/ + +/** + * Shared constants and utilities for RA highlighting and preview rendering. + */ + +// RA operator symbol sets +export const UNARY_SYMBOLS = new Set(["σ", "π", "ρ", "γ", "τ", "δ"]); +export const BINARY_SYMBOLS = new Set(["×", "⋈", "∪", "∩", "−", "÷", "⟕", "⟖", "⟗", "⋉", "⋊", "▷", "←"]); + +// RA keyword sets (lowercase) +export const UNARY_KEYWORDS = new Set([ + "sigma", "select", "pi", "project", "rho", "rename", + "gamma", "tau", "sort", "delta", "distinct", +]); +export const BINARY_KEYWORDS = new Set([ + "cross", "natjoin", "join", "union", "intersect", "minus", + "divide", "leftjoin", "rightjoin", "fulljoin", + "leftsemijoin", "rightsemijoin", "antijoin", +]); +export const LOGIC_KEYWORDS = new Set(["and", "or", "not"]); +export const MODIFIER_KEYWORDS = new Set(["asc", "desc", "as"]); +export const AGGREGATE_KEYWORDS = new Set(["count", "sum", "avg", "min", "max"]); + +// Keyword to Unicode symbol mappings (used by RAPreview for pretty-printing) +export const UNARY_KEYWORD_SYMBOLS: Record = { + sigma: "σ", select: "σ", + pi: "π", project: "π", + rho: "ρ", rename: "ρ", + gamma: "γ", + tau: "τ", sort: "τ", + delta: "δ", distinct: "δ", +}; +export const BINARY_KEYWORD_SYMBOLS: Record = { + cross: "×", + natjoin: "⋈", join: "⋈", + union: "∪", + intersect: "∩", + minus: "−", + divide: "÷", + leftjoin: "⟕", + rightjoin: "⟖", + fulljoin: "⟗", + leftsemijoin: "⋉", + rightsemijoin: "⋊", + antijoin: "▷", +}; + +/** Escape HTML special characters */ +export function esc(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +/** Skip whitespace (spaces only) */ +export function skipWs(code: string, i: number): number { + while (i < code.length && code[i] === " ") i++; + return i; +} + +/** Regex patterns for identifier characters */ +export const IDENT_START = /[a-zA-Z_\u00C0-\u024F]/; +export const IDENT_CHAR = /[a-zA-Z0-9_\u00C0-\u024F]/; + +/** + * A wrapper function that produces a styled HTML span. + * Takes a semantic token type and the escaped text. + */ +export type StyleWrapper = (type: "op" | "logic" | "str" | "num" | "ident", text: string) => string; + +/** + * Tokenize and highlight subscript content (conditions, column lists, etc.) + * using the provided style wrapper for output. + */ +export function highlightSubContent(content: string, wrap: StyleWrapper): string { + const parts: string[] = []; + let i = 0; + + while (i < content.length) { + // String literals + if (content[i] === "'") { + let str = "'"; + i++; + while (i < content.length && content[i] !== "'") { str += content[i]; i++; } + if (i < content.length) { str += "'"; i++; } + parts.push(wrap("str", esc(str))); + continue; + } + // Numbers + if (/\d/.test(content[i])) { + let num = ""; + while (i < content.length && /[\d.]/.test(content[i])) { num += content[i]; i++; } + parts.push(wrap("num", esc(num))); + continue; + } + // Keywords and identifiers + if (IDENT_START.test(content[i])) { + let ident = ""; + while (i < content.length && IDENT_CHAR.test(content[i])) { ident += content[i]; i++; } + const lower = ident.toLowerCase(); + if (LOGIC_KEYWORDS.has(lower) || MODIFIER_KEYWORDS.has(lower)) { + parts.push(wrap("logic", esc(ident))); + } else if (AGGREGATE_KEYWORDS.has(lower)) { + parts.push(wrap("op", esc(ident))); + } else { + parts.push(esc(ident)); + } + continue; + } + // Arrow → or -> + if (content[i] === "→") { + parts.push(wrap("op", "→")); + i++; + continue; + } + if (content[i] === "-" && i + 1 < content.length && content[i + 1] === ">") { + parts.push(wrap("op", esc("->"))); + i += 2; + continue; + } + // Comparison ops + if ("<>!=".includes(content[i])) { + let op = content[i]; i++; + if (i < content.length && (content[i] === "=" || content[i] === ">")) { op += content[i]; i++; } + parts.push(wrap("logic", esc(op))); + continue; + } + // Everything else + parts.push(esc(content[i])); + i++; + } + + return parts.join(""); +} + +/** + * Extract bracket content from code starting at position i. + * Handles [..], {..}, and _{..} patterns. + * Returns [newIndex, extractedContent] or [originalIndex, null] if no bracket found. + */ +export function extractBracketContent(code: string, startI: number): [number, string | null] { + let i = skipWs(code, startI); + if (i >= code.length) return [startI, null]; + + // Handle _{ or _[ prefix + if (code[i] === "_") { + i++; + i = skipWs(code, i); + } + + if (code[i] === "[" || code[i] === "{") { + i++; // past opening bracket + let depth = 1; + let content = ""; + while (i < code.length && depth > 0) { + if (code[i] === "[" || code[i] === "{") depth++; + if (code[i] === "]" || code[i] === "}") depth--; + if (depth > 0) content += code[i]; + i++; + } + return [i, content]; + } + + return [startI, null]; +} + +/** + * Extract implicit subscript content (tokens until '(' or newline). + * Returns [newIndex, extractedContent] or [originalIndex, null] if nothing found. + */ +export function extractImplicitSubscript(code: string, startI: number, beforeWs: number): [number, string | null] { + let i = startI; + if (code[i] === "(" || i <= beforeWs) return [startI, null]; + + let content = ""; + while (i < code.length && code[i] !== "(" && code[i] !== "\n") { + content += code[i]; + i++; + } + const trimmed = content.trimEnd(); + const trimDiff = content.length - trimmed.length; + if (trimDiff > 0) i -= trimDiff; + if (trimmed.length > 0) { + return [i, trimmed]; + } + return [startI, null]; +} From 1aa3a2c5b278df444c3f57a880e9be9ac38fe169 Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:37:55 +0000 Subject: [PATCH 05/13] Fix SQL generation and add comprehensive execution tests --- src/App.test.tsx | 8 - src/ra-engine/raHighlight.ts | 8 +- src/ra-engine/relationalAlgebra.test.ts | 623 +++++++++++++++++++++++- src/ra-engine/relationalAlgebra.ts | 113 ++++- 4 files changed, 722 insertions(+), 30 deletions(-) delete mode 100644 src/App.test.tsx diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 9382b9a..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import App from "./App"; - -test("renders learn react link", () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/ra-engine/raHighlight.ts b/src/ra-engine/raHighlight.ts index 5745a22..d22c76a 100644 --- a/src/ra-engine/raHighlight.ts +++ b/src/ra-engine/raHighlight.ts @@ -208,10 +208,12 @@ function renderSubscript(code: string, i: number, result: string[]): number { const [newI, content] = extractBracketContent(code, i); if (content !== null) { result.push(`${highlightSubContent(content, subWrap)}`); - // Render closing bracket - if (newI > i + 1) { + // Render closing bracket (only if bracket was actually closed) + if (newI > i + 1 && newI <= code.length) { const closeBracket = code[newI - 1]; - result.push(`${esc(closeBracket)}`); + if (closeBracket === "]" || closeBracket === "}") { + result.push(`${esc(closeBracket)}`); + } } return newI; } diff --git a/src/ra-engine/relationalAlgebra.test.ts b/src/ra-engine/relationalAlgebra.test.ts index 3b16610..70bfa1f 100644 --- a/src/ra-engine/relationalAlgebra.test.ts +++ b/src/ra-engine/relationalAlgebra.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import initSqlJs from "sql.js"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import initSqlJs, { type Database } from "sql.js"; import { raToSQL, RAError } from "./relationalAlgebra"; // ─── Helper ───────────────────────────────────────────────────────────────── @@ -281,6 +281,52 @@ describe("set operations", () => { const sql = raToSQL("A \\ B"); expect(norm(sql)).toContain("EXCEPT"); }); + + it("should generate valid SQL for bare table set operations", () => { + // Set ops need SELECT statements on both sides, not bare table names + const unionSql = norm(raToSQL("A union B")); + expect(unionSql).toMatch(/SELECT \* FROM A\s+UNION\s+SELECT \* FROM B/i); + + const exceptSql = norm(raToSQL("A minus B")); + expect(exceptSql).toMatch(/SELECT \* FROM A\s+EXCEPT\s+SELECT \* FROM B/i); + + const intersectSql = norm(raToSQL("A intersect B")); + expect(intersectSql).toMatch(/SELECT \* FROM A\s+INTERSECT\s+SELECT \* FROM B/i); + }); + + it("should handle set difference with hyphenated expression", () => { + const sql = raToSQL("PI[name, city](Person - Teacher)"); + expect(norm(sql)).toContain("EXCEPT"); + expect(norm(sql)).toContain("SELECT name, city"); + }); + + it("should error on union-incompatible column counts when database is provided", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE A (x INTEGER, y TEXT)"); + db.run("CREATE TABLE B (x INTEGER, y TEXT, z TEXT)"); + + expect(() => raToSQL("A union B", db)).toThrow(RAError); + expect(() => raToSQL("A union B", db)).toThrow(/same number of columns/i); + expect(() => raToSQL("A minus B", db)).toThrow(RAError); + expect(() => raToSQL("A intersect B", db)).toThrow(RAError); + + // Should NOT throw when column counts match + db.run("CREATE TABLE C (a INTEGER, b TEXT)"); + expect(() => raToSQL("A union C", db)).not.toThrow(); + db.close(); + }); + + it("should include column names in union-incompatible error message", async () => { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + db.run("CREATE TABLE R1 (name TEXT, city TEXT)"); + db.run("CREATE TABLE R2 (name TEXT, city TEXT, age INTEGER)"); + + expect(() => raToSQL("R1 union R2", db)).toThrow(/Left has 2/); + expect(() => raToSQL("R1 union R2", db)).toThrow(/right has 3/); + db.close(); + }); }); // ─── Outer joins ──────────────────────────────────────────────────────────── @@ -793,3 +839,576 @@ describe("implicit return from last assignment", () => { expect(norm(sql)).toContain("SELECT * FROM X_v3"); }); }); + +// ─── SQLite execution ────────────────────────────────────────────────────── +// Every generated SQL statement must actually execute against a real database. + +describe("SQLite execution", () => { + let db: Database; + + beforeAll(async () => { + const SQL = await initSqlJs(); + db = new SQL.Database(); + db.run(` + CREATE TABLE Person (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, city TEXT); + INSERT INTO Person VALUES (1, 'Alice', 25, 'Stockholm'); + INSERT INTO Person VALUES (2, 'Bob', 19, 'York'); + INSERT INTO Person VALUES (3, 'Carol', 30, 'Bristol'); + + CREATE TABLE Student (id INTEGER PRIMARY KEY, hasDisability INTEGER); + INSERT INTO Student VALUES (1, 0); + INSERT INTO Student VALUES (2, 1); + + CREATE TABLE Teacher (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, city TEXT, department TEXT); + INSERT INTO Teacher VALUES (10, 'Dave', 45, 'Stockholm', 'CS'); + INSERT INTO Teacher VALUES (11, 'Eve', 38, 'York', 'Math'); + + CREATE TABLE Course (course_id INTEGER PRIMARY KEY, title TEXT, credits INTEGER); + INSERT INTO Course VALUES (100, 'Databases', 7); + INSERT INTO Course VALUES (101, 'Algorithms', 5); + + CREATE TABLE Enrollment (id INTEGER, course_id INTEGER); + INSERT INTO Enrollment VALUES (1, 100); + INSERT INTO Enrollment VALUES (1, 101); + INSERT INTO Enrollment VALUES (2, 100); + `); + }); + + afterAll(() => db.close()); + + /** Convert RA to SQL and execute, returning result rows */ + function execRA(expr: string): initSqlJs.QueryExecResult[] { + const sql = raToSQL(expr, db); + return db.exec(sql); + } + + // ── Selection ── + + it("σ with condition", () => { + const res = execRA("σ[age > 20](Person)"); + expect(res[0].values.length).toBe(2); // Alice (25) and Carol (30) + }); + + it("σ with string comparison", () => { + const res = execRA("σ[name = 'Alice'](Person)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); + + it("σ with compound condition", () => { + const res = execRA("σ[age > 20 and city = 'Stockholm'](Person)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); + + // ── Projection ── + + it("π selects columns", () => { + const res = execRA("π[name, city](Person)"); + expect(res[0].columns).toEqual(["name", "city"]); + expect(res[0].values.length).toBe(3); + }); + + // ── Rename ── + + it("ρ renames columns", () => { + const res = execRA("ρ[name→fullName](Person)"); + expect(res[0].columns).toContain("fullName"); + expect(res[0].columns).not.toContain("name"); + }); + + // ── Natural join ── + + it("⋈ natural join", () => { + const res = execRA("Person ⋈ Student"); + expect(res[0].values.length).toBe(2); // ids 1 and 2 match + }); + + it("natjoin keyword", () => { + const res = execRA("Person natjoin Student"); + expect(res[0].values.length).toBe(2); + }); + + // ── Cross product ── + + it("× cross product", () => { + const res = execRA("Person × Course"); + expect(res[0].values.length).toBe(6); // 3 × 2 + }); + + // ── Theta join ── + + it("⋈[cond] theta join", () => { + // Use unqualified column names since the generator aliases tables as _raN + const res = execRA("Person ⋈[age > credits] Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + // ── Set operations ── + + it("∪ union", () => { + const res = execRA("π[name](Person) ∪ π[name](Teacher)"); + expect(res[0].values.length).toBe(5); // 3 + 2, all distinct names + }); + + it("∩ intersect", () => { + // No overlapping names between Person and Teacher + const res = execRA("π[name](Person) ∩ π[name](Teacher)"); + expect(res.length === 0 || res[0].values.length === 0).toBe(true); + }); + + it("− set difference", () => { + const res = execRA("π[name](Person) − π[name](Teacher)"); + expect(res[0].values.length).toBe(3); // All Person names, none overlap + }); + + it("minus keyword", () => { + const res = execRA("π[name](Person) minus π[name](Teacher)"); + expect(res[0].values.length).toBe(3); + }); + + it("bare table set difference (Person - Teacher)", () => { + // Person and Teacher are union-compatible (both have id, name, age, city) + // but Teacher has an extra column (department) — use projections + const res = execRA("π[id, name](Person) − π[id, name](Teacher)"); + expect(res[0].values.length).toBe(3); + }); + + it("set difference with hyphen syntax", () => { + const res = execRA("π[name, city](Person) - π[name, city](Teacher)"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + it("errors on union-incompatible set operations", () => { + // Person has 4 cols (id, name, age, city), Course has 3 cols (course_id, title, credits) + expect(() => execRA("Person union Course")).toThrow(RAError); + expect(() => execRA("Person union Course")).toThrow(/same number of columns/i); + expect(() => execRA("Person minus Course")).toThrow(RAError); + expect(() => execRA("Person intersect Course")).toThrow(RAError); + }); + + it("backslash set difference", () => { + const res = execRA("π[name](Person) \\ π[name](Teacher)"); + expect(res[0].values.length).toBe(3); + }); + + // ── Outer joins ── + + it("leftjoin", () => { + // Use unqualified column names since the generator aliases tables as _raN + const res = execRA("Person leftjoin[age > credits] Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + // ── Semi-join ── + + it("⋉ left semi-join returns only matching rows", () => { + const res = execRA("Person ⋉ Student"); + // Person ids 1,2,3 — Student ids 1,2 — semi-join on common col "id" + expect(res[0].values.length).toBe(2); + }); + + // ── Anti-join ── + + it("▷ anti-join returns only non-matching rows", () => { + const res = execRA("Person ▷ Student"); + // Only Carol (id=3) has no matching Student row + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Carol"); + }); + + // ── Distinct ── + + it("δ distinct", () => { + const res = execRA("δ(Person)"); + expect(res[0].values.length).toBe(3); + }); + + // ── Sort ── + + it("τ sort", () => { + const res = execRA("τ[name](Person)"); + expect(res[0].values[0]).toContain("Alice"); + expect(res[0].values[2]).toContain("Carol"); + }); + + it("τ sort DESC", () => { + const res = execRA("τ[age DESC](Person)"); + expect(res[0].values[0]).toContain("Carol"); // age 30, highest + }); + + // ── Grouping / aggregation ── + + it("γ group by with COUNT", () => { + const res = execRA("γ[city; COUNT(id) AS cnt](Person)"); + expect(res[0].columns).toContain("cnt"); + expect(res[0].values.length).toBe(3); // 3 distinct cities + }); + + // ── Division ── + + it("÷ division returns correct result", () => { + // Enrollment: (1,100),(1,101),(2,100) — Course: (100),(101) + // Only id=1 is enrolled in ALL courses + const res = execRA("π[id, course_id](Enrollment) ÷ π[course_id](Course)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain(1); // id=1 (Alice) + }); + + // ── Composition / nesting ── + + it("π of σ", () => { + const res = execRA("π[name](σ[age > 20](Person))"); + expect(res[0].columns).toEqual(["name"]); + expect(res[0].values.length).toBe(2); + }); + + it("σ of ⋈", () => { + const res = execRA("σ[age > 20](Person ⋈ Student)"); + expect(res[0].values.length).toBe(1); // Only Alice (25) matches + }); + + it("deeply nested", () => { + const res = execRA("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); + + // ── Parenthesis-free syntax ── + + it("σ[cond] Table without parens", () => { + const res = execRA("σ[age > 20] Person"); + expect(res[0].values.length).toBe(2); + }); + + it("π[cols] σ[cond] Table chained", () => { + const res = execRA("π[name] σ[age > 20] Person"); + expect(res[0].columns).toEqual(["name"]); + expect(res[0].values.length).toBe(2); + }); + + // ── Implicit subscripts ── + + it("σ cond (R) implicit", () => { + const res = execRA("σ age > 20 (Person)"); + expect(res[0].values.length).toBe(2); + }); + + it("π cols (R) implicit", () => { + const res = execRA("π name, city (Person)"); + expect(res[0].columns).toEqual(["name", "city"]); + }); + + // ── Assignments ── + + it("single assignment", () => { + const res = execRA("A ← σ[age > 20](Person)\nπ[name](A)"); + expect(res[0].values.length).toBe(2); + }); + + it("multiple assignments", () => { + const res = execRA("A ← σ[city = 'Stockholm'](Person)\nB ← π[name](A)\nB"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); + + it("variable reassignment", () => { + const res = execRA("X ← Person\nX ← σ[age > 20](X)\nX ← π[name](X)"); + expect(res[0].values.length).toBe(2); + }); + + // ── LaTeX-style curly braces ── + + it("σ_{cond}(R)", () => { + const res = execRA("σ_{age > 20}(Person)"); + expect(res[0].values.length).toBe(2); + }); + + it("π_{cols}(R)", () => { + const res = execRA("π_{name, city}(Person)"); + expect(res[0].columns).toEqual(["name", "city"]); + }); + + it("ρ_{old→new}(R)", () => { + const res = execRA("ρ_{name→fullName}(Person)"); + expect(res[0].columns).toContain("fullName"); + expect(res[0].columns).not.toContain("name"); + }); + + it("σ{cond}(R) without underscore", () => { + const res = execRA("σ{age > 20}(Person)"); + expect(res[0].values.length).toBe(2); + }); + + it("⋈{cond} theta join with curly braces", () => { + const res = execRA("Person ⋈{age > credits} Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + // ── Selection edge cases ── + + it("σ with OR condition", () => { + const res = execRA("σ[city = 'Stockholm' or city = 'York'](Person)"); + expect(res[0].values.length).toBe(2); // Alice and Bob + }); + + it("σ with NOT condition", () => { + const res = execRA("σ[not age > 20](Person)"); + expect(res[0].values.length).toBe(1); // Only Bob (19) + expect(res[0].values[0]).toContain("Bob"); + }); + + it("σ with nested OR and AND", () => { + const res = execRA("σ[age > 20 or (name = 'Bob' and city = 'York')](Person)"); + expect(res[0].values.length).toBe(3); // Alice, Bob, Carol + }); + + it("σ with all comparison operators", () => { + expect(execRA("σ[age = 25](Person)")[0].values.length).toBe(1); + expect(execRA("σ[age <> 25](Person)")[0].values.length).toBe(2); + expect(execRA("σ[age != 25](Person)")[0].values.length).toBe(2); + expect(execRA("σ[age < 25](Person)")[0].values.length).toBe(1); + expect(execRA("σ[age > 25](Person)")[0].values.length).toBe(1); + expect(execRA("σ[age <= 25](Person)")[0].values.length).toBe(2); + expect(execRA("σ[age >= 25](Person)")[0].values.length).toBe(2); + }); + + it("σ with table.column references", () => { + const res = execRA("σ[Person.age > 20](Person)"); + expect(res[0].values.length).toBe(2); + }); + + // ── Rename edge cases ── + + it("ρ with multiple rename mappings", () => { + const res = execRA("ρ[name→fullName, age→years](Person)"); + expect(res[0].columns).toContain("fullName"); + expect(res[0].columns).toContain("years"); + expect(res[0].columns).not.toContain("name"); + expect(res[0].columns).not.toContain("age"); + expect(res[0].values.length).toBe(3); + }); + + it("ρ with ASCII arrow", () => { + const res = execRA("rho[name->fullName](Person)"); + expect(res[0].columns).toContain("fullName"); + }); + + // ── Outer joins ── + + it("rightjoin", () => { + const res = execRA("Student rightjoin[age > hasDisability] Person"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + it("fulljoin", () => { + const res = execRA("Person fulljoin[age > credits] Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + it("⟕ left outer join with Unicode", () => { + const res = execRA("Person ⟕[age > credits] Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + it("left join preserves non-matching rows", () => { + // Carol (age 30) has no Student row — left join should keep her with NULLs + const res = execRA("Person leftjoin[Person.id = Student.id] Student"); + expect(res[0].values.length).toBe(3); // All 3 Person rows + }); + + // ── Semi-join edge cases ── + + it("⋊ right semi-join", () => { + const res = execRA("Student ⋊ Person"); + // Student ids 1,2 both exist in Person — all Student rows match + expect(res[0].values.length).toBe(2); + }); + + it("rightsemijoin keyword", () => { + const res = execRA("Student rightsemijoin Person"); + expect(res[0].values.length).toBe(2); + }); + + it("leftsemijoin keyword", () => { + const res = execRA("Person leftsemijoin Student"); + expect(res[0].values.length).toBe(2); + }); + + it("antijoin keyword", () => { + const res = execRA("Person antijoin Student"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Carol"); + }); + + // ── Sort edge cases ── + + it("τ with multiple sort columns", () => { + const res = execRA("τ[city, age DESC](Person)"); + // Bristol(30), Stockholm(25), York(19) + expect(res[0].values[0]).toContain("Carol"); // Bristol + expect(res[0].values[1]).toContain("Alice"); // Stockholm + expect(res[0].values[2]).toContain("Bob"); // York + }); + + it("sort keyword", () => { + const res = execRA("sort[name](Person)"); + expect(res[0].values[0]).toContain("Alice"); + expect(res[0].values[2]).toContain("Carol"); + }); + + // ── Aggregation edge cases ── + + it("γ with SUM", () => { + const res = execRA("γ[city; SUM(age) AS totalAge](Person)"); + expect(res[0].columns).toContain("totalAge"); + expect(res[0].values.length).toBe(3); + }); + + it("γ with AVG", () => { + const res = execRA("γ[city; AVG(age) AS avgAge](Person)"); + expect(res[0].columns).toContain("avgAge"); + expect(res[0].values.length).toBe(3); + }); + + it("γ with MIN and MAX", () => { + const res = execRA("γ[city; MIN(age) AS youngest, MAX(age) AS oldest](Person)"); + expect(res[0].columns).toContain("youngest"); + expect(res[0].columns).toContain("oldest"); + }); + + it("γ COUNT without alias", () => { + const res = execRA("γ[city; COUNT(id)](Person)"); + expect(res[0].values.length).toBe(3); + }); + + it("gamma keyword", () => { + const res = execRA("gamma[city; COUNT(id) AS cnt](Person)"); + expect(res[0].columns).toContain("cnt"); + }); + + // ── Distinct edge cases ── + + it("delta keyword", () => { + const res = execRA("delta(Person)"); + expect(res[0].values.length).toBe(3); + }); + + it("distinct keyword", () => { + const res = execRA("distinct(Person)"); + expect(res[0].values.length).toBe(3); + }); + + it("δ without parens", () => { + const res = execRA("δ Person"); + expect(res[0].values.length).toBe(3); + }); + + // ── Keyword variants for operators ── + + it("select keyword", () => { + const res = execRA("select[age > 20](Person)"); + expect(res[0].values.length).toBe(2); + }); + + it("project keyword", () => { + const res = execRA("project[name, city](Person)"); + expect(res[0].columns).toEqual(["name", "city"]); + }); + + it("cross keyword", () => { + const res = execRA("Person cross Course"); + expect(res[0].values.length).toBe(6); + }); + + it("join keyword with condition", () => { + const res = execRA("Person join[age > credits] Course"); + expect(res[0].values.length).toBeGreaterThan(0); + }); + + it("|X| as natural join", () => { + const res = execRA("Person |X| Student"); + expect(res[0].values.length).toBe(2); + }); + + it("|><| as natural join", () => { + const res = execRA("Person |><| Student"); + expect(res[0].values.length).toBe(2); + }); + + it("intersect keyword", () => { + const res = execRA("π[name](Person) intersect π[name](Teacher)"); + expect(res.length === 0 || res[0].values.length === 0).toBe(true); + }); + + it("divide keyword", () => { + const res = execRA("π[id, course_id](Enrollment) divide π[course_id](Course)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain(1); + }); + + // ── Implicit subscripts edge cases ── + + it("ρ old→new (R) implicit", () => { + const res = execRA("ρ name→fullName (Person)"); + expect(res[0].columns).toContain("fullName"); + }); + + it("τ col (R) implicit", () => { + const res = execRA("τ name (Person)"); + expect(res[0].values[0]).toContain("Alice"); + }); + + it("σ compound implicit with AND", () => { + const res = execRA("σ age > 20 and city = 'Stockholm' (Person)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); + + it("nested implicit subscripts", () => { + const res = execRA("π name (σ age > 20 (Person))"); + expect(res[0].columns).toEqual(["name"]); + expect(res[0].values.length).toBe(2); + }); + + // ── Parenthesis-free edge cases ── + + it("triple chain without parens", () => { + const res = execRA("π[name] σ[age > 20] δ Person"); + expect(res[0].columns).toEqual(["name"]); + expect(res[0].values.length).toBe(2); + }); + + it("π[cols] over union with parens", () => { + const res = execRA("π[name] (π[name](Person) ∪ π[name](Teacher))"); + expect(res[0].values.length).toBe(5); + }); + + // ── Assignment edge cases ── + + it("assignment with <- ASCII arrow", () => { + const res = execRA("A <- σ[age > 20](Person)\nπ[name](A)"); + expect(res[0].values.length).toBe(2); + }); + + it("semicolons as statement separators", () => { + const res = execRA("A ← Person; π[name](A)"); + expect(res[0].values.length).toBe(3); + }); + + it("comments in multi-line input", () => { + const res = execRA("-- Get adults\nA ← σ[age > 20](Person)\n-- Project names\nπ[name](A)"); + expect(res[0].values.length).toBe(2); + }); + + it("implicit return from last assignment", () => { + const res = execRA("A ← σ[age > 20](Person)"); + expect(res[0].values.length).toBe(2); + }); + + it("complex pipeline with reassignment", () => { + const res = execRA("X ← Person ⋈ Student\nX ← σ[age > 20](X)\nX ← π[name](X)"); + expect(res[0].values.length).toBe(1); + expect(res[0].values[0]).toContain("Alice"); + }); +}); diff --git a/src/ra-engine/relationalAlgebra.ts b/src/ra-engine/relationalAlgebra.ts index 5077118..b1e8514 100644 --- a/src/ra-engine/relationalAlgebra.ts +++ b/src/ra-engine/relationalAlgebra.ts @@ -1001,6 +1001,11 @@ function columnRefToSQL(ref: ColumnRef): string { let subqueryCounter = 0; +/** Return a SQL alias for a node: use the table name for bare tables, otherwise _raN */ +function aliasFor(node: RANode): string { + return node.type === "table" ? node.name : `_ra${subqueryCounter++}`; +} + /** * Interface for a minimal database handle used to resolve column names. * Compatible with sql.js Database. @@ -1048,6 +1053,32 @@ function resolveColumns(sql: string, db: DatabaseHandle): string[] { throw new RAError(msg); } } +/** Ensure a node produces a full SELECT statement (needed for UNION/INTERSECT/EXCEPT) */ +function asSelect(node: RANode, db?: DatabaseHandle): string { + const sql = nodeToSQL(node, db); + // Bare table names need wrapping; anything starting with SELECT is already a query + return sql.trimStart().toUpperCase().startsWith("SELECT") ? sql : `SELECT * FROM ${sql}`; +} + +/** + * Build a WHERE clause correlating two aliases on their common columns. + * Returns empty string if no db or no common columns found. + */ +function buildCorrelation(leftSQL: string, rightSQL: string, lAlias: string, rAlias: string, db?: DatabaseHandle): string { + if (!db) return ""; + try { + const leftCols = resolveColumns(leftSQL, db); + const rightCols = resolveColumns(rightSQL, db); + const common = leftCols.filter(c => rightCols.includes(c)); + if (common.length > 0) { + return " WHERE " + common.map(c => `${lAlias}.${c} = ${rAlias}.${c}`).join(" AND "); + } + } catch { + // If we can't resolve columns, fall back to uncorrelated + } + return ""; +} + function nodeToSQL(node: RANode, db?: DatabaseHandle): string { switch (node.type) { case "table": @@ -1090,7 +1121,7 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { return `SELECT DISTINCT * FROM (${nodeToSQL(node.relation, db)})`; case "crossProduct": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} CROSS JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++}`; + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} CROSS JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)}`; case "naturalJoin": { const leftSQL = nodeToSQL(node.left, db); @@ -1109,56 +1140,104 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { } } } - return `SELECT * FROM (${leftSQL}) AS _ra${subqueryCounter++} NATURAL JOIN (${rightSQL}) AS _ra${subqueryCounter++}`; + return `SELECT * FROM (${leftSQL}) AS ${aliasFor(node.left)} NATURAL JOIN (${rightSQL}) AS ${aliasFor(node.right)}`; } case "thetaJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "leftJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} LEFT JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} LEFT JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "rightJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} RIGHT JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} RIGHT JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "fullJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS _ra${subqueryCounter++} FULL OUTER JOIN (${nodeToSQL(node.right, db)}) AS _ra${subqueryCounter++} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} FULL OUTER JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "leftSemiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - return `SELECT ${lAlias}.* FROM (${nodeToSQL(node.left, db)}) AS ${lAlias} WHERE EXISTS (SELECT 1 FROM (${nodeToSQL(node.right, db)}) AS ${rAlias})`; + const leftSQL = nodeToSQL(node.left, db); + const rightSQL = nodeToSQL(node.right, db); + const corr = buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); + return `SELECT ${lAlias}.* FROM (${leftSQL}) AS ${lAlias} WHERE EXISTS (SELECT 1 FROM (${rightSQL}) AS ${rAlias}${corr})`; } case "rightSemiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - return `SELECT ${rAlias}.* FROM (${nodeToSQL(node.right, db)}) AS ${rAlias} WHERE EXISTS (SELECT 1 FROM (${nodeToSQL(node.left, db)}) AS ${lAlias})`; + const leftSQL = nodeToSQL(node.left, db); + const rightSQL = nodeToSQL(node.right, db); + // Outer relation is right; EXISTS checks left — correlate right (outer) with left (inner) + const corr = buildCorrelation(rightSQL, leftSQL, rAlias, lAlias, db); + return `SELECT ${rAlias}.* FROM (${rightSQL}) AS ${rAlias} WHERE EXISTS (SELECT 1 FROM (${leftSQL}) AS ${lAlias}${corr})`; } case "antiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - return `SELECT ${lAlias}.* FROM (${nodeToSQL(node.left, db)}) AS ${lAlias} WHERE NOT EXISTS (SELECT 1 FROM (${nodeToSQL(node.right, db)}) AS ${rAlias})`; + const leftSQL = nodeToSQL(node.left, db); + const rightSQL = nodeToSQL(node.right, db); + const corr = buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); + return `SELECT ${lAlias}.* FROM (${leftSQL}) AS ${lAlias} WHERE NOT EXISTS (SELECT 1 FROM (${rightSQL}) AS ${rAlias}${corr})`; } case "union": - return `${nodeToSQL(node.left, db)} UNION ${nodeToSQL(node.right, db)}`; - case "intersect": - return `${nodeToSQL(node.left, db)} INTERSECT ${nodeToSQL(node.right, db)}`; - - case "difference": - return `${nodeToSQL(node.left, db)} EXCEPT ${nodeToSQL(node.right, db)}`; + case "difference": { + const leftSQL = asSelect(node.left, db); + const rightSQL = asSelect(node.right, db); + if (db) { + const leftCols = resolveColumns(leftSQL, db); + const rightCols = resolveColumns(rightSQL, db); + if (leftCols.length > 0 && rightCols.length > 0 && leftCols.length !== rightCols.length) { + const opName = node.type === "union" ? "Union (∪)" : node.type === "intersect" ? "Intersect (∩)" : "Difference (−)"; + throw new RAError( + `${opName} requires both sides to have the same number of columns. ` + + `Left has ${leftCols.length} column(s): [${leftCols.join(", ")}], ` + + `right has ${rightCols.length} column(s): [${rightCols.join(", ")}].` + ); + } + } + const sqlOp = node.type === "union" ? "UNION" : node.type === "intersect" ? "INTERSECT" : "EXCEPT"; + return `${leftSQL} ${sqlOp} ${rightSQL}`; + } case "division": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - const crossAlias = `_ra${subqueryCounter++}`; const innerAlias = `_ra${subqueryCounter++}`; const leftSQL = nodeToSQL(node.left, db); const rightSQL = nodeToSQL(node.right, db); - return `SELECT * FROM (SELECT DISTINCT * FROM (${leftSQL}) AS ${lAlias}) AS ${innerAlias} WHERE NOT EXISTS (SELECT * FROM (${rightSQL}) AS ${rAlias} WHERE NOT EXISTS (SELECT * FROM (${leftSQL}) AS ${crossAlias}))`; + + if (db) { + try { + const leftCols = resolveColumns(leftSQL, db); + const rightCols = resolveColumns(rightSQL, db); + const aOnlyCols = leftCols.filter(c => !rightCols.includes(c)); + const bCols = rightCols; + + if (aOnlyCols.length === 0) { + throw new RAError( + "Division requires the dividend to have columns not present in the divisor. " + + `Left columns: [${leftCols.join(", ")}], Right columns: [${rightCols.join(", ")}].` + ); + } + + const aOnlyMatch = aOnlyCols.map(c => `${innerAlias}.${c} = ${lAlias}.${c}`).join(" AND "); + const bMatch = bCols.map(c => `${innerAlias}.${c} = ${rAlias}.${c}`).join(" AND "); + + const selectCols = aOnlyCols.map(c => `${lAlias}.${c}`).join(", "); + return `SELECT DISTINCT ${selectCols} FROM (${leftSQL}) AS ${lAlias} WHERE NOT EXISTS (SELECT 1 FROM (${rightSQL}) AS ${rAlias} WHERE NOT EXISTS (SELECT 1 FROM (${leftSQL}) AS ${innerAlias} WHERE ${aOnlyMatch} AND ${bMatch}))`; + } catch (e) { + if (e instanceof RAError) throw e; + // Fall through to no-db version + } + } + + // Without db: best-effort uncorrelated version + return `SELECT * FROM (SELECT DISTINCT * FROM (${leftSQL}) AS ${lAlias}) AS ${innerAlias} WHERE NOT EXISTS (SELECT * FROM (${rightSQL}) AS ${rAlias} WHERE NOT EXISTS (SELECT * FROM (${leftSQL}) AS _ra${subqueryCounter++}))`; } } } From e514b304e60bb25a723dff25420bed62c9dbe310 Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:08:44 +0100 Subject: [PATCH 06/13] Fix editor highlighter, preview rendering, dark mode, and export readability --- src/App.css | 21 ++- src/App.tsx | 6 +- src/ExportRenderer.tsx | 4 +- src/ra-engine/RAPreview.tsx | 112 +++++++++----- src/ra-engine/raHighlight.test.ts | 174 ++++++++++++++++----- src/ra-engine/raHighlight.ts | 245 +++++++++++++----------------- src/ra-engine/raShared.ts | 59 ++++++- 7 files changed, 380 insertions(+), 241 deletions(-) diff --git a/src/App.css b/src/App.css index 50a362b..f69272f 100644 --- a/src/App.css +++ b/src/App.css @@ -46,17 +46,22 @@ body { background-color: rgba(34, 197, 94, 0.15); } -/* RA Preview styling */ -.ra-prev-op { color: #7c3aed; font-weight: bold; } -.ra-prev-assign { color: #7c3aed; font-weight: bold; } -.ra-prev-logic { color: #2563eb; font-weight: bold; } -.ra-prev-str { color: #059669; } -.ra-prev-num { color: #d97706; } -.ra-prev-comment { color: #9ca3af; font-style: italic; } -.ra-prev-sub { font-size: 0.8em; color: #6d28d9; } +/* RA Preview styling — light mode */ +.ra-prev-op { color: #a78bfa; font-weight: bold; } +.ra-prev-assign { color: #a78bfa; font-weight: bold; } +.ra-prev-logic { color: #1d4ed8; font-weight: bold; } +.ra-prev-str { color: #047857; } +.ra-prev-num { color: #b45309; } +.ra-prev-comment { color: #6b7280; font-style: italic; } +.ra-prev-sub { font-size: 0.9em; color: #4c1d95; } +.ra-prev-paren { color: #9ca3af; } + +/* RA Preview styling — dark mode */ .dark .ra-prev-op { color: #a78bfa; } .dark .ra-prev-assign { color: #a78bfa; } .dark .ra-prev-logic { color: #60a5fa; } .dark .ra-prev-str { color: #34d399; } .dark .ra-prev-num { color: #fbbf24; } +.dark .ra-prev-comment { color: #9ca3af; } .dark .ra-prev-sub { color: #c4b5fd; } +.dark .ra-prev-paren { color: #6b7280; } diff --git a/src/App.tsx b/src/App.tsx index 83c3554..e3446dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -850,7 +850,7 @@ function App() { height: exportRenderer.scrollHeight, pixelRatio: 1 }).then((dataUrl) => { - triggerDownload(dataUrl, `validator_${question.id}_${question.category.display_number}${question.display_sequence}.png`); + triggerDownload(dataUrl, `validator_${editorMode === "ra" ? "ra_" : ""}${question.id}_${question.category.display_number}${question.display_sequence}.png`); setExportQuestion(undefined); setExportQuery(undefined); setExportingStatus(0); @@ -939,7 +939,7 @@ function App() { value={editorMode === "ra" ? t("raPlaceholder") : t("selectQuestionComment")} disabled={true} onValueChange={_code => null} - highlight={code => editorMode === "ra" ? highlightRA(code) : highlight(code, languages.sql)} + highlight={code => editorMode === "ra" ? highlightRA(code, isDarkMode()) : highlight(code, languages.sql)} padding={10} tabSize={2} className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 min-h-32 rounded-md" @@ -951,7 +951,7 @@ function App() { itemID="editor" value={query} onValueChange={code => setQuery(code)} - highlight={code => editorMode === "ra" ? highlightRA(code) : highlight(code, languages.sql)} + highlight={code => editorMode === "ra" ? highlightRA(code, isDarkMode()) : highlight(code, languages.sql)} padding={10} tabSize={2} className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 border dark:border-slate-600 border-gray-300 min-h-32 rounded-md" diff --git a/src/ExportRenderer.tsx b/src/ExportRenderer.tsx index f80ba19..3dde3f2 100644 --- a/src/ExportRenderer.tsx +++ b/src/ExportRenderer.tsx @@ -59,8 +59,8 @@ const ExportRenderer = React.forwardRef(({ } {query?.mode === "ra" ? (
) : ( tags * - Assignment arrows rendered as ← + * + * Two modes: + * - CSS classes (default): uses .ra-prev-* classes from App.css, supports dark: variants + * - Inline styles (forExport=true): hardcoded light colors for PNG export on white background */ -const previewWrap = (type: string, text: string) => - `${text}`; +// Inline styles for export only (always light, white background) +const EXPORT_STYLES = { + op: "color: #7c3aed; font-weight: bold;", + logic: "color: #1d4ed8; font-weight: bold;", + str: "color: #047857;", + num: "color: #b45309;", + comment: "color: #6b7280; font-style: italic;", + assign: "color: #7c3aed; font-weight: bold;", + sub: "font-size: 0.9em; color: #1f2937;", + paren: "color: #9ca3af;", +}; + +type TagFn = (token: string, content: string) => string; +type SubFn = (content: string) => string; + +function cssTag(token: string, content: string): string { + return `${content}`; +} + +function cssSub(content: string): string { + return `${content}`; +} + +function makeExportTag(): TagFn { + return (token: string, content: string) => { + const style = EXPORT_STYLES[token as keyof typeof EXPORT_STYLES] || ""; + return style ? `${content}` : content; + }; +} + +function makeExportSub(): SubFn { + return (content: string) => `${content}`; +} -function renderSubscriptHtml(content: string): string { - return `${highlightSubContent(content, previewWrap)}`; +function makeSubWrap(tag: TagFn) { + return (type: string, text: string) => tag(type, text); +} + +function renderSubscriptHtml(content: string, tag: TagFn, sub: SubFn): string { + return sub(highlightSubContent(content, makeSubWrap(tag))); } /** Consume a subscript and return [newIndex, html] */ -function consumeSubscript(code: string, i: number): [number, string] { +function consumeSubscript(code: string, i: number, tag: TagFn, sub: SubFn): [number, string] { const beforeWs = i; const wsI = skipWs(code, i); - // Try bracket content const [bracketI, bracketContent] = extractBracketContent(code, i); if (bracketContent !== null) { - return [bracketI, renderSubscriptHtml(bracketContent)]; + return [bracketI, renderSubscriptHtml(bracketContent, tag, sub)]; } - // Try implicit subscript const [implI, implContent] = extractImplicitSubscript(code, wsI, beforeWs); if (implContent !== null) { - return [implI, renderSubscriptHtml(implContent)]; + return [implI, renderSubscriptHtml(implContent, tag, sub)]; } return [beforeWs, ""]; } -export function renderRAPreview(code: string): string { +export function renderRAPreview(code: string, forExport = false): string { + const tag: TagFn = forExport ? makeExportTag() : cssTag; + const sub: SubFn = forExport ? makeExportSub() : cssSub; const result: string[] = []; let i = 0; while (i < code.length) { - // Comments if (code[i] === "-" && i + 1 < code.length && code[i + 1] === "-") { let comment = ""; while (i < code.length && code[i] !== "\n") { comment += code[i]; i++; } - result.push(`${esc(comment)}`); + result.push(tag("comment", esc(comment))); continue; } @@ -68,109 +106,105 @@ export function renderRAPreview(code: string): string { continue; } - // Unicode unary operators if (UNARY_SYMBOLS.has(code[i])) { - result.push(`${esc(code[i])}`); + result.push(tag("op", esc(code[i]))); i++; - const [newI, html] = consumeSubscript(code, i); + const [newI, html] = consumeSubscript(code, i, tag, sub); i = newI; result.push(html); continue; } - // |X| or |><| natural join if (code[i] === "|") { if (i + 2 < code.length && (code[i + 1] === "X" || code[i + 1] === "x") && code[i + 2] === "|") { - result.push(""); + result.push(tag("op", "⋈")); i += 3; continue; } if (i + 3 < code.length && code[i + 1] === ">" && code[i + 2] === "<" && code[i + 3] === "|") { - result.push(""); + result.push(tag("op", "⋈")); i += 4; continue; } } - // Binary symbols if (BINARY_SYMBOLS.has(code[i])) { - result.push(`${esc(code[i])}`); + result.push(tag("op", esc(code[i]))); i++; if (i < code.length && (code[i] === "[" || code[i] === "{" || code[i] === "_")) { - const [newI, html] = consumeSubscript(code, i); + const [newI, html] = consumeSubscript(code, i, tag, sub); i = newI; result.push(html); } continue; } - // <- assignment if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { - result.push(""); + result.push(tag("assign", "←")); i += 2; continue; } - // -> rename if (code[i] === "-" && i + 1 < code.length && code[i + 1] === ">") { - result.push(""); + result.push(tag("op", "→")); i += 2; continue; } - // String literals if (code[i] === "'") { let str = "'"; i++; while (i < code.length && code[i] !== "'") { str += code[i]; i++; } if (i < code.length) { str += "'"; i++; } - result.push(`${esc(str)}`); + result.push(tag("str", esc(str))); continue; } - // Numbers if (/\d/.test(code[i])) { let num = ""; while (i < code.length && /[\d.]/.test(code[i])) { num += code[i]; i++; } - result.push(`${esc(num)}`); + result.push(tag("num", esc(num))); continue; } - // Identifiers and keywords if (IDENT_START.test(code[i])) { let ident = ""; while (i < code.length && IDENT_CHAR.test(code[i])) { ident += code[i]; i++; } const lower = ident.toLowerCase(); if (UNARY_KEYWORD_SYMBOLS[lower]) { - result.push(`${UNARY_KEYWORD_SYMBOLS[lower]}`); - const [newI, html] = consumeSubscript(code, i); + result.push(tag("op", UNARY_KEYWORD_SYMBOLS[lower])); + const [newI, html] = consumeSubscript(code, i, tag, sub); i = newI; result.push(html); } else if (BINARY_KEYWORD_SYMBOLS[lower]) { - result.push(`${BINARY_KEYWORD_SYMBOLS[lower]}`); + result.push(tag("op", BINARY_KEYWORD_SYMBOLS[lower])); const j = skipWs(code, i); if (j < code.length && (code[j] === "[" || code[j] === "{" || code[j] === "_")) { - const [newI, html] = consumeSubscript(code, j); + const [newI, html] = consumeSubscript(code, j, tag, sub); i = newI; result.push(html); } } else if (LOGIC_KEYWORDS.has(lower)) { - result.push(`${esc(ident)}`); + result.push(tag("logic", esc(ident))); } else { result.push(esc(ident)); } continue; } - // Comparison operators if ("<>!=".includes(code[i])) { let op = code[i]; i++; if (i < code.length && (code[i] === "=" || code[i] === ">")) { op += code[i]; i++; } - result.push(`${esc(op)}`); + result.push(tag("logic", esc(op))); + continue; + } + + if (code[i] === "(" || code[i] === ")") { + result.push(tag("paren", esc(code[i]))); + i++; continue; } - // Everything else result.push(esc(code[i])); i++; } @@ -196,7 +230,7 @@ export default function RAPreview({ code }: RAPreviewProps) { return (
diff --git a/src/ra-engine/raHighlight.test.ts b/src/ra-engine/raHighlight.test.ts index 5585296..b8e0c5a 100644 --- a/src/ra-engine/raHighlight.test.ts +++ b/src/ra-engine/raHighlight.test.ts @@ -1,82 +1,174 @@ import { describe, it, expect } from "vitest"; import { highlightRA } from "./raHighlight"; +import { renderRAPreview } from "./RAPreview"; -describe("RA highlighter", () => { - it("should highlight σ operator in purple", () => { - const html = highlightRA("σ[age > 20](Person)"); - expect(html).toContain("color: #7c3aed"); // operator color - expect(html).toContain("σ"); +/** Strip HTML tags and decode entities to recover visible text */ +function visibleText(html: string): string { + return html + .replace(/<[^>]+>/g, "") + .replace(/</g, "<").replace(/>/g, ">") + .replace(/&/g, "&").replace(/"/g, '"'); +} + +// ─── Character count invariant ────────────────────────────────────────────── +// The editor overlay MUST produce visible text identical to the input. +// These tests are the most important — if any fail, the editor is broken. + +describe("RA highlighter character preservation", () => { + const inputs = [ + "Person", + "σ[age > 20](Person)", + "π[name, city](Person)", + "ρ[name→fullName](Person)", + "σ_{age > 20}(Person)", + "σ{age > 20}(Person)", + "PI [name, city](Person)", + "PI [name, person_id, address, postal_code] SIGMA [city='York'] Person", + "PI name, person_id SIGMA city='York' Person", + "PI _name, person_id Person", + "A <- σ[age > 20](Person)\nπ[name](A)", + "-- comment\nPerson", + "Person ⋈ Student", + "Person |X| Student", + "Person |><| Student", + "ρ[name->fullName](Person)", + "σ[a <> 1](T)", + "σ[a != 1](T)", + "σ[a >= 1](T)", + "σ[a <= 1](T)", + "γ[city; COUNT(id) AS cnt](Person)", + "Person ⋈[Person.id = Student.id] Student", + "σ[name = 'Alice'](Person)", + "PI [name SIGMA [age > 20] Person", // unclosed bracket + "σ age > 20 (Person)", // implicit subscript + "", + ]; + + for (const input of inputs) { + it(`preserves: ${JSON.stringify(input).slice(0, 60)}`, () => { + if (input === "") return; // empty input = empty output + const html = highlightRA(input); + expect(visibleText(html)).toBe(input); + }); + } +}); + +// ─── Token coloring ───────────────────────────────────────────────────────── + +describe("RA highlighter coloring", () => { + it("colors σ operator", () => { + expect(highlightRA("σ[age > 20](Person)")).toContain("color: #7c3aed"); }); - it("should render brackets faintly", () => { - const html = highlightRA("σ[age > 20](Person)"); - expect(html).toContain("opacity: 0.5"); // faint brackets + it("colors keyword operators", () => { + const html = highlightRA("sigma[age > 20](Person)"); + expect(html).toContain("color: #7c3aed"); + expect(html).toContain("sigma"); + }); + + it("colors binary keyword operators", () => { + expect(highlightRA("A cross B")).toContain("color: #7c3aed"); }); - it("should render subscript content in light purple", () => { - const html = highlightRA("π[name](Person)"); - expect(html).toContain("color: #c084fc"); // subscript styling + it("renders brackets faintly", () => { + expect(highlightRA("σ[age > 20](Person)")).toContain("opacity: 0.5"); }); - it("should highlight string literals in green", () => { + it("colors string literals green", () => { const html = highlightRA("σ[name = 'Alice'](Person)"); - expect(html).toContain("color: #059669"); // string color + expect(html).toContain("color: #059669"); expect(html).toContain("Alice"); }); - it("should highlight numbers in amber", () => { - const html = highlightRA("σ[age > 20](Person)"); - expect(html).toContain("color: #d97706"); // number color + it("colors numbers amber", () => { + expect(highlightRA("σ[age > 20](Person)")).toContain("color: #d97706"); }); - it("should highlight AND/OR in blue", () => { + it("colors AND/OR/NOT blue", () => { const html = highlightRA("σ[a > 1 and b < 2](T)"); - expect(html).toContain("color: #2563eb"); // logic color - expect(html).toContain("and"); + expect(html).toContain("color: #2563eb"); }); - it("should highlight comments in gray italic", () => { + it("colors comments gray italic", () => { const html = highlightRA("-- this is a comment\nPerson"); expect(html).toContain("font-style: italic"); - expect(html).toContain("this is a comment"); }); - it("should render <- with assignment styling", () => { + it("colors <- assignment", () => { const html = highlightRA("A <- Person"); + expect(html).toContain("color: #7c3aed"); expect(html).toContain("<-"); - expect(html).toContain("font-weight: bold"); }); - it("should render -> with operator styling", () => { + it("colors -> rename arrow", () => { const html = highlightRA("ρ[name->fullName](Person)"); expect(html).toContain("->"); }); - it("should handle LaTeX-style _{} notation", () => { + it("colors _ as bracket when before {", () => { const html = highlightRA("σ_{age > 20}(Person)"); - expect(html).toContain("opacity: 0.5"); // faint brackets - expect(html).toContain("color: #c084fc"); // subscript content + expect(html).toContain("opacity: 0.5"); }); - it("should highlight keyword operators", () => { - const html = highlightRA("sigma[age > 20](Person)"); - expect(html).toContain("color: #7c3aed"); // operator color - expect(html).toContain("sigma"); + it("does not color _ when part of identifier", () => { + const html = highlightRA("PI _name (Person)"); + // _name should not have bracket styling + expect(html).not.toMatch(/opacity.*_n/); }); - it("should highlight binary keyword operators", () => { - const html = highlightRA("A cross B"); - expect(html).toContain("color: #7c3aed"); - expect(html).toContain("cross"); + it("preserves newlines", () => { + expect(highlightRA("A <- Person\nA")).toContain("\n"); + }); +}); + +// ─── RAPreview rendering ──────────────────────────────────────────────────── + +describe("RA preview", () => { + it("should render paren-free implicit projection without eating the table name", () => { + const html = renderRAPreview("PI person_id, address Person"); + expect(html).toMatch(/]*>.*person_id.*address.*<\/sub>/); + // Person must not be in a subscript + expect(html).not.toMatch(/]*>.*Person.*<\/sub>/s); + }); + + it("should render paren-free implicit projection with Unicode symbol", () => { + const html = renderRAPreview("π person_id, address Person"); + expect(html).toMatch(/]*>.*person_id.*<\/sub>/); + }); + + it("should still render parenthesised implicit subscript correctly", () => { + const html = renderRAPreview("π person_id, address (Person)"); + expect(html).toMatch(/]*>.*person_id.*address.*<\/sub>/); + }); + + it("should render paren-free σ condition Table correctly", () => { + const html = renderRAPreview("sigma age > 20 Person"); + expect(html).toContain("Person"); + expect(html).toMatch(/]*>.*age.*<\/sub>/); + }); + + it("should not swallow content after unclosed bracket", () => { + const html = renderRAPreview("PI [name, person_id SIGMA city='York' Person"); + const opMatches = html.match(/ra-prev-op/g); + expect(opMatches!.length).toBeGreaterThanOrEqual(2); + }); + + it("should render chained paren-free operators correctly", () => { + const html = renderRAPreview("PI name, person_id, address, postal_code SIGMA city='York' Person"); + const opMatches = html.match(/ra-prev-op/g); + expect(opMatches!.length).toBeGreaterThanOrEqual(2); + expect(html).toMatch(/]*>.*name.*postal_code.*<\/sub>/); }); - it("should preserve newlines", () => { - const html = highlightRA("A <- Person\nA"); - expect(html).toContain("\n"); + it("should render chained Unicode operators correctly", () => { + const html = renderRAPreview("π name, city σ age > 20 Person"); + const opMatches = html.match(/ra-prev-op/g); + expect(opMatches!.length).toBeGreaterThanOrEqual(2); }); - it("should handle implicit subscripts with whitespace", () => { - const html = highlightRA("σ age > 20 (Person)"); - expect(html).toContain("color: #c084fc"); // subscript styling for implicit content + it("should not treat underscore-prefixed column names as LaTeX subscript", () => { + const html = renderRAPreview("PI _name, person_id Person"); + expect(html).toMatch(/]*>.*_name.*person_id.*<\/sub>/); + expect(html).not.toMatch(/opacity/); }); }); diff --git a/src/ra-engine/raHighlight.ts b/src/ra-engine/raHighlight.ts index d22c76a..d448000 100644 --- a/src/ra-engine/raHighlight.ts +++ b/src/ra-engine/raHighlight.ts @@ -9,225 +9,188 @@ You may obtain a copy of the License in the LICENSE.md file in this repository. /** * Custom syntax highlighter for relational algebra expressions. - * Returns HTML with subscript rendering for bracket content and - * colored tokens for operators, keywords, strings, etc. * - * IMPORTANT: This highlighter must preserve character count (each input char - * maps to exactly one output char) for editor overlay alignment. + * INVARIANT: The visible text of the output must be EXACTLY the input string. + * Every input character must appear exactly once in the output. We only wrap + * characters in tags for coloring — never skip, reorder, or add chars. */ import { UNARY_SYMBOLS, BINARY_SYMBOLS, UNARY_KEYWORDS, BINARY_KEYWORDS, - LOGIC_KEYWORDS, IDENT_START, IDENT_CHAR, - esc, skipWs, highlightSubContent, extractBracketContent, extractImplicitSubscript, + LOGIC_KEYWORDS, IDENT_START, IDENT_CHAR, esc, } from "./raShared"; -// Inline styles for portability with the code editor -const S = { - op: "color: #7c3aed; font-weight: bold;", - kw: "color: #7c3aed; font-weight: bold;", - logic: "color: #2563eb; font-weight: bold;", +// Only color/opacity — never font-weight/size which affect character width +const LIGHT = { + op: "color: #7c3aed;", + logic: "color: #2563eb;", str: "color: #059669;", num: "color: #d97706;", comment: "color: #9ca3af; font-style: italic;", bracket: "color: #a1a1aa; opacity: 0.5;", - assign: "color: #7c3aed; font-weight: bold;", - sub: "color: #c084fc; font-weight: 500;", }; -const subWrap = (type: string, text: string) => - `${text}`; +const DARK = { + op: "color: #a78bfa;", + logic: "color: #60a5fa;", + str: "color: #34d399;", + num: "color: #fbbf24;", + comment: "color: #6b7280; font-style: italic;", + bracket: "color: #6b7280; opacity: 0.5;", +}; /** - * Highlight a relational algebra expression, rendering bracket content - * as subscripts and coloring operators/keywords. + * Wrap a raw substring in a styled span, escaping HTML entities. + * The visible text length is always === raw.length. */ -export function highlightRA(code: string): string { +function span(style: string, raw: string): string { + return `${esc(raw)}`; +} + +/** + * Highlight RA code for the editor overlay. + * Simple token-coloring only — no rewriting, no subscript rendering. + * Character count is guaranteed correct by construction. + */ +export function highlightRA(code: string, dark = false): string { + const S = dark ? DARK : LIGHT; const result: string[] = []; let i = 0; while (i < code.length) { - // Comments: -- + // ── Comments: -- until newline ── if (code[i] === "-" && i + 1 < code.length && code[i + 1] === "-") { - let comment = ""; - while (i < code.length && code[i] !== "\n") { - comment += code[i]; - i++; - } - result.push(`${esc(comment)}`); + let end = i; + while (end < code.length && code[end] !== "\n") end++; + result.push(span(S.comment, code.slice(i, end))); + i = end; continue; } - // Newlines — preserve them + // ── Newlines ── if (code[i] === "\n") { result.push("\n"); i++; continue; } - // Unicode RA operators (single char) + // ── Unicode unary operators (σ, π, ρ, γ, τ, δ) ── if (UNARY_SYMBOLS.has(code[i])) { - result.push(`${esc(code[i])}`); + result.push(span(S.op, code[i])); i++; - i = renderSubscript(code, i, result); continue; } - // |X| or |><| — natural join + // ── Unicode binary operators (×, ⋈, ∪, ∩, etc.) ── + if (BINARY_SYMBOLS.has(code[i])) { + result.push(span(S.op, code[i])); + i++; + continue; + } + + // ── |X| or |><| natural join ── if (code[i] === "|") { if (i + 2 < code.length && (code[i + 1] === "X" || code[i + 1] === "x") && code[i + 2] === "|") { - result.push(`${esc("|X|")}`); + result.push(span(S.op, code.slice(i, i + 3))); i += 3; continue; } if (i + 3 < code.length && code[i + 1] === ">" && code[i + 2] === "<" && code[i + 3] === "|") { - result.push(`${esc("|><|")}`); + result.push(span(S.op, code.slice(i, i + 4))); i += 4; continue; } } - if (BINARY_SYMBOLS.has(code[i])) { - result.push(`${esc(code[i])}`); - i++; - if (i < code.length && (code[i] === "[" || code[i] === "{" || code[i] === "_")) { - i = renderSubscript(code, i, result); - } - continue; - } - - // Arrow: <- (assignment) — keep both chars for editor alignment + // ── <- assignment ── if (code[i] === "<" && i + 1 < code.length && code[i + 1] === "-") { - result.push(`<-`); + result.push(span(S.op, "<-")); i += 2; continue; } - // Arrow: -> (rename) — keep both chars for editor alignment + // ── -> rename arrow ── if (code[i] === "-" && i + 1 < code.length && code[i + 1] === ">") { - result.push(`->`); + result.push(span(S.op, "->")); i += 2; continue; } - // String literals - if (code[i] === "'") { - let str = "'"; + // ── → Unicode rename arrow ── + if (code[i] === "→") { + result.push(span(S.op, "→")); i++; - while (i < code.length && code[i] !== "'") { - str += code[i]; - i++; - } - if (i < code.length) { str += "'"; i++; } - result.push(`${esc(str)}`); continue; } - // Numbers + // ── String literals ── + if (code[i] === "'") { + let end = i + 1; + while (end < code.length && code[end] !== "'") end++; + if (end < code.length) end++; // include closing quote + result.push(span(S.str, code.slice(i, end))); + i = end; + continue; + } + + // ── Numbers ── if (/\d/.test(code[i])) { - let num = ""; - while (i < code.length && /[\d.]/.test(code[i])) { - num += code[i]; - i++; - } - result.push(`${esc(num)}`); + let end = i; + while (end < code.length && /[\d.]/.test(code[end])) end++; + result.push(span(S.num, code.slice(i, end))); + i = end; continue; } - // Identifiers and keywords - if (IDENT_START.test(code[i])) { - let ident = ""; - while (i < code.length && IDENT_CHAR.test(code[i])) { - ident += code[i]; + // ── Brackets — render faintly ── + if (code[i] === "[" || code[i] === "]" || code[i] === "{" || code[i] === "}") { + result.push(span(S.bracket, code[i])); + i++; + continue; + } + + // ── Underscore before bracket — render as faint bracket prefix ── + if (code[i] === "_") { + // Check if this is a LaTeX-style _{} or _[] prefix + let j = i + 1; + while (j < code.length && code[j] === " ") j++; + if (j < code.length && (code[j] === "{" || code[j] === "[")) { + result.push(span(S.bracket, "_")); i++; + continue; } - const lower = ident.toLowerCase(); - if (UNARY_KEYWORDS.has(lower)) { - result.push(`${esc(ident)}`); - i = renderSubscript(code, i, result); - } else if (BINARY_KEYWORDS.has(lower)) { - result.push(`${esc(ident)}`); - const j = skipWs(code, i); - if (j < code.length && (code[j] === "[" || code[j] === "{" || code[j] === "_")) { - if (j > i) result.push(esc(code.slice(i, j))); - i = renderSubscript(code, j, result); - } + } + + // ── Identifiers and keywords ── + if (IDENT_START.test(code[i])) { + let end = i; + while (end < code.length && IDENT_CHAR.test(code[end])) end++; + const word = code.slice(i, end); + const lower = word.toLowerCase(); + if (UNARY_KEYWORDS.has(lower) || BINARY_KEYWORDS.has(lower)) { + result.push(span(S.op, word)); } else if (LOGIC_KEYWORDS.has(lower)) { - result.push(`${esc(ident)}`); + result.push(span(S.logic, word)); } else { - result.push(esc(ident)); + result.push(esc(word)); } + i = end; continue; } - // Comparison operators - if (code[i] === "<" || code[i] === ">" || code[i] === "!" || code[i] === "=") { - let op = code[i]; - i++; - if (i < code.length && (code[i] === "=" || code[i] === ">")) { - op += code[i]; - i++; - } - result.push(`${esc(op)}`); + // ── Comparison operators ── + if ("<>!=".includes(code[i])) { + let end = i + 1; + if (end < code.length && (code[end] === "=" || code[end] === ">")) end++; + result.push(span(S.logic, code.slice(i, end))); + i = end; continue; } - // Everything else (whitespace, parens, etc.) — pass through + // ── Everything else (spaces, parens, etc.) — pass through ── result.push(esc(code[i])); i++; } return result.join(""); } - -/** - * Render a subscript section after a unary/binary operator. - * Handles: _{ }, [ ], { }, or implicit (content until '('). - * Returns the new index position. - */ -function renderSubscript(code: string, i: number, result: string[]): number { - const beforeWs = i; - i = skipWs(code, i); - - if (i >= code.length) { - if (i > beforeWs) result.push(code.slice(beforeWs, i)); - return i; - } - - // Handle _{ or _[ (LaTeX-style) - if (code[i] === "_") { - result.push(`${esc("_")}`); - i++; - i = skipWs(code, i); - } - - // Try bracket content - if (code[i] === "[" || code[i] === "{") { - const openBracket = code[i]; - result.push(`${esc(openBracket)}`); - const [newI, content] = extractBracketContent(code, i); - if (content !== null) { - result.push(`${highlightSubContent(content, subWrap)}`); - // Render closing bracket (only if bracket was actually closed) - if (newI > i + 1 && newI <= code.length) { - const closeBracket = code[newI - 1]; - if (closeBracket === "]" || closeBracket === "}") { - result.push(`${esc(closeBracket)}`); - } - } - return newI; - } - } - - // Implicit subscript - const [newI, content] = extractImplicitSubscript(code, i, beforeWs); - if (content !== null) { - result.push(code.slice(beforeWs, i)); // whitespace - result.push(`${highlightSubContent(content, subWrap)}`); - return newI; - } - - // No subscript found — restore whitespace - if (i > beforeWs) result.push(code.slice(beforeWs, i)); - return i; -} diff --git a/src/ra-engine/raShared.ts b/src/ra-engine/raShared.ts index c8215eb..925c65f 100644 --- a/src/ra-engine/raShared.ts +++ b/src/ra-engine/raShared.ts @@ -152,10 +152,12 @@ export function extractBracketContent(code: string, startI: number): [number, st let i = skipWs(code, startI); if (i >= code.length) return [startI, null]; - // Handle _{ or _[ prefix + // Handle _{ or _[ prefix — only if followed by a bracket if (code[i] === "_") { - i++; - i = skipWs(code, i); + const afterUnderscore = skipWs(code, i + 1); + if (afterUnderscore < code.length && (code[afterUnderscore] === "{" || code[afterUnderscore] === "[")) { + i = afterUnderscore; + } } if (code[i] === "[" || code[i] === "{") { @@ -168,14 +170,23 @@ export function extractBracketContent(code: string, startI: number): [number, st if (depth > 0) content += code[i]; i++; } - return [i, content]; + // Only treat as bracket content if the bracket was actually closed + if (depth === 0) { + return [i, content]; + } + return [startI, null]; } return [startI, null]; } +/** All keywords that are RA operators (unary + binary) */ +const RA_OPERATOR_KEYWORDS = new Set([...UNARY_KEYWORDS, ...BINARY_KEYWORDS]); + /** - * Extract implicit subscript content (tokens until '(' or newline). + * Extract implicit subscript content (tokens until '(' or newline or another RA operator). + * When no '(' or operator follows (paren-free syntax like `π cols Table`), the last + * space-separated token is the operand, not part of the subscript. * Returns [newIndex, extractedContent] or [originalIndex, null] if nothing found. */ export function extractImplicitSubscript(code: string, startI: number, beforeWs: number): [number, string | null] { @@ -183,15 +194,49 @@ export function extractImplicitSubscript(code: string, startI: number, beforeWs: if (code[i] === "(" || i <= beforeWs) return [startI, null]; let content = ""; - while (i < code.length && code[i] !== "(" && code[i] !== "\n") { + let hitBoundary = false; // true if we stopped at '(' or an RA operator + while (i < code.length && code[i] !== "\n") { + if (code[i] === "(") { hitBoundary = true; break; } + + // Stop if we hit a unary/binary Unicode symbol (next operator) + if (UNARY_SYMBOLS.has(code[i]) || BINARY_SYMBOLS.has(code[i])) { hitBoundary = true; break; } + + // Stop if we hit an RA operator keyword (e.g., SIGMA, PI, cross, natjoin) + if (IDENT_START.test(code[i])) { + let word = ""; + let j = i; + while (j < code.length && IDENT_CHAR.test(code[j])) { word += code[j]; j++; } + if (RA_OPERATOR_KEYWORDS.has(word.toLowerCase())) { hitBoundary = true; break; } + content += word; + i = j; + continue; + } + content += code[i]; i++; } const trimmed = content.trimEnd(); const trimDiff = content.length - trimmed.length; if (trimDiff > 0) i -= trimDiff; + if (trimmed.length > 0) { - return [i, trimmed]; + // If we stopped at a clear boundary ('(' or another operator), the entire + // scanned content is the subscript. + if (hitBoundary) { + return [i, trimmed]; + } + // Otherwise we hit end-of-line / end-of-input — the last token is the + // operand (table name), not part of the subscript. + const lastSpace = trimmed.lastIndexOf(" "); + if (lastSpace > 0) { + const sub = trimmed.slice(0, lastSpace).trimEnd(); + if (sub.length > 0) { + i = startI + sub.length; + return [i, sub]; + } + } + // Single token with no parens — it's the operand, not a subscript + return [startI, null]; } return [startI, null]; } From ae1da67bea188125d2d8c1ac6a248efd75c8d3a3 Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:15:40 +0100 Subject: [PATCH 07/13] Add license attribution dialog with full dependency listing --- .gitignore | 4 ++ package.json | 3 +- scripts/generate-licenses.ts | 76 ++++++++++++++++++++++++ src/App.tsx | 3 +- src/LicenseDialog.tsx | 110 +++++++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 scripts/generate-licenses.ts create mode 100644 src/LicenseDialog.tsx diff --git a/.gitignore b/.gitignore index b455588..4e41fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # dependencies /node_modules + +# generated +/public/licenses.json /.pnp .pnp.js @@ -35,6 +38,7 @@ scripts/* !scripts/generate-language.ts !scripts/extract-oracle.ts !scripts/generate-erd.ts +!scripts/generate-licenses.ts !scripts/hooks/ data/* diff --git a/package.json b/package.json index 06ab4eb..8db643e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "scripts": { "dev": "npm-run-all copy-sqljs start-vite", "start": "npm-run-all copy-sqljs start-vite", - "build": "npm-run-all copy-sqljs build-vite", + "build": "npm-run-all copy-sqljs generate-licenses build-vite", "preview": "vite preview", "copy-sqljs": "copyfiles -f node_modules/sql.js/dist/sql-wasm.wasm public/dist/sql.js/", "start-vite": "vite", @@ -50,6 +50,7 @@ "decrypt-oracle": "tsx scripts/decrypt-oracle.ts", "encrypt-oracle": "tsx scripts/encrypt-oracle.ts", "generate-erd": "tsx scripts/generate-erd.ts", + "generate-licenses": "tsx scripts/generate-licenses.ts", "generate-all": "tsx scripts/generate-language.ts --all --plain && tsx scripts/generate-erd.ts --all" }, "eslintConfig": { diff --git a/scripts/generate-licenses.ts b/scripts/generate-licenses.ts new file mode 100644 index 0000000..c6a77bb --- /dev/null +++ b/scripts/generate-licenses.ts @@ -0,0 +1,76 @@ +/** + * Generates a JSON file with license information for all production dependencies. + * Output: public/licenses.json + */ + +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { join, dirname } from "path"; + +const ROOT = join(dirname(new URL(import.meta.url).pathname), ".."); +const NODE_MODULES = join(ROOT, "node_modules"); + +interface LicenseEntry { + name: string; + version: string; + license: string; + repository?: string; + author?: string; +} + +// Read the root package.json to get production dependencies +const rootPkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")); +const prodDeps = Object.keys(rootPkg.dependencies || {}); + +function resolvePackage(name: string): LicenseEntry | null { + const pkgDir = join(NODE_MODULES, name); + const pkgJsonPath = join(pkgDir, "package.json"); + if (!existsSync(pkgJsonPath)) return null; + + const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); + const license = typeof pkg.license === "string" + ? pkg.license + : typeof pkg.license === "object" + ? pkg.license.type + : Array.isArray(pkg.licenses) + ? pkg.licenses.map((l: { type: string }) => l.type).join(", ") + : "Unknown"; + + const repo = typeof pkg.repository === "string" + ? pkg.repository + : typeof pkg.repository === "object" + ? pkg.repository.url + : undefined; + + const cleanRepo = repo + ?.replace(/^git\+/, "") + ?.replace(/^git:\/\//, "https://") + ?.replace(/\.git$/, "") + ?.replace(/^ssh:\/\/git@github\.com/, "https://github.com"); + + const author = typeof pkg.author === "string" + ? pkg.author + : typeof pkg.author === "object" + ? pkg.author.name + : undefined; + + return { + name: pkg.name, + version: pkg.version, + license, + repository: cleanRepo, + author, + }; +} + +const entries: LicenseEntry[] = []; + +for (const dep of prodDeps) { + const entry = resolvePackage(dep); + if (entry) entries.push(entry); +} + +// Sort alphabetically +entries.sort((a, b) => a.name.localeCompare(b.name)); + +writeFileSync(join(ROOT, "public", "licenses.json"), JSON.stringify(entries, null, 2)); +console.log(`Generated licenses.json with ${entries.length} dependencies`); diff --git a/src/App.tsx b/src/App.tsx index e3446dd..0f38f39 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ import sha256 from "crypto-js/sha256"; import { format as formatFns } from "date-fns"; import { toPng } from "html-to-image"; import PrivacyNoticeToggle from "./PrivacyNoticeToggle"; +import LicenseDialog from "./LicenseDialog"; import ThemeToggle from "./ThemeToggle"; import useTheme from "./useTheme"; import { isCorrectResult, Result } from "./utils"; @@ -1132,7 +1133,7 @@ function App() {
Copyright © Edwin Sundberg {new Date().getFullYear()} - - GPL-3.0 + Report Issue diff --git a/src/LicenseDialog.tsx b/src/LicenseDialog.tsx new file mode 100644 index 0000000..fa7a707 --- /dev/null +++ b/src/LicenseDialog.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from "react"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { X } from "lucide-react"; + +interface LicenseEntry { + name: string; + version: string; + license: string; + repository?: string; + author?: string; +} + +const PROJECT_LICENSES = [ + { + name: "sql-validator (core)", + license: "GPL-3.0", + description: "The core application is licensed under the GNU General Public License v3.0.", + url: "https://github.com/Edwinexd/sql-validator?tab=GPL-3.0-1-ov-file", + }, + { + name: "ra-engine (Relational Algebra)", + license: "BSL-1.1", + description: "The relational algebra engine (src/ra-engine/) is licensed under the Business Source License 1.1, converting to GPL-3.0 on 2035-03-20.", + url: "https://github.com/Edwinexd/sql-validator/blob/master/src/ra-engine/LICENSE.md", + }, +]; + +export default function LicenseDialog() { + const [open, setOpen] = useState(false); + const [deps, setDeps] = useState([]); + + useEffect(() => { + if (open && deps.length === 0) { + fetch("/licenses.json") + .then(r => r.json()) + .then(setDeps) + .catch(() => setDeps([])); + } + }, [open, deps.length]); + + return ( + <> + + + +
+

Licenses

+ +
+ +
+
+

Project Licenses

+
+ {PROJECT_LICENSES.map(l => ( +
+
+ {l.name} + {l.license} +
+

{l.description}

+
+ ))} +
+
+ +
+

Third-Party Dependencies

+ {deps.length === 0 ? ( +

Loading...

+ ) : ( +
+ + + + + + + + + + {deps.map(dep => ( + + + + + + ))} + +
PackageVersionLicense
+ {dep.repository ? ( + {dep.name} + ) : dep.name} + {dep.version}{dep.license}
+
+ )} +
+
+
+
+ + ); +} From a629910c680ba4f3bee8955440ffe2ccacedd20c Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:40:10 +0100 Subject: [PATCH 08/13] Add dual SQLite/PostgreSQL engine support with CodeMirror editor and autocomplete --- data/oracle-types.ts | 11 + data/oracle.enc | Bin 75758 -> 78666 bytes languages/sv-pg.ts | 11 + languages/types.ts | 2 + package-lock.json | 244 +++++- package.json | 5 +- public/languages/en-pg/data.sql | 141 ++++ public/languages/en-pg/db_layout_dark.svg | 263 +++++++ public/languages/en-pg/db_layout_light.svg | 263 +++++++ public/languages/en-pg/questionpool.json | 1 + public/languages/en/questionpool.json | 2 +- public/languages/sv-pg/data.sql | 141 ++++ public/languages/sv-pg/db_layout_dark.svg | 263 +++++++ public/languages/sv-pg/db_layout_light.svg | 263 +++++++ public/languages/sv-pg/questionpool.json | 1 + public/languages/sv/questionpool.json | 2 +- scripts/generate-erd.ts | 331 +++++++- scripts/generate-language.ts | 331 ++++++-- src/App.tsx | 597 ++++++++------- src/DatabaseLayoutDialog.tsx | 6 +- src/EditorSettingsDialog.tsx | 68 ++ src/EngineSelector.tsx | 31 + src/ExportRenderer.tsx | 41 +- src/SqlEditor.tsx | 133 ++++ src/ViewsTable.tsx | 2 - src/database/pgliteEngine.ts | 101 +++ src/database/sqliteEngine.ts | 79 ++ src/database/types.ts | 23 + src/i18n/context.tsx | 80 +- src/i18n/languages.ts | 7 + src/ra-engine/raLanguage.ts | 146 ++++ src/ra-engine/relationalAlgebra.test.ts | 849 +++++++++++---------- src/ra-engine/relationalAlgebra.ts | 130 ++-- src/shims/empty.ts | 2 + src/useEditorSettings.ts | 39 + vite.config.ts | 9 + 36 files changed, 3724 insertions(+), 894 deletions(-) create mode 100644 languages/sv-pg.ts create mode 100644 public/languages/en-pg/data.sql create mode 100644 public/languages/en-pg/db_layout_dark.svg create mode 100644 public/languages/en-pg/db_layout_light.svg create mode 100644 public/languages/en-pg/questionpool.json create mode 100644 public/languages/sv-pg/data.sql create mode 100644 public/languages/sv-pg/db_layout_dark.svg create mode 100644 public/languages/sv-pg/db_layout_light.svg create mode 100644 public/languages/sv-pg/questionpool.json create mode 100644 src/EditorSettingsDialog.tsx create mode 100644 src/EngineSelector.tsx create mode 100644 src/SqlEditor.tsx create mode 100644 src/database/pgliteEngine.ts create mode 100644 src/database/sqliteEngine.ts create mode 100644 src/database/types.ts create mode 100644 src/ra-engine/raLanguage.ts create mode 100644 src/shims/empty.ts create mode 100644 src/useEditorSettings.ts diff --git a/data/oracle-types.ts b/data/oracle-types.ts index 5e702fd..37d929d 100644 --- a/data/oracle-types.ts +++ b/data/oracle-types.ts @@ -94,6 +94,17 @@ export interface OracleQuestion { * these alternatives are only used for validation. */ alternativeQueries?: string[]; + /** + * PostgreSQL-specific reference query, using the same placeholder format. + * Used instead of `query` when generating for a PostgreSQL-engine language. + * Only needed when the query differs from the SQLite version (e.g. EXTRACT vs YEAR). + */ + pgQuery?: string; + /** + * PostgreSQL-specific alternative queries. + * Falls back to `alternativeQueries` if not provided. + */ + pgAlternativeQueries?: string[]; } export interface OracleData { diff --git a/data/oracle.enc b/data/oracle.enc index f27117a983d858692259fd96c564239794f07bae..fb947f5cb660732d8c4eeb2fdf4b074402772e98 100644 GIT binary patch literal 78666 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 U@;?_N9BSjeiGb>@;0P%&;!U+5JOBUy literal 75758 zcmV(tKL==Mjs6nyi~(4OC<%6rc50$9p1 z!wDp9nHMVauCy)`!`Fi4Xy;UB8d{&X*Fd;#tbJ7@DoT3y<7aaNJsz-aRJbS+?zs%Bq9{s7Ckq>+kVeL-` z+!>C!c^9KF>U3|_&QDxU6ywAT!h=C&GPQ zzNCoZ*LXh6Pr3toE=aiJ9fZ*!$uN?j;4XFnlEbnq=4GpGaRoEp33>>&Gup-3z{ipo zf11@ILA~y{`jX;ZRRvvtyIaMgXxO`xyT7*a?oLZ07)nwor}!QQM&XY)PX98^(`<57 z+E_DhO(o-x`F#)()}7e)9Ul?AMhHGcD=JY_iY6c=eWh@W@{FXk12?a+;gk z05E_m%QTiG2n@BtnT=NjZdM=LLnOc=SUQZPklUPB)htWvUf z{Sy?Bw|W!VcH{%W+@PKPba9YSem|}}c)~id>?2UMW~sM!kt$+ogwawKR>fO?JEUXzx(r zy7#ffCJsK4LBc(J!KA8=w`%#-MSn7B!?@$eJER+bqph8oxkBxH zg7x^`4dE3r?+HwT6;oX669KbZ%`O5>PBU(GzGAcufDz0a6?!v(m^3(n{s^mZYN_aMIZ|=)$}2nZ zaU7RP4ie?nQ1x7C7vnSo6smb?uEFHvKBP=_vJbZkT6l)?s&Da!(>lFNg9%1@^{x(0 zjc3wi+F{vvNR)n=x(vJyXXf(zKG6s4TM)zihTz@_V;2)G~QbT z+k@6;PT%4*8WtcAU}GG>y0f$pRltT0z}HF}8XafE5`jWXu-no9K#m_zR!D5e?9Oa} z#T^j~I2tVVZHcSCz?f>w>vs=TiU*2IsU?avB#iLk>2>R(@rEdHrO~$Vhx{{tgBT$u zMwG&{H&Ih*w@)I;%<~BF`#fJ|NC7%}$%|j?*s4t}l;FN!?sxkjDFwO~^U#SP4f8(7 zTpxkLKKlcD-$GgrR8m9E(;Sm|n$6Kx!UBrkHkBtV2|+bHQ3IhNcEgTJmv((p4(wP* z1sM6WZ5@lVYFy*)D5ypIxGg&!LdAkx#mN~7Y;XK@TN)`63yr8ig*V+bAx%(@tP*okuY}pJ#JS|-)J;w% zG<(7q2CQ8qfKW~d!f6MFnk=Sa2XgU6X%b(B3Um7N<9P^J|Aa_e&6SeS_cZqursj== zmi;a=hG*)}i0p1oTj~X7fvu%dy;*@2K||!X+KM6q$d5!)B4{C}4%S2w8x; zgC?de=nr?&>>R2#RhK5)L%hPI99jP&$PfUQ;s;ngrrr}t>~;VMfEQ)b0teP4fTY$p zT3l9Lr`YdXG7s{{PGb3kT6ZdYxQnJeiW?&HU-ZG_R!eJ32cjN~8gfO@N`fu8GOod! zxff@Dgtj9o+a@a+aR?p-V|R_%x@Ew~C2j5>;t3qt1H^`B)B@p$U2Te)yvzQ#i1@Qy z{Z{b9Ns{!dwWY7@agc&ejn>Jk$hbBRkt+zsmerVQf+P&~&WV8M;&~`oA%NF2lk8;2 zsQ8SYyO^L`+ELWNGyV5;?-b* z?2eO<^5li4$9nZ>G1uL)L?t$ga+qL^2Y;KO+D%4f{`wEU7#FO#(m{Ripilc{9!Eq+ z+cA@^&K2}7R&D>ABU`8$!u@JPYG|L7dYDDmz(Ya=L#jG!2ch6C;Sq3b!NpTdzZ(fyUDNW z*O`0!Z{rH48`n}(WpbQPBS=*`vGLdwk-Pgig?&>38_#O?M8rivQ^J

#CnO1(1aa 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%0Dh>e%@Xx&`d_@U2g@LxgOBa&oTQcnE#3bN z@cXyj=laYxA9qY2G{JAr*o=_s z(K^C%&h(N7cpE)=`gfVfQIW!tB`tl&pW$s_-(KQyHU@FGfgdX|;3rnL>2E02HFa-_ z|BO-W9b;z8#(>MWyjvvOIpA%ajcH>o7Q5*Sd?ry_E!C*dYCcgUCDk+)0HY+LvT{k% zlqP3wasvVWf2gi4d_eoQ9L_nOZyR1aE{7;8hgh+FR>eg5Rd&GYkZF1hdT7mcZRl?0* z_-1nDXf_GDD=!J^TNWa&T<=awM#xy|dIb!(UNlEsi|l6cxF?t|dG!N}-^V&b>UY9* zjrlCjMHwc{X9Ec|xI^#tzROZU7LaitB{EVSCvL`z#Lw8B$ulxO%E4B|>v~m!HX0Yu zN92SuAKnVz4`V9mhxLINJw7WHbd^hpKxwdP-_Jnaq%{S*@oX*+s;i-jDbq5(EyyOF zOql?B@U4dPprjx1nj3a#IBbvbKI&;y2;il|53GSwk1>Ttjm29KPqb}90 zk%JScPu}dDh`+07Zrs6@aFL6?kWRM9!XgqN{N6F8AW34xf$G0ICi01@h-GU61Xe^r z&*`-wJ$5ny;s-NEHfVg{eaK958bFIbEG@;Gb>dl_^%9NH4q?V({+hW1r+e(yV81qH zacPGJ)-lc1{rO<&6D>H~T;nk{GkIJ@HFaP=tBBV#c>_@H5^UJ&z$Bi9obwpogws^T zC~y#vyjoS@U{Vo=yVG|_QUlUcJVp{f)Vz?C4+v$CUL$!!ABT-L9@FtNK|Iwelu&`* zbtH9l>?ofbpEDNx${m0bKZ~6xg$VB3Ww9>FAQO`+|8D3vg+$+?X9;JijFvBp(AE;* z9F_3*IA!t43RGXIyY7Iag?uDz^vd5(P%`shU}GawIsjCn^o`L54$Z=s{Or~AV33|R zWWlKvqN)MI87~g4Yc*3!<7Y(T1-S&cIa+Gw*aYAew(hDuly9V4$!r0H{F#UY5@N7z zRzy>iR}(snI~a&=xO1RxFAWV5JnsB_9xw~B(Qn~YPds32T4odDD!}G?e6s2kCuKy} zRO@Dr+Q|I!KGJoKbb823lT5IeR==&$i_2)<;EFxW1;8+dJ`=LwWcAccdNCNwigi%q z{Sh1vp)y}HBWny~ZB(8tDFr>6g?UFBVjak41*aMrNEj@}>pi=*kKPjc2PCNtk?fbRk?%U41c5-?e_7W+lBOul3yvsvC(j3$t!u0#O~j;0BnjVs&zu{c$v?_YBT&y5%v(OEVt*B~TT|AR+LxOe7duXrm=w{vtfUptr zf4sUD($7+j4L`@cOo9002QDP=S{(GdT?(G-*=o975#O5;C6D+RTytU`EFexyYGd}e zB&@lBrmI9R%OM2FA_V5~Dd`v$KY3(3Sz?bwWi$Rw(IG-oR&NJg zvtA_#Q!B_cu?{t2uSS-Vd3fe!Ldbj zi^&2-0eyb!trZ0K8#22Vu`Rr{GJrYX=HF6o@GB)*3LFoudx_WSw&LepbWW!;9eo zCZZj^A|$(RFIgpw`qz{-E-Q5C%E z)G_e)*8+Wcxy5fxTMXr@olN{b59cyV3LbgFNY}_b zXSs0dF)o8QFgE5$J$bSY^o#6c(h5^Hs(vb}9VJwUkRypBW)Q^a)S6!b8smh~I-dIwxQj`kU1+Uy^&P*PlxU6L~K_1W&*DZWod5ar+?+vF~7J5&T+sj^H&! zn1<2)Y&RNtm-l>J0E@r#Yz*g`pj3l0_XP!7ls;9U--&YT(biSsGC}@}*1tDj4Kz{R6kpQc!udXX#$~vXBo^ z8{giluyMlDEvBEXsT}7R?n>r1+xpJ1T>X>-Za@hvE<4K2AOJhO7Q> z{^0FL-cgF!k6*pCUaRO=i;?Htm0_${)9v7 zY)7N6goG;aa=<}QlV^}f!4{c^*vhrOw}2cgLY;w%6d|D;%KJG|5;9vVTFS%I+dO&D;fqZKgV#GrmpRH{(Zm!@(jT$zLhCoOTj7{%~YU+n_ z_PyXQs^7B7KCWIFs|>o?9@`!5CWW z$mL^|9;a6t5Jn2Bs0}8tdYYqTL%38NJ_u-^Q`m^&#>>J;grsfFT%-Dx?TI^@_rL3> zY$UAIVFE*PW%2pd7K`#gD{U*cAAU91b}s~4Rbghk`1?*Uv_8k1>hs^M=->_qn18=0 zS0=2gfz;%oijQ{_r86dCl_5iJ7yXjAN;=uOpiffZrGtX)yQ*gLR4gN@m?ibmY{>b2mwHyMty2GoszL759192$+>h^_UbOWYOoJh~MkI zJ_so1-Se0ymiV|^YqdYM(lv{#1#+*b&ZRpSW2Sgz+Y%aV0=*;c5f>ZI=e^iwS~6mu zk+_`yX`nbz!41#*G5Dg-N-`wkdf0lp+nLIJPcLzO8FP&=PRB}HHg&kQREO-}(WeZi zHOb0c0EIOawAK~`d6t&#q(4oQLufDtLUl`}m-nbR5fL$3W?d0;H=yFfD%X+~Cd03F zUol)Sk}=evrNuqeH)j4k3Oc7Q4(Zgk4pW?+l2Z@75*XRfz&#VkeT|#qQN z=A)n4V@WcHEXYoN>o;G!N5m-tC7PLcnc)ECr$^^KJdZ`=IWE*U?f-4yV`7-(R`dFk zVe-%kZUBUmG%+8 zEK+2B<tQ0wSxOus<$2gag7olrPcsY8&gVgFA^d>0h2`1g9TmYyg1jah1qs(45wL zF}xWXWaY;i@dwyxe{SF=XoLsVhKO3J{pnF@*^b=e3q8v?iT$c&kX$l-WYM_?pDBI1 zEXVlkh8QTa9|ltzet4kWeX|15_Xd{O17~NSmal%3OdvvP_%#vabm0o+ozc#8wrXsQ zbmx6>c@e|P@x_?}%f#L}6M8H>xMB|rtix;&nF&Gike|Z1L9nat#((-7wmauf-sq{% zN{GMZ*N_?THWt4;Rh3M{3=5qz`{78ww}o2vFAuCn5&6Go; zLdMR328T@0Ney9xM1mq!;9#KDPah-6-f!aKY3ks!#TkYfegp5dWbAF{rr!PObm7HD z#8NwV(Z=S+dBYo>lbTTcjVRzjUSb`6ESp)jY3nIY5K{;dAjPL(&R7?o3aD#czn^1lWagz2}clkziPhkq^NsR=hUhOuV%Y(r_*&CY&x2G4Azo z(`sim_N$kZQblA0;f!(RSs7bu=)2`1f`z)EX?;5i1H5VxA zwWL;`1w8WK81spNIAhaZx66h<1uUYgZ0byax^po(q9!}}Gye=r*RkT* z9E|Qv6Flrj=2F%%5hKk2SfVC83c5EmlG3dc~c(EHWpfIx2?Lj4MrnqKaqBc!-z-nDz z<5O%z)W*%;s^z_JZMqf5mvD$`0*u+)Tj=Fe5bCImSU$Zz$!BCJo^w#Qz_nYIvida# z_O6G0jVDeC4i9kNzeH3SZ~)t|QKpd9-b~ts(PLbCd|kYXkZGvQ4R)y|twkt+k5e%GjsL1HkNt!8&5Tv`F3%b zVRE*N-#*aGU_~ww)gkurCXkEoU--`XOgkyqbLH$3Qil+2w5lzts~$fr6@vFG{;QO+ zb1(yqt>)ToiJ(p{1jd>lnDj5s=v_q}6Zk)n7-pqd3;dk-&aK=Fw#Rxf4!=m^>F1sl zzkp8~qjy$Rh^J(LqbJgb#^%rq<5y4-<#sKw(z?*{>pU7Ft~2I#A2oEe4NL^bPwch) zMSO&Ka5g0F+R(0d+Q#<&p z{D}BWVm`&&u|;UT-#=KB!D$=Qu;p)ideHW%POP}stXCF113rK^Cn2uM3WFKt>H@f*@{P`uE*9tMVtk!I|^Iwfq_m z9t=@f^FL)ss$#g{NR}aMg|)KIL0cpN$6ZfwGyf)ytcQm66P!lfa~Tuj6p@Qqge@U@ zww#n>O7C{>O8yi=4D&bY)gIM$L#>jW`B?id`sPA|7Il2uZ14)I{m#OJD$B~7&qS8o zHX-eve?ztbB%U)wH_4t93rziFwOPeEDrCcO;GPa6UBuDp5JiBV)h(FdVUHyj{wsiX zeiKOQ%pre|glM~?;}n2Pb$CF8wnhYkMKwZpKoQM%_0{s;n7|vLLy=i27dmQNn4A92 zCgZ9}O#Mk`3ElPKV)9s{EkyyGq(FEp2YB~?8V&i)*zn=F$X;%4^lYTnl+L}a-&K|Q zB)Sh8))_C-aL6=r}g97xG_2k9W<|Rb-X)igI#8H;RaB3llWgxSG6lhKK?J3 zg_rHDWM7sxI2vY0B-~0n{JFYR4gj|M7r{jkVUPdZfQ5TzFMlI*Iw>OFBjZ^rQL&^n zXs-Gd1EWsHm|_NJt2qBW!`}+ix}X3=4YZS3irl#Et43=5 ze%Ch<_~Bs0?h7ctnj!*xdrozmK_-DI%xE#7{TJsA3oWGKl3*tgdoRnM;RF=r-lbQC z8;wguj9d!K4kb8~pn6$M>*v|LLKVp|wVQD+$9Ohy`72UfD_8>@jS}`=TYt{rbC-eP zf?gTpz=I(2I8p|D>FqmV!Kylr9ZGQe%;NXMe6V12;B6O$$|?*Xcl_RKo>|g>INRgx zy$1d=aLN@ezZD0qx2JNW8x|oMm25p`r!VLxlr>~>L#$Qgf1~Og#WeqgBg%O$Va36* zJ$wK^of3OB61xi_ubQVT%o~$%bSw4%4+cr3)eTSo3w@cb0vdbQtNyE`4Ve+=%u^)u z7%=@m9Bgd=QHY%J-LmqR;T{I55(5zW3^1HHj*{T6Bc@F}zffM-skFOg4dH2bT#|HN z3#r#6Z$w(qCm^U~7oV>_jA38Jac8gp)kk29K^F6-d@US-V`kecS5FODwJ%BRULu zE6|R?wY$8XI1^1UVu>+9VnW^qY$=9qe!-JwOQUN~^wE&@%xdnm%U)GA`KfIa*7%M| zjbq&)(&X9+gqoxw^q_;{bTwp%7hB0*z@1$*Ojc0E(8e_ywQ@M1KvEXOB}tMLY}b(a z?_$W~AN0Y2uw%VltXXTfS0GFwK6%H_CWByyR*CAABQ*pH*e+H1Q+}8gZ|Uy~$#+p1 zL>s@~EIE{~)&$3gFsPJ81wZ;T$n~aLH+?DF<~HOhYI`~HOCxeG4$GuAWu@9OznYEH zm-mV;@ftm3loh{#U7VUyoPNU3vdw;2qVKDxa!*WC1I}J*AmRvt$9~4J6n(#cK0(dY z`4EwH)c6!RWj*;V@ez1d#pRL*s<~MANil>Mb&=>Mq-bs zz-Vkm{HOQtEm}%+LA3=);pzlSa#9!JK({o$OQDsyt*EW@tw@iBU&@H(RHLX5Xd9Ai z4Bd62BNX|qehQ*Mx~&<6lpBZJq1b+)#qx8-K~JlMu{^Szz@$nBtI&~yHbLm!XA*Kv zq4}RjiQ^&YdAPdzRt#XQG~*Z!7&|!{Je~kMy*vTJf&xS)@e%1cjhB(s6`xHSP6RORN zzT6(E*M(jIdgQAIzXO&`ijnGf?ou^(R0|D2NOa~!X@O}Mi=p*YkeJ+%aecXrSMO&A z2H~+12nr7@3CX}I!BKbKU)A&#x{nxXL?``(*EV=bqMsXm&Yof#c~;wVfr?P+Q5zE& zPrDW^O=D^z_9|4Yl%QF^E~(iZy8*!ptC=|Jxa~R}V|;iWqFFUW*i9Wu23?3uquF*n z`VUcg7sF!eZ>}wRC)M@asX!=?H|6w6cN*O}Bx1arHMg0)K+Ka?c8j%MvN7+{SF!E1=Ofa5*0%GMaPWI{d8Yj zG6xjn2+v&{IYtxr1oG{rnCO5Tz-C#C z^@cU^cX%8;kd`43L+Qcq=#?X*|D#5z*Si4h1+~Zd@~qwlt@Q5duJ*kweh#>%u_P}5 z*x3RzQO0bcV*FD5z2*nOBUD)Hp5McZM06aRO`+Lq^2yEt<8)uMCdjUfTWuB&&+zIp z#n4fSP{VC0^kJ0AU!?;QP3Di}LoS3@bEuy{W=~>c{)~P=o2O3RoNc|{1;XJtg0?&) z*iA9{A5eaXyArnrd75>5K~+|$ z8^k;{Wp-zNYH0Xdg=i5Lw7VDW6VmPz9<5M}oy8o#A}HuiZp1x+q~p_U;YKD||GgmL7) zv{r>im}&635?n-8g(o;&V;&P-`BdakPeJfEPrNJKf(E{2gyt2kO=6SScz}~>j0b1C zBqH&Cl2CP*EeG}$Av?`_i>F(NbPE$G4{8soqVY2r^|cERC6=W%D!`Vk4fiyj0I`QEY4M~w=tXW^o|7=C1KALwAX5Hsh+P88K1d2Q z#ZIh=&j~{#7%3aO9txKOImy@9h-SZ5`zU6%-*$;)fcGd?QEJCCfXm8koiqiig=WE&ic9?Ffm;hT<@~v%It1W_2C+k9v44!rObsXmaApA6ik_Ep}`M( z@}V1&(Vnsq>%#g8w|msN!sNDlEa1C2P6kal_9~a2bEtE0oeV}=C8RQ$rz=d0wL!9< zWUwwcxVOX*z=>+DJ#P(YG`$YHXLAP{)kI;1^7e(b_{i@#C-l&rqbyvooG*R3g2Kc2 z25jf$^dY;w{{%Jd;_w2|UDn?mgPaLvJpyqRtb;agdPH&d!pMegdPm*AfP= zgJ=@z-N;SUc4x{dU4n6qg<5C}b&A=Z0f5}()=BGED*di&G%x)G`5<8+_N+s5%Ck(sd^wxYxK;X{9`tiPzg2jEtK!K}Ip{AsNC zMCSi)Ejl{5(;#HR-EIkaad30vLH1pHutx1k3nQtUDTn+p+do53dGOPc|5qC*>t%2^ zHUV28;qn0T8jNKOsau;Z0c+BKeWk+I$)ub5$#WAeE(D!ut=S})7*;X1Z@ZQmWa4>% zHR=+T-ZyE;qN&kDrmAZALHrErUI>av8<*D)7On{Pk`;%bP%FbA<^tQTK+WN!a=ZuO z&*MKBUk~`Sy5Qn&$Vp#0Sp$J8reV}-00WVtkehEQES#Y-2@4%FX@e~G`s0t-+6wBi zm^c=Us03gt{#ihFQPyNMsrf2fzbubsbyrBS;nxY`F%3d3JU(0nJQ*BirYgfhndvM* z%G5{xb#oD$;u5xrs785WwD+*N!yTfaUt4W<}#U8#9Tl}^$N|AnoybU>G z9CFEKxxGqQRF2%(1xfzgS$Hr;{NT=gPEPxayFu;9u>Q~`oEa?+$=_}SovE<(!Bh&c z_q*mmFfB!?$Z_meN zqW43 zVh;DogAB@1j6~2)KUzYis`yd8r=5wZI@Qb^?I*=9cZV&lRHZNUKAVS z8kf0B^Mzql!yC>#Xl$Fu!Vg!}f~Dp@qN7OeFiGJTPHL3&+@ zvWkwZkAb@qE@77S;+|JMD7-KHBXlh{>m#F#^JBvV?90)&iG2~cp-@!3M3cT1s6nr& zJR0yt)d#-@WmKVCsG88}=d;d}wK|Dv_E@>0e$k*U4D^6pS+~0$wt?)PhEDG0;3NuA z5QtV-eK+=P8VkV}_5W5#egTF-=BJy6qQeKynY|<%{!Nh2Pqz$l%SW+lcip*(2M`B@ z*NnLcrdP?Jz#3B_!%az@8u5v-rM@gT)GWE(NpzFKbLd=ev!8|d7*AcaL$H3fUo_l5 zwfl%z_fTcKv|BS3y&#Tg)EDT|Lrc8GGH+GYVE_<^PUiE=6MTI$G1o2^TjW}#fl@!o z=Gb-(vY^F-iOc`IuOcOS%pnsC95!l*6?h}$^iIG`~e3uUG(5c4R?kqIodm3Dsy;S)NqpF=4_C>bo^wEo34 zE*hGwZHT2HgEHW*v$jTW17BX1)5dYe*=Hn=-J{MesyLc6Uj?c1pO5h9pgByagsCPw z?G+wG4pT5m#Jsr8SuAeDp~XHZ*1YEk(y4-5xFr7&B~1zb@HG4mbw<7|pYMiK82T&Q zhv&JfI7f8WeOSh4wC{r|=&s0&e=?v`HChjfJ~k@{p)zbe>Ynz(E>u!X8qet zTeN_syY3}du_=b{S>8;FUbdGIrVFvFjj97MaAP#S%51K#V)Su5<&v+CaL~=;Y-+jF zZo}C$GHJVG!!k;@1j=y?(}_E#I-h%m#Zv$z;ia*YF2%j065@+7#5^=%YJjat>0>~p z9=oMveNKS=nJ8OFB5>af(VRMYhRWpTvw})SKs%U)W((X_^-2iab~tEH6B`&NhM0S) z`-S6God_dA#E3x0Dn+_|4nw9{3{I}kO9Nx* zf7o$gyr8)1OIq{KW`AUbm(>ff&oOc9nb3td56z^OmFzj8V4-zdi4oK-ZDPbWQHRxf z9#5)_;E=!l^(CW!F^S6@G9R-GDx`8?W;&jS^t2YtX4E<#|6)1$Kh%2oX|l;UL2#by zaS8AiCJc;kHsOXd5#B^?0$3&PO1TNl0Hk8Ji=Cop;OoaZ3)gpYwD<5-k75AK@J+t; z%n{?eDu1N(%WE92ycjYb{LHm@`e4CDP&o9WO&hEE8qfXazpX>*USgHBC2oO?QH&5( zNiwodGRMJTVuVIFFso4AYv{{1=Hg+jKW!>mBH{@G*S^=0Vd}PWwXK#aFuqL?wp=ts zL_zbdHk$u7;)JG$Z*`aRHTjmeO~oDNzsBXU-6$amD?3g^?Bq?sReJtWKdCU;K_L;G z;pM{u)98j&^vZ;LowErf{9r^LAOJSIyxr1wC1XrT7uuUYs40XjE1IFP;x@_4n*Y}o zY8a&4zxoqkkyr~^JW6GUp-xwhd|6dnP`m&CW|Y5k4A`F;*^zLJDFc1fuh7xv3^KDn z@GDOBiRj(9L2!W`1@2ytB0J`#vW&oqHE7u9NTU@jM16!Xs9+wQJP@I8;Hi_ofN}<wgRo=|^R@9A@zh}PX=TWs^lGg28DBT6Kb53FHH_oS3z9mWK&959X8S;GSM zV}oD!$G;KXIKIc&+vPQ`!cZ^6;<1|=DBcj4RUFo;OWguMScQSm&Y9!v_yWLpSpz5v zDjA2(LKzHuwkA0k58(cZiM&*eUlLr2F90)0Ahv6QzFGo1c1tDV2Jdaf( zOIO5v>bc4q%=re!_!}cLvis=|a2F!(30T!KU#`aJf<~Mj%-H+{x(Xm(|DnOR{3czU zL}B%nA`a_VtB^YRDlx!)fr$LCbA&o)fVFf*a@hBSi4KIX0M2JXc25*Pp;kRUL}1%$ zRVm|{U(dkJj`83-hlxZVMpaR1Yk$Ba>JY#lXssH?<5z}|! zmm`9R%H0tRpR5N|2~3GBChZJg89JMf5*H29#3 zJSlmyEB|*x>zpR$RuKbr@=1h)a^F~8&TlA7zFhxLt`RWsc#Df092tD55+rx4+^$xl z^FWQ6X+~s@>acJP=1j({=h^WEd@l&WVWeBjmtp>?BUR>NBCc1Raha9G;}BSso*B+&F?9S&Lb}VseK>PXE`*0j5A1| zQl`Yxuimb^jRY=K#Uq+vz&PT--OTz&7`JN+{M4n)li1ZKiTh=NbVrr)TxTi;!{+9f zP@buJgy9=|xiR7;&K-H3f>Et3Lk8d#Zd0@xgAx~4 z^W)y8QOH^r7LqeHhOot%IuHnpNxO`1vWL_h#-!)%mt(U9ZKFUy8)s)rdo*O21)U5s zr(EOGiC_N5??s+}G$%80@O7@Fgsr|lW9ekaEXQ|n3+!)#yoWBiI_+C&Qq$6XK@@7> za?Dx8$iY{xe2la^$|;k9`pmuH@(qzALy+0V%0cE-Y zN7XQKp-Y8(FhXeEEqO9YSOz{_eKB0lF17P~IJ0t^q7;wm^a(lM4ue3a_ zgyPJ2&gL@-|DLQ=mI4-;r%$-ZJ?@ZzWB?HF#-e_jU$PEXo;`)g6@($gv1qBoq(O55 z{%#|t+Imjq_4x&}P*0Zs33Jn9&&9UaP<9z!0q|Los@8mk4P^QcN-CqPF-dPgPkNicReTI|7gi#+ zf%+Mv_#rt#=s|dZ=fYI6&KLL4ComD)yWnQ531@-x& zvIs}iLasGh9&!(#w~%fZJ2zitZP{19jGA?SO<6o*q2c}VQJ4hfUcDk6_@3X;xJ|ay~Shp{U~!5 z!5GCNFf~=(YqVZ;z$+&68Jq#3Q@M||&>T9T+j@ZaIDjm!$#igtntWyWH;08vjxq8{ zT{FXXT`<8W&VPWHP*Frs^2zRAji>O|OITZv#^>?h%bD69?v6()nzL+ea4V4|r`(Nz zml#}WBo|=VT>6+_Rj;tXnc>BrWe!JLr4;240wUQ5;)Mml%Fp+e_8oBsi;QCFk{4O0 zdi4SU+;P4g`+D|KmpS66&wUQ+Q#o&6{xSy~Q?x^dt=|W}%Zgq-(X!c&Y=3S;((2`~ z!YNY37#Y$Ak1inmjFy5`PNVVqvUNG+y*;Sr>Lik4tU3=g2A~ng$&~roIPWV(#0=V- z*Z=+BPX1>S^abTE=heqEz)mrH@U9fcQ{rtT)#;PFr z`v^3Z1aCvT96I4=1Z}Cc+*fZAaaJ;QJ|9YZ{$!{dBr;m{v_OCFRwoTJ=n-oV&q4u4 zogsH*4FDjbaZ9CwhY7yA`Zu-No1~zC@ht|;WqPrt>2@Nh$+KG{x3Nt2Cd)M-Gr~$A zr6TUpqN~b;v=k%P(TuBAm_B@(m_!%u)F@h7_Wf-f#n#@p!F$~}M8zhg)$7ZQyQybE;! zovaI{y8MAJYh0l!GQA1nJc6Alt{+mi@~8cwl{}K~nC>jsX_<$>3ILOSGio$6os zue>@jz^R}+UbU85sDQ`VlgM&S@(dkTN4=rB;!u~$ZNayPwO+l~Fy zKx9`OY#?#i-Iqpj7R9V=>PM_mxA@ ziy7j*T1`|NMo*kOP$P5O)DB4i0asnvjx>n(Z+8mEV9B|Xy+hRwmF1e=D0Z?5MQ}I( z+D!>oE(uQyfmx1n{>Kf^5z{FneYLUzg}@h{33jeIa$YDBHREAchx%e}HmNic20lsY zZ}@y+PP-nwr+%Pmpk-(pDhC;;9J~VS3+w3Einy2C8#xJo_Vt*7^5=q*lg=nwLdA>_??`pFGUP6XYj# zle0`PI?n*B?swiPh0=gu^U)DxS<@irV;r+U=Oh-a!N8S=d-`74ST0X1L{r?;+x>c% zKG)0qy?Mtl+FN3j!#F7BicEBYc@G0OZ9ySif`)3Vuq_=H*Ama6rT;bkaM90XHHyn) z^q;;<(4P1FJYD}9gT1O3mk9}5YrqHPuTIF%uy$jXPk1PYWf{5Bjog?qY}huY4}Gw< zTO>-$;xt09i$^KYmGRntgy7x2W2h2e4q=~if8Ec*U5zudXRWR6=P6C$I* zPWY3S!29ilKl44qGGod@7`{TXwGm=sG*H~+wfYUk%YEF@fj0&Uz_a4eC{LqxHr8cK zf~H@xdSzEYpI71Wf_3!-?b!At#3t{bGOwmhi%m69!pbpZ8YoKeDl!>my}atjsM@c7 z1U^p(2xI$jGI(s5=2QT5UxQP(&xJp<6(w)g8ezcp)oE)~bP+pzb^-VH_RAP>LG>SO zMn0qx;VXUnKc_WH_aXbymaA%)w=?cx2iE?icGc-D)~oC8uDN({sD0iP+-XemyA;>`|lQE%lx&I7OD&Ku54ZVT1QC^@oX&&uIy+_jYN$RPyDQ2f&2e_$2)k z0C!Uv_1Bd>aFKS>e`PGXbtV?q1N}3H zo10jIw^@Fj4DfolE@HC>BiPOZTQVI4z-3@NI!mgy!chxAJ?&d8rexTBgtv7Fz%vbu zA|+^1cUuo!avhkN&VOgs-d7^ z9PX8jMv>xA7tDyI+kfSls4)2Ilnc7J=6$lp2sdF@7#5sgW zGGW5X(gfWPublW=TugG5u}SvAOy5vQ{PF0Hsq}wm&IV7Qv1%4oYC(x*gsY1k!tR#W(WQ$RW?Ghc&-qRQW=SS z@ehgnnRwQo0z(GMFrx+@jR7hC%F8NhmbjH%(GCwQCKAF^dBVl`OS*acH?VZ;S9D(BRJswCwwp1*@#`oGB$NgH@2uYS z_t$6z^RBeX1!HIiO*Y&}O>WK{DB^uUG8Hc@sp{$WOuqba3TeY6XF`_jD6)EJE@G9_ zNq7bC{qf%%_L&ze=_vkGEtU#Uu3MG!g-NF)wWlEF_lg2NSO> zN6?SCG9(&n13)HTkUhWz;Z?G_*m6iB{wECDwQO*M7O|OVHWN6JGUf11`@-@qUJR+n#+;%Nj;HgHvyaK1tog zMkY@@GpaWDnO+45>;9qRQYXpp0@P(+K2tlERleF{u3SSg& z>3EZ~ivL`jn=t!-%83lqf{;}#6nqXwF+Xi7QAU`^n}Ox0RO%;y$ zEC8z=@OroTQxNf4nYX=|JW|(CbW5pyD)m3C%RLPTw%oq<7&_8RHNB(gqbrbfz~IcY zNsu^SVaiw>{h`AGM97xKSvKA2U6Xxq9?|TdoG3Cav+grNZg2ICNGQ0O?ky?(y<-9)-1twe4L_4Q8@Rwm^8x>nSM2%v!(qZW#K<$KYRv2v8V%NMNav7Ed4DdQ)1= z15jAL|K4amgf-(E7G0GjUouRHmugzg3Rx#*k5%J4rQ@s4H;-TfptLzX)X%-paFf@a zzGV~&6!npMSJ4JNM#VcqG8zh~4q@Zw5w;L+heG(UoRD)%Cs2u6uNA7hQYUl3j|H;= zolOEPnx^_ly&Zw_Z0;oI9s?D$uT`nQAHy;>vZm*K@0i5`t8`yd5y)toc_KOg#+Rsj zaR!TYhLL~e2uvE@1aYNo`Td0}B*J zuC|r8sRnP|xBuiR`up-w8OcdYe;vL9}K?fm2BU&xWN2X80NnQOuH~VgGoOp`7 zz1m-bZA?;ZU1r!B<=gkSqk9yeCy`Sx_u$*Qu5^1=g+Q=gng7aNqT*koczm%k+7^Rw zH?<9>BLncG1ikdg@zJ^40+tdPfwyooCj)rDEBTFQMUpU&_l=y!9o=}?MlO-!qr-}^ zt;!Xx+9&mGEYR~3NLu3Ao|@CqYh)1WgWLoaZZe{>{fgJc0v`k`uhzygOR8Y4f3nRY{-n=cs0L}U?ahjPM0YK0hw9i_~9+*1t&kdpw+hP@kyDV&PYN<#ol zGihh;3kG3845wtwku8e^hH}5WPRx-wsD-c*)8pBJoSP(PB?l|!d0e@DHP$?&DpArC zx}MNL3R3n!nHvLzN<^AJoY}>;dRvm0rN%yoqD1cGbxY=he@!d8QZwhgAKdqz%(i-;kdX zfRFOcf7h3~$xh!cd8bj7j&rH@vefTrxE^9(+1mHXk4RZ2avVuhIZIa;Om(hlRxf~d zZ1+?i%fAop+*@M5r(94Ivm(k6*FCm8&}Tlziqs7$_Z;sdXLwh|>%J>-g=*3%KXi}X zV-~bm({E<`_eEPc@!AhXx!}WYV>^qQ`GyBdNJ*dH1ET{VAy^ROxd1}Ba3+Ic|P_vj0ThDj{bRkw2cGtP5pS3)y#4c<|~d50$5$2-fTwg z!kN6_h5D@j!PK+vqEW+J_2MG_Z^cdB9-28lv~67?aQDYL5@bPhty$Z6gfdEr!kSv$ z(&78hYtxCBUDxffeV@EEQdNRRtu|1U7RFT!T=stL{IwGwR7uGv+Y*Q+fGX#Gdc+Ij z`Ko`mxwQ1>OZA8g*s9N6x(BT+S^zFVLc1sr@Sy*` zV19L7Jx~}n$YrlS6G+$MjQ7d(AhvFi>eb)r3roz#7j5+h6R&MIKp(6n!<%zPcdmwf2r~!VnR_MT$(R4O73`1*vc)G7t5qU~e4x z*;2ymTo3`boDiP5a`N#6*pNJEhc7H6d}ICCIq;`b8mgr^T=DV2SE4%c%0h}#kFb! zUaxC?y&&c8=F~!=;b%UuSp$C5v|V5=26<1EZ$B9dh3GgDZKb!VFfD1{%tnNYNb+y7 z`$|+D*h{G0bs^6}eUdrfZve;WX;tvU*2;#U%Z7IS%b4E|K@W%|lUvb{$p3Sz&4Fxl zKjl-G+Ip(V%TKaUT$|c(6{3g^MkhlcYI|$w{$Zydp^a-6cL`IkntyY26qe`l?-=Gb zw;Ss1y+D=%fs05c#aK_1{S?K*JFPcvaA+!!asFu$k^BmW==sVMBh}IqT4IS0!!U9( z$n1irfj7%pZMqmOBh_<(Q@P(Bc!#f(UhpQ@~jbfc-n~P1DqW_2(t4$BTJ( zLS&>^h@iHtnMa5ur1M46^U1E2bKLr({>m~xQ&bN*NJ@|J66`qg%Ez8y1 zy%)F2^@x@JDoU>0)8Y#+_mBqg&SB`YWaM|}QFDh^iW*VmuLD)#fWYU0nQ#9jJS9u< zZyL@!7+RXPKnNwnmruD=$NNQrU6L5*4q`SG0Dv2@5>kdZDr^+=FRQZaHFSQ&(^5p1kRdhCkic)l$z@b-&c7U&v2@YU?<)~(*pZv?+QVZ z<)w8s$cc_bGYIhgA?9yotpcz4K_=W2x|kxPDtz{Ww3D=F-2;-Dt|lx{;Q?5P@fm_R z8B~T|z0tFNh{eC=MehhIPta*G%SHSXBW~#ETmD%AB86|7vQf~pvFTrF9cB{IdZCE7 zBpoTC(5k1GWu&4Re`yD3XiZV07!`#9-E7YtQ{2~-26bzFZ|8U47vmG~e7S*T4$Cyf z_E5-fx&w$R7~=ncU|SBy75UUPDu@ui$8srQ0SzHcR%THrbG_xY33NJkll#_&=T^`8 z`>KAs$QF|&P1a!!Sv3qZpY~_j z1TJEv^Ao;x1<+TzKAw=8e)1)C$Z;ceScdJ$CEHj*b1bRqY%^kbfL)l19VSh}%vUQU zqGVeaOv|8~Ct!h%4jTmdYHrHe7^4V7O4o=#q6&b+PLopZl}5ndriw%GOCJC~lk|vM zwld@{EjT@!qtqdgXPfO0c(`kpm5G|u zu5<*$(i^_)Xl-juiN@k_`EwV5`|(g#IMXF6gN+a)Nh*UT@BAg!_ksqG2nE+xz>dS^ zAOX}7H0#a9>^;?8$k*2MQZRdBZD4E*sppCm?V$ zhrZnMSwx0T`n?z)y%Ub1iQ$Osp%CxqW;miI6;oW1Tl4$%fPDys=RlL4+2JJr-`|`g zYC&~6l)vYj=C&hIDl^GAjtxwH#zPF}`6j}=;P)2f&SM$#WwCtEaV&ngk6F$6=5=5- zDLwf=gy*-t&tkQ4US+Xt^kXHK8@LBfaeyh9RBpDs(!?zs&lW1jX#!Z`Vs7RNlswq| z>GW$F+fQkH3}F96)c$&hs$wl)ww7Gh*FEz?QmxW<_-LJ^nE zE$GZH$H1-m-uYEc$|_jY*SF+7Dn?@{aaUW4-zJMN&R=gp0piq<-THfbBhVF*qlR-U ztw*2K+`XzgM0Ktay3lBpJs4ei8wBF1p6OLDnhbU?d_(P<4>7WjdjtXw-3B;MD|)ID z^#c{TkJE#zQDc@(DJm3Ji`jM$TECC}a?hI-82}Y5f#UG@EHaOpZ`Tx^T#SXSxD z=26L4k?aDdUeQ!>t=h^-jjiuxj^qKJrZyc5sf`GJd?mfV{M%J*OToCEyPmjw;?-?% zdndqeu{%yh;`x$z486#_aFC($d#%-_t-bcnmcor&`$JAleyrLdQ{jU|>#$gGH$Mk8f3Ri{+`^vd{NXaN3;L(8F9tO3Ln*2(O9jI|FO{!^<1Y{DJTa0S14Bg4 zMa$i@B`kAc#)yfu!i`ftg3DqMMP@60<^-t8ACz=@jcQbuaWWSbU+0S)$}R}oH8MPF zY9GVt<}9}0)wNBo^;T`*64bg$Yf3c}yay)riN6Gvy(S}kz#$8&c9PvT8 zi>AVe+K+_(-Z%>?Z!AsHoB)yZwbq8Ak)Q9QusUfpKtco-0pv~p1ve;RW(@3Q2u{JxpRp-;449Yr*y2?HWfao%+nqcc;#g6h{n@c#d+^xl!J1Ue+sWD@ZfR5--hVi>}?inSDvuQ40d#-lFW<6e`a%r~Hj`?iZg z3Sf7U4|UrxM`z1)hvoCMNJ4#;-TetYILO)J@pIOt>YME2EC=V(bSt?Y&~q0N7KM1# zy&_$y)K5c^b`n+zV;uGKH0}069HQe(E#CXzuW}2i7OnrNP$SujHu?RE7$r+f?S&h? z4C(U5U<%auIm#>UqSzj}v053|mV7{4PqV)LgV01Bpn3BR0_`=F2-OIN{Wm)@FE`zy zp*87qA;g}x?!!4{?U15Fli(E$N~~KOEb8jt-)3E>P@clq+>30OfJfHe)gcZV$;3r6 zV5YAP!-ym1zQ`DbR zdjD+z*~B8n`Qb^zfNB;!MP4~)V97$;E^4{*xV+Fv%~*~U9gg#cgtJ`~#;ZRrda1#= z0iz3Xm}xFjr+-t!)xO((jb~1BJ0>yry9l3^;Zg*rcbCId_xOCif9iEv(qQy@yjD!- z$<`&6ig9#2xvV8nE=^`{)Sgl#Ui0{K<%n?DY|W^5{-Q|(jY zXK7;C^Z~nvW*=0C2sive1zl;Ajt0A*=)X0zNU^xXp{U9_LBgrg(XkxdrFZ>ja9nJ5 zQX4Tlq}K{BCXNAOwdC-Vy@LPcuJ|9H&+~6(H+-a~xWYJ2HWVt|fH^5wn zE#LdR@~5s0Xub^xB|c#X(b-g8SXfBhMr@|T21;f&AT-N@eVIQ_kErI9JIQwz<;l2R z#@LGYW*YU5 z*o^w)Ay3xkO#N$?WWV6EA|l$0sXm?}+6-zx6Awd1V!X^3-Ejo1B8N1$iMuP_$uKFg7h4_ z=4ZJxn>ibN+~G?3Nwb9#dyKtEs=Ti6@uTNU726`>KIYrJ#Z zY=<9U7?lzBCl&#M?*ocdrN>D8f-|__fKV3 zgRdNoJ_a71XpXqG|G_d;`=!K-)x}@`o4NZ$+hEAe@0P%Bg>F{dH1LDEnPfCjc?qAW zQ=WE;$sp&S6JSRHOWBaXd`36cPLr)!b<}R3t43v`(;co*53!vzlcMFo#i_V_)tiNS;h;cvhvZw=1v3 z)SntKKvDjHhhQqWugJkxQCVbVsk)yy2&jE*b)pM==4B5b1N%Tgj$|nTrA7~=O<^>d z8|94R;E#!zn!5V`p)%)yK#MSj>B-hxFQauowReI4q!kM@x$ZNvjO<&@CbyqND z64xSbMZutlUQ*E~XJFFv)AlH9Pd?@+P*fT#gH4WK;yqF$5Ih!=dwumg-vs(!!Ot=X z#2E^^p8RF;ILnyqL3{k(+rA&$u?%WwJpk~#HMgm_g@b3)voSRBDFsd1g-m6MAnUI9 z+;WZ8GkHn0FpDyVA=lLT3HLcq=APfes5J{U#Mic3&N?iqcwo8Hk4+M~+j$Q`)(+l7 zv&_WOIxB%1{X}c5ik%*l)O^cmb`M3L1vDMkdi$(*FN7PQ(J~tvN3%)G@V`-QhnMzw zbk*-DA+S#FDDFgoerwkHBiR?O>}o7SC=GU&A@^z?h3a0y_m!O&2?#V9lY$Pq$EdYUG@$wK zWg$T8n;?7wL|0TVQNzvt9!mf?XC~)t*9no@^*l;iUG-2b=H6=$KoBBaV<<>pC7fi7 z%c0fto9<9hBMwc7j}J+~kjgE5U&2-j+i^4=V`YUQ{Zn^soTokfO4#eM1LsZffWKj9a3TUCRCKkSXk z-KfEBD!f2WKg#N3`Yb6Gm^ME*A&$I(i+Pw&goUu$cZqwxtHE=)t(U3VHR#p~t6+1$ zlB=SaoS@mEN!C@pk(=A5Lp`{*-xiU865P6L|GD?--yTKgiyHKG?Z8g~kq{E){jW9= zi%hMCa12OS{-6omF90{#=)^%FMh96s9w%`uje@n@xO1CUH;;Ej>89De)y;A+9fFIq zZH0b%uw@SDGLlfsX0Jkpa`Izoaw@paX8=)0xi%7q8>$D}nQSpoX=eAMP))Gw^UmVs z;C!hv%2HXxC?gkb_kTdxn|QJP!YHJJfhMS#RH#@B>&U4+K^@9Ki%LT>)@u5Qu$8GF z9xZ`ngKnALx6eXBj8PoDgkSims}2;o&WMx=V8A*_TIYByZKOa7N&vfqZ%G#fZ)V&k zZNy@b$^W;0#^*0$fv)fwfuU8*pa0rY)y1N zV>O&jlXp=;j^KScxlIT6s)+VzR^E?QSA5*ZY@^Z12uJ8q(kaIRq` zQiLo~;&sQ7St7+7B-5@ZRUX!hWAJflQVW?sapzV=aNv}Yx zwnRKo1?{HB_A;+&CK=d*HI3-UPi_CT!we4O9(A)sbu|b$f3uvVunXUizN6<8P1?Yf z6LiT@z>q!Iw=3ivxSz1PUl4Wjys43_=V{-juvmB9Exgaz;zmaw+HAHYb6Df{COt)2 zy5Ib`hZ})#@b|On-a*?9J#hyf=rHE5ED>Wvb(d6gr~8QG z(B0}YbM%i)hyg&h``u)ym|pBGIIXN}K!M!cYeJxOoiAk*2J5-Y9XiR^hGXZAz1 zq9i)R`cf*CZq!~lNWY+9cc76;tT`PFMCn%^aaZ?rYcvLGR1h-!JnBb`nN+7 znBWmoU&vHecfyNND{%e&4i3W6Z%YwZ_>-mr1RcZH32tI?{czSO-S-`=%4Z(ibSc-h z3(F-zrfXq5H`*bZPz*_;cuI#c{R|I(6uuV8=guT#v!XEP?6OQ|{x2dN7SyGB#j^XZ z2pJ5nEVHU`u~9RBfS+O8xDlc6dto2q{sy)QQ`)3O-;B+}FWBG9_b;i7l0BmKLv|Bb zyPNh4gAh*t8zh)_oZ91xuEs5Xk*jhHTuEnP{f$$XK2GckmBtpS!{t_BNXe3Uv(|@Sa@Wg(G*7`1`QINE5gw zRx~)I-jeB@2?oj{R%O)6t1Nox@6mFAW6v0@!M%N7DDM0gW%{`FG*WzgWm6M$M z*)N;}wFL!?@;Q|&!E<45#>Jo8t=%my24E)i#3HARr%DvQS$Q*H0tZ$rNm#>Ri0!!( zItqK(OOG{74_QuAJYN(P^7TfZy2DLkKPNqzmT`$NBF32sWfm;;lVyb2<%e1lwRwh3 zsRJ!6Sou5|u4q<_Q($VY(D6^jSi%+|y&TSdneOmV1gf~>mB0zyyl@^HLP;2BL##1E ze`(sv$Mr&`1C!rdPc&^1>YjNeJw50wd1kIVX!I^Zzehc@Go`n94^e3>2sVu%sN_{C z{HIk0L+Z$39Vv1L{;Fg`NvtZ5g8O#3Da=!clNAn?22ZXQ$JQ|w%m9T>7xDJB8iWur zINN|DCu zu_)z=l8)6XbW))BdNpA=JD2yh>L4jFJKn8ER=bL!tbIE z_6K=K!#wUqtq>$MQn%Z=k$Sm-?Alz?xvW?`*2RnRhx1q^!eCIKs<_cuNj|k+`D}kY z>E}|xKA?NJjInoy9Q8cBb@Cw>h(Bf=6Wn1C^0)?Tfs9hMIDc%Fvd}=3IPlXv zKv&oPJ4tK=g7Drrg$VKTa6}{m7uU<;QG-EsM0|dJmSI}Jw$Ty=rxSF-o#MCQ3l`Z8e1=BQkB3@ z>};te6wM?{-cxt~soqf!5B*#Voa-?wU{KbM_wak|I%c&ued@E=$g)q%owHYa+%0&J zmt$m7ywqg$xQ0qMUG@mImrYh9btAE?9~p20hh*tkk|Ub3Sq#_TLO@KpGcF;HKpR3n zZaPD<9Ch?w((;?vFa6goV;B$uE-am6dz36EE-E{f@}xnp@pB26e1?&jKWq>`|5&>) zrM!$k)5++Jj4w&OS|>82i7jK8pe65ckKK5x77@vVv+@Io9a>n7 zASb>#ttV#EdaJS4*YhI;!1v(^cb^tf=J;uTLW=LED8m@<-%g>H0v$$b>%36LA|Mt^ zME^fACShpIj``~FJ2_T?5{p?hR4^)}`V|J!rm^%c$H#(G7xQl!1mUkN6w(ti#^2Nh zu9*;^pC?h+4@x!T2&k>*>A(j=v|R#sxd$Ag+mRo(fr`c7_4HNatC%ZKb%kUSO`md| z^}fZ10sGOv0^BH6-mmt!xIV5QEL#7;`yVs^vDIDX&AL?p)H@wNYNHSDab{62Qb-x; z8Aa)PFReq~V7qpZn%TQ0B!WFg(P!0u2qc0H-tW($6EsI+C*?lHQvv1R>|Txp&^rM; z0+}$Bzs*Uk1^P6A(bW2hEx03M5^k0?p0$_nj~h7d1!M&HlYkON<4TZQZPXV))rTno zMl6`d9yJ-|HJfexh?E0AAYDjj)IZH8|A^4aV0l16w)u-7Bi_xssla;{rX%>v-wnCi zTLWU}ewdu|wMq~h`}Y2XdCXsYTC}=E$qoxy7ez1Hf4c$Ldfgj4V`#Iv7W(v&5ne62 z`_~;bQIy>fvRKklZw7x3w9U?}=^#;mD>t)bmTC<3be&+6;UhK=3B6VvYCUNPh}J&R zd>=TsfW;K~Kz@)Xld6Ad50klySE$~JQxNh=B5+!WsOBCMUT5t;^eUiadJrIsUI$cS zW7`XB&1saw!ir@u8N>3VGy#eA0r*>gv#QA@R~>Bf9EYYbW_!HHLI{|^`_ z{Dr3ivUAB>Mq)!nFp^R2nC}A)>5b8=20a0b8rqk`W9V+0QjXfU{Mk+_M=P`A;0whG z;Jp)T-Ru%y6Sg(MVrGI~B$M;x6^h*{vU->5kNJ`m@OF$J3KH`1CCE+=>C~18ey011 zZe>%+s=8101{P3*OIAzO*5?4EMMx4*c$lh^+<(?7OdxwvPu3TUgW&jV;Sr$as^hwa&7W#WNZ9~{?OHGDQ0CAliftNGhz2d`n5@AgBZLy1 zXFf(-r)|**^8M=5cg={$WSRR)IBkzsqk!^abHx$W__MbMuJt~jzB zdQF{`;1{vA5cFj%wt|F8k4x)UD{d#z1=NtL!82F*bh7HIYKh5e$qIcGUS7}t1C4qz zDT^}8zav1Wap!e)hMp#^tUc;)6-O=X_k(9}hg>OxW<{PGd(MRnX9>}>r4s9JiipD? zK2Ip$e}S5V_n*|WM2CAQ0&7kM1CIkOMc(A2y1=*2Jh1D9WzlkF& z;W?QT>Ei-)0p1LbX^8XtEa!3|$#TwYX;wQP)ccNAL+-p;e1dD!1K|tG0gi78%F5A2 zJ{=)CoGq4O<%(Hk*qp?i+@=|$J-F}_b^XKhQo0e;!#KU zEal9}@r=5lp!$bA&=L;t6RxMI9;3DYvqtKb=UkuiaLJ%w0t4f4&5LDjapIBlhUEff zG{RbRQ&sb(v5mTeadZA_H$dRAVqkrn`HrH&HeDD#1 z13GDKH@4hLh9=m}BfB;2Yx0VO(tZOayWv$w=w`HCix>$=L*^W&A=fn<03>9L=0o8` zruS-n=64-*qPcgGpd6b0*xLTYy_VYCj5p2_V09aS6s4GM7E)>8Fe;3e3(Imt5739L zX1^)h;KIEyXNm#@vziVxpw{Pi8WEd@+RB?SRG3>^o1 zdKg#1KTYF^H8l4zSW`C^{E5UNQT%1ZzdbwfS! zRU6E&-^KmbwSe`tGo`^VZxl{p7#D}P7OXjBith4}WIg3L9!(5h#&hH7uu{Mgfs$@o zFvv#RGtm+SKCR!vO$dxBA!?UI0jYGN0B z>z(w7OQq;Z5Gio3Bvb#;ea7Un%eWqun47vp0VpTe*xlfV95F*NQEM6OCy*=%TZjOj zB)|x)jCD0yIE^-zq#E0`(17kwYjI*6^GwZ0wjZ&cvzbvrOYH6|8w;7;J&Fr0p&H8a z3bIRW#0-Wvp5p-#tkd!gOG|#3M7R5@XS3^#Ts4DJx^r4~yUYZVdZQfK-VXLAs?5rgrH@ zU=}Qvg@N7~c`7Z-3o@?=zehzv5qw_WqE>Zz$TbKkx_h7#l5(R%1_^% zW+Wwkk5coo!|~0nG3hDz{CDF2Qm@Vvj|?@q0a6iz7oI{JAob3S8A9p+Jj>&Rf$U%A zc|99X_0&|ED0J4S@W=+Wjh8fiiw;>}wsX_^ai99vA&(VJrDPoEQM;Np<8mi?y-B{>h%5(|^cXV`RJ&yRY%>r=fr zwz70yhnQN+yX2!054K>B2Z_L+YvS^ln^h^U4#lid>B)e#wxr23iZQad5PeVVqxps+ zmyoMcgfX8JE?Tnv^W!bcn+77Y2fde`zSmxL1PBaxn?kSA18iiG#77RjgJ$|maiv<7 z*6+Kjh#sU1E>Hehp{}Tb(FAPRH@<3jC#K)a52J{fBOxiK^p1Um83P{RHm^{ zAi^+9@bZ?6O({>Z92Wevxy#Zuv%95Xju455;c1KQW^PFS7%jpbz8u;7R{KZ-U67J$ zeEMK;a`)l02`_s#oaCl5&OL(7H9R`%E$Tu2J7*s)DzMdFu;i{GS?J|+*Nh1@(?5Wo z7~jGm@5H_b-!_v&0dNNitwMDdw^VjRF==Rx?p?HheSIj^U$0D_-D5dIEn)2=6=Eoi znelbZN4;aSjOiuK$_sm`cBou?bp6SCE{s|(A3#o$6ei#D)QlmL;|nZ|ARyHCsClv4bDlI`hSjXEpbMk4%6H zk?=)TSNK;Mf78C=dk3W4-uBAg)f!TxAqX%RWXY2DyUt5axFzutlbn|S2;A8*B z=+s^o`gSE0e}pHa0y+W4Ocopk$5j?%)0PIyO>aAa%P%6W%wVqJS3?L-R6<1wn97+U zQE=DEsLx^!L=DDMWjL_|{U%5ufM4_K@Q&ME8bTt^@JW=&4Ci;UTg_X`yg{=myP>fqX9NsMio~91Gb$ShkX8$ zt#lz}1|S%*%ZHNo8@gS=hLgWw#*VxcPAmUoi+9EVZp)!G|4j3)yZ+GcLnTpdCYhF0 zAQ*Mrj~y$CAein=ZKHHgaCY1HnzdqdD1~$>n<@^*4N|$Et-`%^LyctWf3P|g9o$*~jKfx1>xJJMWRx;EE| zRL!k(Mr&^lnAI#3>~&k7K5T`#>OALP7_{pQd)H_JS=`udeF$x9n6vlM;Rckpq#y zmX@SO*Xqh}gx55eLYf*HNrr|Mq+Wn|{Ko}A1m;Dxfze^g<@!IsDIdENg2NZ(`v4aC z+MGMdkZO)HpT0(Ynw*Hs)xjZv5S6h4YiW-LN=}xmfbtbBJ=+>@cnLb$zQXu3l1@9; zR#kktvbz$b{=^(rB``I=Tk5AAkoB{W`WBs^wk*R8$HRxn-CG`P7@uuWhKtEUZ`es= z2`8S~xWZ^}PM%hEfZGLkdtZ^!-x-a0@jLW0;AgvA79@tKI1-S@7=bd!-7N~>+$j`$ z;z_4-1F___F6f@c=sW3K58}3VsQ}{FRe8cMV{I+lG}Xy{AAL`~4M5*m`#70)FU*B+ zWFLV~g9W&Oa^JagA7+4G4aW`2uvuw&QfZD;lreu)^U{RW2)E9>2+A{Fy0?y1S5A6( zKmAeO&ck@E!vkIHcqwD#A*H5Iv1KKiB_n^M+i?f}U1Oej2U!%KV@&RFq=r212L^5l z-<%Dq2z;SFxF5eY=mjO5I#3LRDBuPvfn6QOJQls!;_QAM)VjeQc}HF_Ayv?GO_6GL z71oCbJ^0ixQb?t5>vJUT(eUB0PO4Nvs1?*?0ioNttsh~KI}SBzueRYI zw&+Tp_Inz0ju853?7HC)#8NW>ujo+M|378d=7|C&n_UT@f`YYsTZ~uwe!I|&>L%H- zU(+QdlMwxM@(1QfTc!a;B&=^__QXw%v)Hd}q|}^iVV!~B!EDSZP;4A|-^5#g;<8t* z^*$g8NlGJ{KNnk1__qNhYLr<>@W?1q7{6g?Y981fqLDdw9y&%fpYDVzIL84I8m9YE zj~>(?$5_5xQ^y!V{x^OmJ7jLlR@B5E0Qoxd!~TW;vhPFm(*9M_mp``Q3K*XX?J~@? z&hnMeEQcr7ICS(Vx_3td@D-`vcCo~j`NZ*M=1tCPAB4a4`NxJjVR|048uQG1x7Mzq z_j2Rg_rZH#pjpDo<)J2QD%^?0LFp~Qf7b2iT4XmBSU#m$#TQZJ!p)2y>e>kf&~?JR z=IhhCCExE&?5v0v=o`ahu1Bd=93ZAB@JU7$P(jZNPR5tY)-G5>(I(E5F+M`={R|Rf zMM`f{aTR1keUKRMvzEGFN$G(^rp~HPV4ZiilJHQDME*+>=O6q{{)(dj#~Mb_GpW~D z;mrFd@>#^`icbD|&@wtKaYWRnG12LLm0p_F4Oq8k#jAzx)L53CCEIXKjg%a+Y+=Gj zN{7nKHfcb1vx>%0b6xEgb7dtH&Dt&P{64Z{Wt9C+EGityU0YqWh77qv2uP>EsuqhD znOA;heB40TsmyV!SyT8#P4%cB-DK5DOb??bS_7iOeaul2f$!=G`^;$$wKa*;q*#SVF9JxdkiWsmw;jbl-9RGGVkNYYdm6wyx zjKAh|6E=uFz8U;JUmyfkXV~2vkYgx1eE@j3~f%|^zcI*#5A0X zkJQIep+;C*UI9S(&8os|@s@SEUKbz@!)f8|#I<)LGqNSPqDguO(W7O>s)eh${dfeD zFT7z|{T+9vv*~=?!gol9DqE9e@K59az?D)WyuLm%!(OV#Q(3PxH_C`wMY<#Sy?bASw&^3<|O|2`=N{pfMqAG`8c26s1^a6C15$g9O+*-7{j@kvNI|Ya9NKI_L}ww$kr6I_P(X-%GB0|f ziU+y{&ZLSALd)#dTVG=-&(r_xI)Hra0yHn7U?eXMovEyLGP@4aiOPOwxweWTMU5(k zT9}fmau1J%`gIz0|3dYfB7aZE5uzthvwntmK5zIy_$IJ$P*PCiKa#QS6_-i{!luzo zj1ZV6HfiEx;jGTOb^U=6lpjO}5fr$DV8x_)ubH+6J7dGnh@b;mIm7SL5CF|B1NCD2MkWm5CoD31Z}f8{lU8wNKGcxv)$3rF-gw z@b#@s2r_R6l4_~tu){j?JFb-hi4|ikeXP}c@&+bH)3mYHza*AG1@bt(HYy73k~>)D z_?uABCa(Ig3l3fL|9TT_D$jHbN&+dy?#~y$is*LqU&;(7AL;|SD={hezk+4P zp23qcuBs(KNRA?@&0@$bxmojXQT*%RioiJsG~!2zl$OKuE7HoxjGGUPMz5OayX7fL zR;i&Tm`Ep;38x_7`AV#GivRpvs?+CDrW1|G8Q4^D`f%Q4gs2)I_@K*)t~|_&%yYP)M(^LL&r? zXxX(uQTvMM*xq-{pt+8e!lNVE4!v4!O%;@w=?8o=CcI>bjRn3EO9t#`9=D6B(zQM3 zl>a_8Tavp*Vq-@rk_e@)1?WzEd9vx4ofMXwy@=X}E*Zcgj{bT`4|(#A8+=7+m`bps zzo6$xsNSIIx>N?>d&d%Ep7r(F&t+L9A}21_r_kY;6AdMC@@9)8U@MSe2X_2GRNNT(GXQt#jSM-vSb&vZR5!!Y+EW`!+8S`sKoYYx znk`^QFQ^w-1T$?xYa*kHg`Pn4u>$ivg?jaPh?Fn4XA@flzMhCDlHv1s&$eo0PHf$g z@fe~rx5}k2Zza9ry0!UsjQ*S2?zkyKReswUoKU|hl*@4>FSOw5h4OVjHlF~6pTV(L z(vNuquDs-JWqDq4*n_`qJzenVKY`E6WreJ2-5KPsT+*{LTJybDBiV$kdCPXZFcvah zi@w-$kff|z{9I2hP&I2IwkDNUruf}bjTBvoG(0w5I<-Llr*5hi$_@<>IIP|QjSs=s zlpzgVe$&r5-4xQuqZ#hKq)g3HrdH={NO8kBee^i=V8bQoI3k! z3xU1mgMPQy!!)c`;Jzy{Ymy?wyas|?0RUHOToMIZ+EjWl}%joG!{Ehrwd}|&c~tajgMbeC;`*0y%BTVkuR=? zfdDypqei44?#*YD%+`GlE=%Xu)|YfMJB_`QM2X>q} zEbVCF(rxEf2xU`pm?iB-WosduX~g&3rQz`u2d>*OFEbyaL36ij=9Nx z2lrwb7J!EJuqW~k{(Y#&Y(LSa9Ee z^rQt9Dq3GW4DHLwwcZqN=9rME)t8oHri3o-kys(5KZfN{$xlO${E-H<^kKX}24GFR z*mORW%#kC=J5RdC>0eKM?I)|M9qM|vDtRSU?c@Pv>dEZ?z0E>l<+$06I~Jz-l*cwCMnFW`0w%ae16EcJFW8z-WshY`@f6wXizF z1(o)dv`!r&ULOzEte?pKg=(>3j4Y(PBy4p{e0Mr#`RSn8{uBIU zDf`b-5%r^+q^o-3GnOb#QDZl?=}X*-AWA9vH!Gc}z<&I?1vY?SmtB`BDReKlrYGVq zplEn`X`@7mQ>9+}|Lks!Y|#wMt_hm*8sPlb%5a3OHY-A-<{!P_l*D86(R?Hqx98OG zQs(cLOu|~Q{;}C1E@DW@8~wH)))x=UJ8_1KtPIWk)H8M9Nl<2G zz+K_}y^0cP#o#0+81SGNuA9J78@%4Z=a2J&oZTuA1XhT5disKrQN8EKYClxbUhP<^ z{A)>}34(AfLEiqh>*1~(2v9w=$;7ULWFM^~RBHF=R5zV!X!MqXf=$~m{Sz^bPSQ44Gc$whg(;iEIo)+E1-`?_d_!*NEeNADeesgOrEBbRwMtXCmtP6}>$X;{ViBh7Yre4q2^=<@PD?_^0126u4Z)DaZFL= z1Ys2 z(k{)R8vN+wO5#(iHsqsijEmjqKk}kk5L+GK$W&=lu!C48?AMSivpi1m4uU9bAb@p2 z43r8oL!vaTxnSSEzP^Awl10v-%C*+xLO%B$T}I0uPD_aN(ASRvq3$8B&2uSlq)W9<(M5bA!-~3m4_)s<*P`AR5B?tU74~oA zo9~8Ew%Yf`_KWG=HH4byq-`@R5UEx+@{FlS+Q?2csd;X@$U%1}TG^(}x+XFQFVFu+$Xa1qX=ip>P1)2EFSUA+mPi_EcNDSX<|@+Vjn@5%+X7B+=@A-u7fy;lFn` zCs8ig1G*?3C7B$tIv~SLFum3s=D*So{x%g~ifqf_X6$Id|4vX)6*3T94Pxc}Y6QdY zhMR}4RprEc0wb*kevYg_)|woa2=7Xg*F0Tg&<@qM@cjKdZ5WgCI`);Kn?76Gfe+`Y zKJK*_d9OIRU)`)5N8h3Gr?qHzVn@YW22m5SQhetEHVdy<6#jO$=iQ135>+I}5vL<{ zxU#)FonJe)-)o1Z=)*D})}QM|mD-!T9sOr2#CgAfcb4Y367i-}!#Dnzh8#BTnPYOq z1@*zhqw2769;21UOF{1qp4`fy>*A@kRQ{ppREdkfX6TJ{S-ps;#VQsU)F?7)ypA{v zNNXZ5j7feK*vLV+$s=)79+}vepBv0D2%LV))2MuNQd^hS&jAkc5*Y1-9Jk)|X}`7* z0{duQ;&Vxmq%l(+Sa@Ua0-#hMnB;}%^}VS3GLH`4qkwyA&OFT=}&kcu*u%pU=XFwKVXf zo&di}Z>0=|wcv=Hg!RO_G)hO2S8FbydB#p$>s_;SZKHguDyVmS`07k%?u)A;7l{jFCZQ4OD&0SKfHT+S!Ha#nYj%CoAmx;p}ZB069 z+DqIUH6-OB;`eCfDNK*6%5x+Kk~lNdkJg~?CD?!(PR(#dMY5G5$&dS^JBAIomse65 z&c5A_{FJAVu>Ee=8cE5mz~IvZvVs({((S5!*%5L%yge8sU}LC9M9-PN3?iBJVLoyI zd7jC%J5Cd~6#xw?KXoj5-Q-t}u1QtqszD|Q2g3a7s2OE8BKRO5YIaJE!R@50;9he7 zrW^bk*S6TU)7SzPik7?IbM4yhxHFk@73zND2pa~4Wu^d!493y)CVH*)_%SzyWPlds z3N961$_UFs(Pv~b%Hq&QJ_62d+Nd19?=`s?YuN-l)u`3u%DDHI7H*tha)MH5S0a7%+#sL)C39p>dF3v~OAhHr~ zd713}&6%>T`9^{5DmMb|^`j5fBv6!w$F7fT4b!;!{_$&cCf!xE)91N7Vjr64{T4R0 z<;I@>-=Xz5@r`H;W_3WRlfJ`7Jo%ir0>+qB4j>o2`1MQpFU(d{+7iB?`1s?wO+ciH9oMWEYkbw zDgPQkP^n)zlY98KKljGixo}d9KVc4+tZTrX!tQ(WwOfOazt>!tjNvbiOYTi(`}P6t zf)t{D=m5f~F>*YsjY0^If3pLkKtH%c)i$8tHeKT$xeZgBGGiZ;nok6%LK`KZ`U8Iu4mX4U6zv_0BON1|9z zq{}D@Z~n9_#A`ApK?z`pPwtiO-gNWbBNqkyLy=d6FoScoZXqmZyCveq+FqX(lfTJ` zmN}Y`TIgy?5U$x>f(-uIM`J7?yIcsg8(dleYRR7{*{Pz%6_L>Bs+jerBG^8H5a!K; z^s89vR+Ps5s_Gkc^mzhxyOnJf7zf;IANOkI;LoxLK5GzW>mcm%W7s)$FPLV(nEHv+ z@US0jKyhR}X(?w;l@9bQBv@BQnT5^>&8wo#W5zRl?`FLja1gV46S&+ zlQ`P8|7-CQF!;l(cGq7qp4-_lRRDnwKhbmgnL2Q32Ea065H;g*+P>8q9W;YDy?v6a zr1h}5jn{MB%w7PS$ zg^>Q}>ZxXDXCpZ6=~g=&wJgVCiuStr+j?H;>q=j^Z0it#(syxt$*npm^BqFhU<)omIqP%@%L{U+zRd4Fxdhg zaNn`5IDm6dE8d(|k)NL5rvNIl5vq6#P}zh7AvG8PsrFbj(R-Y)fWq-IW{A&8YB^*X z-Ns9aE(X0><)BWrAHV>v=ToWqr8j+nF8_K;aaVG=$EH6exAT`W5is7t!Y1;1AcXzT zQ;Oqa%3{EDEn&>_TC4#)h19uo9KP2oJdCOSfOg}HjD zkz8-5FCfw~&nE@+w<()tRjBBQxsO#lc9&hnmQ6>*XVAq} zj1NHlT7~tiQuJMdQjyu)?}>XxTlejDW!w54BVWxKRsp?-rXC9HHG&>yDLW|!?aUi} z%WohT@`q!7O zn2D|K;_jhZ?%K~{hckQ?EGNf0ECw8iJ>1j^-3$K=vW+VEaMwkdNrKhk}%U#bMh? zwEjP#s6EI&hG`oJpM7DT@6pEJFv3G8w7eg@?z@UkEtc`-;q!pfmMXkm-f>>iElw5f zlz~U)?qr16IaXLGE;cB&KPL))uWX#h`D-Ac%H-~C&ISL}2n){TpXw6)XJ$GbOu+si zF-TA;#auTS+kM(dY2SaTJR}vW9kP59?A`@600@*qKN(ff+(?OhMUHg`Cm89WssRn! zlG+SP!+NWbI)*Yy)QGt=?gk{>?pcGB57iA9u$fjklkT8T%y4ejTC+8GEjb~T=X><| zR&znpADKsF!T&J!QRqdL2A6kBV3t&zmW&=7e$ADR$F0oJX_ZZKgBjU})_m zZakXqHx0kt`gHRgpE7nUo_?44KYbB(=hpB=Ga!N9aSji?1~u>*iL32c%OGX1^})5U zY_h)!Hy_(~ild%i+|)x%bWVoM?53!y^r5dU({KSm|F}fTo_+Q9__(zU){YTyx`RSiP|8$qdxR2a=)NyUv@CF5ZXtN2yM6nirAi#JOsuQp zI3kE82XkHQuOO0^5cgSZw@<7U! zBKahAY!Yu95CFb+BK{{Kd#-cS_C-aw`J^kLSo(RbH1vCK6so3l4eK0iUS7NDjh{Mw z6aUdWgQ^|$k0q9IK8bcB{c&b{rj0a@MDZeLP@Q$kY!gtrRMU-0sc z;!v5wTJYKO{eQx=K1gjrl-+zCTj*&J*nj$^WL^A~30P?{+9MoTFJG1mFgHX0#BFLZ zO+nf)BB>x-d#Mz9F;_!HjQy{t0fXjFUs`q0D+dVrq~^%s1E#*sDBOG;CK;N>T+2bV zl*N)Bh*X3z{1Ilrqbe)3j~T6yTDOaQI~(;>6Qw%-&vdBEi-=2>_JKnKkVME|4{=R^ zJ8nsv+J%k2@IGnXVjGxds;wfPXe>Kn5a=YJPdvC|A|_Y2N4az72csXFG}I6Aq^CGG Yap41YY~$qETk)3=8zMG`0i2sU_Bvk8T>t<8 diff --git a/languages/sv-pg.ts b/languages/sv-pg.ts new file mode 100644 index 0000000..2a0ec82 --- /dev/null +++ b/languages/sv-pg.ts @@ -0,0 +1,11 @@ +import { LanguageDefinition } from "./types"; +import sv from "./sv"; + +const svPg: LanguageDefinition = { + ...sv, + code: "sv-pg", + displayName: "Svenska (PostgreSQL)", + engine: "postgresql", +}; + +export default svPg; diff --git a/languages/types.ts b/languages/types.ts index c87566d..4b48343 100644 --- a/languages/types.ts +++ b/languages/types.ts @@ -39,6 +39,8 @@ export interface LanguageDefinition { code: string; /** Display name in the language itself, e.g. "Svenska", "English" */ displayName: string; + /** Database engine: "sqlite" (default) or "postgresql" */ + engine?: "sqlite" | "postgresql"; // ── Data: Names ────────────────────────────────────────────── /** First names pool (min 15, indexed by canonical person slot) */ diff --git a/package-lock.json b/package-lock.json index c1dd6e8..ffc1b2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "sql-validator", "version": "1.0.0", "dependencies": { + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@electric-sql/pglite": "^0.4.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -25,6 +28,7 @@ "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/sql.js": "^1.4.9", + "@uiw/react-codemirror": "^4.25.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "crypto-js": "^4.2.0", @@ -34,7 +38,6 @@ "prismjs": "^1.30.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-simple-code-editor": "^0.13.1", "react-syntax-highlighter": "^16.1.0", "sql-formatter": "^15.3.1", "sql.js": "^1.10.3", @@ -472,6 +475,113 @@ "specificity": "bin/cli.js" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -612,6 +722,12 @@ "node": ">=20.19.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -1526,6 +1642,36 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3403,6 +3549,59 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.8", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz", + "integrity": "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.8", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz", + "integrity": "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.8", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4419,6 +4618,21 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4597,6 +4811,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9006,16 +9226,6 @@ } } }, - "node_modules/react-simple-code-editor": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", - "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -10129,6 +10339,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -11050,6 +11266,12 @@ "dev": true, "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 8db643e..73f151b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "dependencies": { + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@electric-sql/pglite": "^0.4.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -20,6 +23,7 @@ "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/sql.js": "^1.4.9", + "@uiw/react-codemirror": "^4.25.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "crypto-js": "^4.2.0", @@ -29,7 +33,6 @@ "prismjs": "^1.30.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-simple-code-editor": "^0.13.1", "react-syntax-highlighter": "^16.1.0", "sql-formatter": "^15.3.1", "sql.js": "^1.10.3", diff --git a/public/languages/en-pg/data.sql b/public/languages/en-pg/data.sql new file mode 100644 index 0000000..80ad349 --- /dev/null +++ b/public/languages/en-pg/data.sql @@ -0,0 +1,141 @@ +CREATE TABLE Person (person_id TEXT NOT NULL, name TEXT NOT NULL, address TEXT NOT NULL, postal_code TEXT NOT NULL, city TEXT NOT NULL, phone TEXT NOT NULL, PRIMARY KEY (person_id)); +CREATE TABLE Room (id INTEGER NOT NULL, name TEXT NOT NULL, capacity INTEGER NOT NULL, PRIMARY KEY (id)); +CREATE UNIQUE INDEX rum_namn ON Room (name); +CREATE TABLE Student (person_id TEXT NOT NULL, has_disability BOOLEAN NOT NULL, CONSTRAINT STUDENT_PK PRIMARY KEY (person_id), CONSTRAINT Student_Person_FK FOREIGN KEY (person_id) REFERENCES Person (person_id) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Teacher (person_id TEXT NOT NULL, office_room INTEGER NOT NULL, CONSTRAINT teacher_pk PRIMARY KEY (person_id), CONSTRAINT fk_teacher_person FOREIGN KEY (person_id) REFERENCES Person (person_id) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Course (course_code TEXT NOT NULL, name TEXT NOT NULL, duration INTEGER NOT NULL, price INTEGER NOT NULL, description TEXT NOT NULL, PRIMARY KEY (course_code)); +CREATE UNIQUE INDEX kurs_ben ON Course (name); +CREATE TABLE CourseInstance (course TEXT NOT NULL, start_date DATE NOT NULL, teacher TEXT NOT NULL, room INTEGER NOT NULL, CONSTRAINT courseinstance_pk PRIMARY KEY (course, start_date), CONSTRAINT fk_courseinstance_course FOREIGN KEY (course) REFERENCES Course (course_code) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_teacher FOREIGN KEY (teacher) REFERENCES Teacher (person_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_room FOREIGN KEY (room) REFERENCES Room (id) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Participation (student TEXT NOT NULL, course TEXT NOT NULL, start_date DATE NOT NULL, CONSTRAINT participation_pk PRIMARY KEY (course, start_date, student), CONSTRAINT fk_participation_courseinstance FOREIGN KEY (course, start_date) REFERENCES CourseInstance (course, start_date) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_participation_student FOREIGN KEY (student) REFERENCES Student (person_id) ON DELETE RESTRICT ON UPDATE CASCADE); +INSERT INTO Person VALUES ('EN-001', 'James Smith', 'Main Street 1', 'GU1 1AA', 'Guildford', '01234567890'); +INSERT INTO Person VALUES ('EN-002', 'John Brown', 'Oak Avenue 2', 'RG1 2AB', 'Reading', '01234567891'); +INSERT INTO Person VALUES ('EN-003', 'William Jones', 'Park Road 3', 'OX1 3AC', 'Oxford', '01234567892'); +INSERT INTO Person VALUES ('EN-004', 'Mary Wilson', 'High Street 4', 'CB2 4AD', 'Cambridge', '01234567893'); +INSERT INTO Person VALUES ('EN-005', 'Sarah Taylor', 'Mill Lane 5', 'BN1 5AE', 'Oxford', '01234567894'); +INSERT INTO Person VALUES ('EN-006', 'John Clark', 'Church Road 6', 'BS1 6AF', 'Brighton', '01234567895'); +INSERT INTO Person VALUES ('EN-007', 'Anne Hall', 'Elm Drive 3', 'YO1 7AG', 'Bristol', '01234567896'); +INSERT INTO Person VALUES ('EN-008', 'Emily Ford', 'River Road 4', 'BS2 8AH', 'Brighton', '01234567897'); +INSERT INTO Person VALUES ('EN-009', 'Robert Stone', 'Lake Drive 1', 'YO2 9AI', 'Bristol', '01234567898'); +INSERT INTO Person VALUES ('EN-010', 'David Wood', 'School Lane 3', 'BA1 1AJ', 'York', '01234567899'); +INSERT INTO Person VALUES ('EN-011', 'Susan Blair', 'Birch Way 2', 'YO3 2AK', 'Bristol', '01234567900'); +INSERT INTO Person VALUES ('EN-012', 'George Hunt', 'Birch Way 24', 'YO4 3AL', 'Bristol', '01234567901'); +INSERT INTO Person VALUES ('EN-013', 'Peter Grant', 'Hill Road 112', 'OX2 4AM', 'Bath', '01234567902'); +INSERT INTO Person VALUES ('EN-014', 'Jane Bell', 'Bridge Street 5', 'BA2 5AN', 'York', '01234567903'); +INSERT INTO Person VALUES ('EN-015', 'Laura Miles', 'Valley Road 3', 'CB3 6AO', 'Cambridge', '01234567904'); +INSERT INTO Person VALUES ('EN-016', 'Alice Cole', 'King Street 44', 'OX3 7AP', 'Oxford', '01234567905'); +INSERT INTO Person VALUES ('EN-017', 'Jane Palmer', 'Harbour Lane 15b', 'OX4 8AQ', 'Oxford', '01234567906'); +INSERT INTO Person VALUES ('EN-018', 'William White', 'Station Road 4', 'BA3 9AR', 'York', '01234567907'); +INSERT INTO Person VALUES ('EN-019', 'Jane Bell', 'School Lane 12', 'BA4 1AS', 'York', '01234567908'); +INSERT INTO Person VALUES ('EN-020', 'David Wood', 'School Lane 1', 'BA5 2AT', 'York', '01234567909'); +INSERT INTO Person VALUES ('EN-021', 'John Clark', 'High Street 63', 'CB4 3AU', 'Cambridge', '01234567910'); +INSERT INTO Room VALUES (1, 'Jupiter', 12); +INSERT INTO Room VALUES (2, 'Orion', 24); +INSERT INTO Room VALUES (3, 'Sirius', 16); +INSERT INTO Room VALUES (4, 'Tellus', 32); +INSERT INTO Room VALUES (5, 'Saturn', 24); +INSERT INTO Student VALUES ('EN-001', FALSE); +INSERT INTO Student VALUES ('EN-006', FALSE); +INSERT INTO Student VALUES ('EN-007', TRUE); +INSERT INTO Student VALUES ('EN-008', FALSE); +INSERT INTO Student VALUES ('EN-009', FALSE); +INSERT INTO Student VALUES ('EN-010', FALSE); +INSERT INTO Student VALUES ('EN-011', TRUE); +INSERT INTO Student VALUES ('EN-012', FALSE); +INSERT INTO Student VALUES ('EN-013', FALSE); +INSERT INTO Student VALUES ('EN-014', FALSE); +INSERT INTO Student VALUES ('EN-015', TRUE); +INSERT INTO Student VALUES ('EN-016', FALSE); +INSERT INTO Student VALUES ('EN-017', TRUE); +INSERT INTO Student VALUES ('EN-018', FALSE); +INSERT INTO Student VALUES ('EN-019', FALSE); +INSERT INTO Student VALUES ('EN-020', TRUE); +INSERT INTO Student VALUES ('EN-021', FALSE); +INSERT INTO Teacher VALUES ('EN-001', 634); +INSERT INTO Teacher VALUES ('EN-002', 604); +INSERT INTO Teacher VALUES ('EN-003', 603); +INSERT INTO Teacher VALUES ('EN-004', 605); +INSERT INTO Teacher VALUES ('EN-005', 622); +INSERT INTO Teacher VALUES ('EN-018', 558); +INSERT INTO Course VALUES ('DBM1', 'Database Methodology', 2, 2800, 'DB basics, normalisation, modelling, SQL'); +INSERT INTO Course VALUES ('PDB', 'Physical Database Design', 5, 7200, 'Storage structures, indexing, denormalisation'); +INSERT INTO Course VALUES ('Java1', 'Java, Basic Course', 5, 6700, 'Basic programming with Java'); +INSERT INTO Course VALUES ('Java2', 'Java, Advanced Course', 4, 6000, 'Advanced programming with Java'); +INSERT INTO Course VALUES ('LDD', 'Logical Database Design', 4, 6000, 'Advanced design, analysis patterns, metamodelling'); +INSERT INTO Course VALUES ('LOG1', 'Logic', 3, 4500, 'First order logic, predicate logic, propositional logic'); +INSERT INTO CourseInstance VALUES ('Java1', '2008-03-06T00:00:00.000Z', 'EN-002', 4); +INSERT INTO CourseInstance VALUES ('DBM1', '2008-04-02T00:00:00.000Z', 'EN-005', 3); +INSERT INTO CourseInstance VALUES ('Java2', '2008-04-16T00:00:00.000Z', 'EN-002', 2); +INSERT INTO CourseInstance VALUES ('LOG1', '2008-04-16T00:00:00.000Z', 'EN-003', 1); +INSERT INTO CourseInstance VALUES ('Java1', '2008-05-06T00:00:00.000Z', 'EN-002', 1); +INSERT INTO CourseInstance VALUES ('LDD', '2008-05-06T00:00:00.000Z', 'EN-005', 3); +INSERT INTO CourseInstance VALUES ('PDB', '2008-05-09T00:00:00.000Z', 'EN-004', 1); +INSERT INTO CourseInstance VALUES ('Java1', '2008-09-02T00:00:00.000Z', 'EN-003', 4); +INSERT INTO CourseInstance VALUES ('LDD', '2008-09-02T00:00:00.000Z', 'EN-004', 2); +INSERT INTO CourseInstance VALUES ('Java2', '2009-01-22T00:00:00.000Z', 'EN-002', 2); +INSERT INTO CourseInstance VALUES ('DBM1', '2009-01-28T00:00:00.000Z', 'EN-005', 1); +INSERT INTO CourseInstance VALUES ('LOG1', '2009-09-17T00:00:00.000Z', 'EN-005', 1); +INSERT INTO CourseInstance VALUES ('DBM1', '2010-02-15T00:00:00.000Z', 'EN-004', 3); +INSERT INTO CourseInstance VALUES ('Java2', '2010-04-11T00:00:00.000Z', 'EN-018', 4); +INSERT INTO CourseInstance VALUES ('Java1', '2010-05-01T00:00:00.000Z', 'EN-003', 5); +INSERT INTO CourseInstance VALUES ('LOG1', '2010-05-01T00:00:00.000Z', 'EN-002', 1); +INSERT INTO CourseInstance VALUES ('DBM1', '2010-09-22T00:00:00.000Z', 'EN-018', 3); +INSERT INTO CourseInstance VALUES ('LDD', '2010-09-24T00:00:00.000Z', 'EN-004', 5); +INSERT INTO CourseInstance VALUES ('Java1', '2011-05-01T00:00:00.000Z', 'EN-002', 5); +INSERT INTO Participation VALUES ('EN-006', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-008', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-014', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-015', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-019', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-006', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-007', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-008', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-015', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-007', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-010', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-012', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-009', 'LOG1', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-010', 'LOG1', '2008-04-16T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-007', 'Java1', '2008-05-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'Java1', '2008-05-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-006', 'LDD', '2008-05-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-012', 'LDD', '2008-05-06T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-008', 'PDB', '2008-05-09T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'PDB', '2008-05-09T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'PDB', '2008-05-09T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-006', 'Java1', '2008-09-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-014', 'Java1', '2008-09-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'LDD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'LDD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-018', 'LDD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-007', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-014', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-015', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-019', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-008', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-010', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-019', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-009', 'LOG1', '2009-09-17T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-017', 'LOG1', '2009-09-17T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-008', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-015', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-009', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-010', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-012', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-015', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-011', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-012', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-010', 'LOG1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'LOG1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-016', 'LOG1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-021', 'LOG1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-009', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-016', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-017', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-020', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-013', 'LDD', '2010-09-24T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-016', 'LDD', '2010-09-24T00:00:00.000Z'); +INSERT INTO Participation VALUES ('EN-017', 'LDD', '2010-09-24T00:00:00.000Z'); \ No newline at end of file diff --git a/public/languages/en-pg/db_layout_dark.svg b/public/languages/en-pg/db_layout_dark.svg new file mode 100644 index 0000000..183472e --- /dev/null +++ b/public/languages/en-pg/db_layout_dark.svg @@ -0,0 +1,263 @@ + + + + +SQLiteLayout + + +course + + + +course + + +course_code +   +TEXT + + +name +          +TEXT + + +duration +      +INTEGER + + +price +         +INTEGER + + +description +   +TEXT + + +    name (INDEX, UNIQUE)     + + + + +courseinstance + + + +courseinstance + + +course +       +TEXT + + +start_date +   +DATE + + +teacher +      +TEXT + + +room +         +INTEGER + + +    course, start_date, course, start_date     + + + + +courseinstance:f7->course:f1 + + +1 + + + + +room + + + +room + + +id +         +INTEGER + + +name +       +TEXT + + +capacity +   +INTEGER + + +    name (INDEX, UNIQUE)     + + + + +courseinstance:f10->room:f22 + + +1 + + + + +teacher + + + +teacher + + +person_id +     +TEXT + + +office_room +   +INTEGER + + + + +courseinstance:f9->teacher:f28 + + +1 + + + + +participation + + + +participation + + +student +      +TEXT + + +course +       +TEXT + + +start_date +   +DATE + + +    course, course, start_date, start_date     + + + + +participation:f15->courseinstance:f11 + + +1 + + + + +student + + + +student + + +person_id +        +TEXT + + +has_disability +   +BOOLEAN + + + + +participation:f12->student:f26 + + +1 + + + + +person + + + +person + + +person_id +     +TEXT + + +name +          +TEXT + + +address +       +TEXT + + +postal_code +   +TEXT + + +city +          +TEXT + + +phone +         +TEXT + + + + +student:f26->person:f16 + + +1 +1 + + + +teacher:f28->person:f16 + + +1 +1 + + + \ No newline at end of file diff --git a/public/languages/en-pg/db_layout_light.svg b/public/languages/en-pg/db_layout_light.svg new file mode 100644 index 0000000..b4a9bff --- /dev/null +++ b/public/languages/en-pg/db_layout_light.svg @@ -0,0 +1,263 @@ + + + + +SQLiteLayout + + +course + + + +course + + +course_code +   +TEXT + + +name +          +TEXT + + +duration +      +INTEGER + + +price +         +INTEGER + + +description +   +TEXT + + +    name (INDEX, UNIQUE)     + + + + +courseinstance + + + +courseinstance + + +course +       +TEXT + + +start_date +   +DATE + + +teacher +      +TEXT + + +room +         +INTEGER + + +    course, start_date, course, start_date     + + + + +courseinstance:f7->course:f1 + + +1 + + + + +room + + + +room + + +id +         +INTEGER + + +name +       +TEXT + + +capacity +   +INTEGER + + +    name (INDEX, UNIQUE)     + + + + +courseinstance:f10->room:f22 + + +1 + + + + +teacher + + + +teacher + + +person_id +     +TEXT + + +office_room +   +INTEGER + + + + +courseinstance:f9->teacher:f28 + + +1 + + + + +participation + + + +participation + + +student +      +TEXT + + +course +       +TEXT + + +start_date +   +DATE + + +    course, course, start_date, start_date     + + + + +participation:f15->courseinstance:f11 + + +1 + + + + +student + + + +student + + +person_id +        +TEXT + + +has_disability +   +BOOLEAN + + + + +participation:f12->student:f26 + + +1 + + + + +person + + + +person + + +person_id +     +TEXT + + +name +          +TEXT + + +address +       +TEXT + + +postal_code +   +TEXT + + +city +          +TEXT + + +phone +         +TEXT + + + + +student:f26->person:f16 + + +1 +1 + + + +teacher:f28->person:f16 + + +1 +1 + + + \ No newline at end of file diff --git a/public/languages/en-pg/questionpool.json b/public/languages/en-pg/questionpool.json new file mode 100644 index 0000000..b1ff5b4 --- /dev/null +++ b/public/languages/en-pg/questionpool.json @@ -0,0 +1 @@ +{"language":"en","engine":"postgresql","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Retrieve person_id, name, address, and postal_code for people living in York!","display_sequence":"A","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-010","David Wood","School Lane 3","BA1 1AJ"],["EN-014","Jane Bell","Bridge Street 5","BA2 5AN"],["EN-018","William White","Station Road 4","BA3 9AR"],["EN-019","Jane Bell","School Lane 12","BA4 1AS"],["EN-020","David Wood","School Lane 1","BA5 2AT"]]}},{"id":2,"description":"Retrieve person_id, name, and phone for people living in York!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":3,"description":"Retrieve person_id, name, address, and postal_code for people living in Bristol!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-007","Anne Hall","Elm Drive 3","YO1 7AG"],["EN-009","Robert Stone","Lake Drive 1","YO2 9AI"],["EN-011","Susan Blair","Birch Way 2","YO3 2AK"],["EN-012","George Hunt","Birch Way 24","YO4 3AL"]]}},{"id":4,"description":"Retrieve person_id, name, and phone for people living in Bristol!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":5,"description":"Retrieve name, address, postal_code, and phone for people living in York!","display_sequence":"E","result":{"columns":["name","address","postal_code","phone"],"values":[["David Wood","School Lane 3","BA1 1AJ","01234567899"],["Jane Bell","Bridge Street 5","BA2 5AN","01234567903"],["William White","Station Road 4","BA3 9AR","01234567907"],["Jane Bell","School Lane 12","BA4 1AS","01234567908"],["David Wood","School Lane 1","BA5 2AT","01234567909"]]}},{"id":6,"description":"Retrieve name, address, postal_code, and phone for people living in Bristol!","display_sequence":"F","result":{"columns":["name","address","postal_code","phone"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","01234567896"],["Robert Stone","Lake Drive 1","YO2 9AI","01234567898"],["Susan Blair","Birch Way 2","YO3 2AK","01234567900"],["George Hunt","Birch Way 24","YO4 3AL","01234567901"]]}},{"id":7,"description":"Retrieve course_code, name, and price for courses of 3 weeks!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["LOG1","Logic",4500]]}},{"id":8,"description":"Retrieve course_code, name, and price for courses of 4 weeks!","display_sequence":"H","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Retrieve person_id, name, and city for people living in York or in Bristol!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-014","Jane Bell","York"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"]]}},{"id":10,"description":"Retrieve person_id, name, and phone for people living in York or in Brighton!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":11,"description":"Retrieve person_id, name, and city for people living in Bristol or in Cambridge!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-021","John Clark","Cambridge"]]}},{"id":12,"description":"Retrieve person_id, name, and phone for people living in Bristol or in Brighton!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-007","Anne Hall","01234567896"],["EN-008","Emily Ford","01234567897"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":13,"description":"Retrieve name, address, postal_code, and city for people living in York or in Cambridge!","display_sequence":"E","result":{"columns":["name","address","postal_code","city"],"values":[["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"],["John Clark","High Street 63","CB4 3AU","Cambridge"]]}},{"id":14,"description":"Retrieve name, address, postal_code, and city for people living in Bristol or in York!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":15,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Brighton!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-004","Mary Wilson","01234567893"],["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-015","Laura Miles","01234567904"],["EN-021","John Clark","01234567910"]]}},{"id":16,"description":"Retrieve person_id, name, and city for people living in York or in Cambridge!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-010","David Wood","York"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"],["EN-021","John Clark","Cambridge"]]}},{"id":17,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Oxford!","display_sequence":"I","result":{"columns":["person_id","name","phone"],"values":[["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-015","Laura Miles","01234567904"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-021","John Clark","01234567910"]]}},{"id":18,"description":"Retrieve name, address, postal_code, and city for people living in York or in Oxford!","display_sequence":"J","result":{"columns":["name","address","postal_code","city"],"values":[["William Jones","Park Road 3","OX1 3AC","Oxford"],["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":19,"description":"Retrieve course_code, name, and price for courses of 3 or 4 weeks!","display_sequence":"K","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000],["LOG1","Logic",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Retrieve person_id for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["teacher"],"values":[["EN-002"],["EN-003"],["EN-004"],["EN-005"]]}},{"id":21,"description":"Retrieve person_id for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["teacher"],"values":[["EN-002"],["EN-004"]]}},{"id":22,"description":"Retrieve person_id for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["teacher"],"values":[["EN-004"],["EN-005"],["EN-018"]]}},{"id":23,"description":"Retrieve person_id for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["teacher"],"values":[["EN-002"],["EN-003"]]}},{"id":24,"description":"Retrieve person_id for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["teacher"],"values":[["EN-002"],["EN-018"]]}},{"id":25,"description":"Retrieve person_id for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["teacher"],"values":[["EN-004"],["EN-005"],["EN-018"]]}},{"id":26,"description":"Retrieve person_id for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["teacher"],"values":[["EN-004"],["EN-005"]]}},{"id":27,"description":"Retrieve person_id for students who have participated in the course with code Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-011"],["EN-012"],["EN-013"],["EN-014"],["EN-015"],["EN-019"]]}},{"id":28,"description":"Retrieve person_id for students who have participated in the course with code DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-009"],["EN-010"],["EN-011"],["EN-015"],["EN-016"],["EN-017"],["EN-019"],["EN-020"]]}},{"id":29,"description":"Retrieve person_id for students who have participated in the course with code Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["EN-007"],["EN-009"],["EN-010"],["EN-011"],["EN-012"],["EN-014"],["EN-015"],["EN-019"]]}},{"id":30,"description":"Retrieve person_id for students who have participated in the course with code LDD!","display_sequence":"K","result":{"columns":["student"],"values":[["EN-006"],["EN-011"],["EN-012"],["EN-013"],["EN-016"],["EN-017"],["EN-018"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Retrieve person_id, name, and phone for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":32,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":33,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-004","Mary Wilson","High Street 4","CB2 4AD"],["EN-005","Sarah Taylor","Mill Lane 5","BN1 5AE"],["EN-018","William White","Station Road 4","BA3 9AR"]]}},{"id":34,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"]]}},{"id":35,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-018","William White","York"]]}},{"id":36,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["William White","Station Road 4","BA3 9AR","York"]]}},{"id":37,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":38,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 1!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-003","William Jones","Oxford"],["EN-004","Mary Wilson","Cambridge"],["EN-005","Sarah Taylor","Oxford"]]}},{"id":39,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 2!","display_sequence":"I","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-002","John Brown","Oak Avenue 2","RG1 2AB"],["EN-004","Mary Wilson","High Street 4","CB2 4AD"]]}},{"id":40,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java2!","display_sequence":"J","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-018","William White","01234567907"]]}},{"id":41,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code LDD!","display_sequence":"K","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-005","Sarah Taylor","Oxford"]]}},{"id":42,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code Java1!","display_sequence":"L","result":{"columns":["name","address","postal_code","city"],"values":[["John Brown","Oak Avenue 2","RG1 2AB","Reading"],["William Jones","Park Road 3","OX1 3AC","Oxford"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-003","William Jones",603],["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}},{"id":44,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-004","Mary Wilson",605]]}},{"id":45,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622],["EN-018","William White",558]]}},{"id":46,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-003","William Jones",603]]}},{"id":47,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-018","William White",558]]}},{"id":48,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["person_id","name","office_room"],"values":[["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622],["EN-018","William White",558]]}},{"id":49,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","office_room"],"values":[["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Retrieve person_id for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["EN-007"],["EN-008"],["EN-009"],["EN-010"],["EN-011"],["EN-013"],["EN-016"],["EN-017"],["EN-019"],["EN-021"]]}},{"id":51,"description":"Retrieve person_id for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["EN-007"],["EN-010"],["EN-011"],["EN-012"],["EN-013"],["EN-014"],["EN-015"],["EN-018"],["EN-019"]]}},{"id":52,"description":"Retrieve person_id for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["EN-006"],["EN-008"],["EN-009"],["EN-010"],["EN-012"],["EN-013"],["EN-014"],["EN-015"],["EN-019"]]}},{"id":53,"description":"Retrieve person_id for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-009"],["EN-011"],["EN-012"],["EN-015"],["EN-016"],["EN-017"],["EN-020"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-008","Emily Ford","01234567897"],["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-011","Susan Blair","01234567900"],["EN-013","Peter Grant","01234567902"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-019","Jane Bell","01234567908"],["EN-021","John Clark","01234567910"]]}},{"id":55,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-018","William White","York"],["EN-019","Jane Bell","York"]]}},{"id":56,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"],["EN-013","Peter Grant","01234567902"],["EN-014","Jane Bell","01234567903"],["EN-015","Laura Miles","01234567904"],["EN-019","Jane Bell","01234567908"]]}},{"id":57,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-020","David Wood","York"]]}},{"id":58,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"E","result":{"columns":["name","address","city","phone"],"values":[["Alice Cole","King Street 44","Oxford","01234567905"],["Anne Hall","Elm Drive 3","Bristol","01234567896"],["David Wood","School Lane 3","York","01234567899"],["Emily Ford","River Road 4","Brighton","01234567897"],["Jane Bell","School Lane 12","York","01234567908"],["Jane Palmer","Harbour Lane 15b","Oxford","01234567906"],["John Clark","High Street 63","Cambridge","01234567910"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Robert Stone","Lake Drive 1","Bristol","01234567898"],["Susan Blair","Birch Way 2","Bristol","01234567900"]]}},{"id":59,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Orion!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["Peter Grant","Hill Road 112","OX2 4AM","Bath"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["William White","Station Road 4","BA3 9AR","York"]]}},{"id":60,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Tellus!","display_sequence":"G","result":{"columns":["name","address","city","phone"],"values":[["David Wood","School Lane 3","York","01234567899"],["Emily Ford","River Road 4","Brighton","01234567897"],["George Hunt","Birch Way 24","Bristol","01234567901"],["Jane Bell","Bridge Street 5","York","01234567903"],["Jane Bell","School Lane 12","York","01234567908"],["John Clark","Church Road 6","Brighton","01234567895"],["Laura Miles","Valley Road 3","Cambridge","01234567904"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Robert Stone","Lake Drive 1","Bristol","01234567898"]]}},{"id":61,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Sirius!","display_sequence":"H","result":{"columns":["name","address","postal_code","city"],"values":[["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["David Wood","School Lane 1","BA5 2AT","York"],["Emily Ford","River Road 4","BS2 8AH","Brighton"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["John Clark","Church Road 6","BS1 6AF","Brighton"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Retrieve the number of course instances each student has participated in! Show the student's person_id, the student's name, and the count!","display_sequence":"A","result":{"columns":["person_id","name","count"],"values":[["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-013","Peter Grant",6],["EN-009","Robert Stone",4],["EN-019","Jane Bell",3],["EN-012","George Hunt",4],["EN-011","Susan Blair",6],["EN-014","Jane Bell",3],["EN-010","David Wood",5],["EN-015","Laura Miles",5],["EN-020","David Wood",1],["EN-001","James Smith",0],["EN-021","John Clark",1],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-016","Alice Cole",3],["EN-008","Emily Ford",5]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-013","Peter Grant",6],["EN-009","Robert Stone",4],["EN-019","Jane Bell",3],["EN-012","George Hunt",4],["EN-011","Susan Blair",6],["EN-014","Jane Bell",3],["EN-010","David Wood",5],["EN-015","Laura Miles",5],["EN-020","David Wood",1],["EN-021","John Clark",1],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-016","Alice Cole",3],["EN-008","Emily Ford",5]]}]},{"id":63,"description":"Retrieve the number of course instances each room has been used for! Show the room's name, the room's capacity, and the count!","display_sequence":"B","result":{"columns":["name","capacity","count"],"values":[["Orion",24,3],["Sirius",16,4],["Saturn",24,3],["Tellus",32,3],["Jupiter",12,6]]},"alternative_results":[{"columns":["name","capacity","count"],"values":[["Tellus",32,3],["Orion",24,3],["Sirius",16,4],["Saturn",24,3],["Jupiter",12,6]]}]},{"id":64,"description":"Retrieve the number of course instances each course has had! Show the course's code, the course's name, and the count!","display_sequence":"C","result":{"columns":["course_code","name","count"],"values":[["LDD","Logical Database Design",3],["Java2","Java, Advanced Course",3],["LOG1","Logic",3],["DBM1","Database Methodology",4],["PDB","Physical Database Design",1],["Java1","Java, Basic Course",5]]},"alternative_results":[{"columns":["course_code","name","count"],"values":[["LDD","Logical Database Design",3],["Java2","Java, Advanced Course",3],["LOG1","Logic",3],["DBM1","Database Methodology",4],["PDB","Physical Database Design",1],["Java1","Java, Basic Course",5]]}]},{"id":65,"description":"Retrieve the number of course instances each teacher has held! Show the teacher's person_id, the teacher's name, and the count!","display_sequence":"D","result":{"columns":["person_id","name","count"],"values":[["EN-005","Sarah Taylor",4],["EN-004","Mary Wilson",4],["EN-003","William Jones",3],["EN-001","James Smith",0],["EN-018","William White",2],["EN-002","John Brown",6]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-005","Sarah Taylor",4],["EN-004","Mary Wilson",4],["EN-003","William Jones",3],["EN-018","William White",2],["EN-002","John Brown",6]]}]},{"id":66,"description":"Retrieve the number of participating students for each course instance! Show the course instance's course_code and course name, the course instance's start_date, and the number of participating students!","display_sequence":"E","result":{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02",4],["DBM1","Database Methodology","2010-09-22",4],["Java1","Java, Basic Course","2010-05-01",3],["Java1","Java, Basic Course","2008-05-06",2],["LDD","Logical Database Design","2008-05-06",2],["LOG1","Logic","2008-04-16",2],["LDD","Logical Database Design","2008-09-02",3],["LOG1","Logic","2009-09-17",2],["Java2","Java, Advanced Course","2010-04-11",4],["Java2","Java, Advanced Course","2009-01-22",4],["DBM1","Database Methodology","2009-01-28",3],["Java1","Java, Basic Course","2008-03-06",6],["Java2","Java, Advanced Course","2008-04-16",4],["Java1","Java, Basic Course","2011-05-01",0],["LOG1","Logic","2010-05-01",4],["PDB","Physical Database Design","2008-05-09",3],["LDD","Logical Database Design","2010-09-24",3],["Java1","Java, Basic Course","2008-09-02",2],["DBM1","Database Methodology","2010-02-15",3]]},"alternative_results":[{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02",4],["DBM1","Database Methodology","2010-09-22",4],["Java1","Java, Basic Course","2010-05-01",3],["Java1","Java, Basic Course","2008-05-06",2],["LDD","Logical Database Design","2008-05-06",2],["LOG1","Logic","2008-04-16",2],["LDD","Logical Database Design","2008-09-02",3],["LOG1","Logic","2009-09-17",2],["Java2","Java, Advanced Course","2010-04-11",4],["Java2","Java, Advanced Course","2009-01-22",4],["DBM1","Database Methodology","2009-01-28",3],["Java1","Java, Basic Course","2008-03-06",6],["Java2","Java, Advanced Course","2008-04-16",4],["LOG1","Logic","2010-05-01",4],["PDB","Physical Database Design","2008-05-09",3],["LDD","Logical Database Design","2010-09-24",3],["Java1","Java, Basic Course","2008-09-02",2],["DBM1","Database Methodology","2010-02-15",3]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances each teacher has held! II. Retrieve person_id, name, and phone for the teacher who has held the most course instances!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"]]}},{"id":68,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances per student! II. Retrieve person_id, name, and phone for the student who has participated in the most course instances!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-013","Peter Grant","01234567902"],["EN-011","Susan Blair","01234567900"]]}},{"id":69,"description":"I. Retrieve course_code, name, and duration as well as the number of course instances per course! II. Retrieve course_code, name, and duration for the course that has had the most course instances!","display_sequence":"C","result":{"columns":["course_code","name","duration"],"values":[["Java1","Java, Basic Course",5]]}},{"id":70,"description":"I. Retrieve name and capacity as well as the number of course instances per room! II. Retrieve name and capacity for the room that has been used for the most course instances!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Retrieve course_code, course name, and start_date as well as the number of participants per course instance! II. Retrieve course_code, course name, and start_date for the course instance that has had the most participants!","display_sequence":"E","result":{"columns":["course","name","start_date"],"values":[["Java1","Java, Basic Course","2008-03-06"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Retrieve person_id, name, and city for students who have participated in at least three DIFFERENT courses!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-019","Jane Bell","York"]]}},{"id":73,"description":"Retrieve person_id, name, and phone for teachers who have held at least three DIFFERENT courses!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":74,"description":"Retrieve person_id, name, and city for teachers who have held course instances in at least three DIFFERENT rooms!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-003","William Jones","Oxford"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":75,"description":"Retrieve name and capacity for rooms that have been used by at least three DIFFERENT teachers!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturn",24]]}},{"id":76,"description":"Retrieve name and capacity for rooms that have been used for course instances of at least three DIFFERENT courses!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":77,"description":"Retrieve code, name, and price for courses that have been held in at least three DIFFERENT rooms!","display_sequence":"F","result":{"columns":["course_code","name","price"],"values":[["Java1","Java, Basic Course",6700],["LDD","Logical Database Design",6000]]}},{"id":78,"description":"Retrieve code, name, and price for courses that have been held by at least three DIFFERENT teachers!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["DBM1","Database Methodology",2800],["LOG1","Logic",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Retrieve name and city for people who are not teachers!","display_sequence":"A","result":{"columns":["name","city"],"values":[["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Robert Stone","Bristol"],["David Wood","York"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Peter Grant","Bath"],["Jane Bell","York"],["Laura Miles","Cambridge"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["Jane Bell","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":80,"description":"Retrieve name and city for students who did not participate in any course instance with a start date during 2010!","display_sequence":"B","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Jane Bell","York"],["William White","York"],["Jane Bell","York"]]}},{"id":81,"description":"Retrieve name and city for students who have not taken the course with code Java1!","display_sequence":"C","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["Robert Stone","Bristol"],["David Wood","York"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["William White","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":82,"description":"Retrieve name and city for students who have not taken the course with code LOG1!","display_sequence":"D","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Jane Bell","York"],["Laura Miles","Cambridge"],["William White","York"],["Jane Bell","York"],["David Wood","York"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Retrieve person_id, name, office_room, and phone for teachers who have never held a course instance for the course \"Java, Advanced Course\"!","display_sequence":"A","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-001","James Smith",634,"01234567890"],["EN-003","William Jones",603,"01234567892"],["EN-004","Mary Wilson",605,"01234567893"],["EN-005","Sarah Taylor",622,"01234567894"]]}},{"id":84,"description":"Retrieve person_id, name, and city for students with disabilities who have not taken the course \"Logical Database Design\"!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-020","David Wood","York"]]}},{"id":85,"description":"Retrieve name and capacity for rooms that have never been used for a course instance of the course \"Logical Database Design\"!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Retrieve name and capacity for rooms that have never been used by the teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Retrieve person_id, name, and phone for students who have taken the course \"Physical Database Design\" or the course \"Logical Database Design\" (or both)!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"],["EN-013","Peter Grant","01234567902"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-018","William White","01234567907"]]}},{"id":88,"description":"Retrieve person_id, name, and city for students who have participated in course instances in room Orion or in room Jupiter (or in both)!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-021","John Clark","Cambridge"]]}},{"id":89,"description":"Retrieve person_id, name, and phone for teachers who have held the course \"Logic\" or the course \"Database Methodology\" (or both)!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-018","William White","01234567907"]]}},{"id":90,"description":"Retrieve name and capacity for rooms that have been used by teacher John Brown or by teacher William Jones (or by both)!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Saturn",24],["Tellus",32]]}},{"id":91,"description":"Retrieve name and capacity for rooms that have been used for the course \"Logic\" or for the course \"Java, Advanced Course\" (or for both)!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Retrieve person_id, name, and city for students who have taken both the course \"Physical Database Design\" and the course \"Logical Database Design\"!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-013","Peter Grant","Bath"],["EN-011","Susan Blair","Bristol"]]}},{"id":93,"description":"Retrieve person_id, name, and phone for students who have participated in course instances in both room Orion and room Jupiter!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-011","Susan Blair","01234567900"],["EN-007","Anne Hall","01234567896"],["EN-010","David Wood","01234567899"],["EN-019","Jane Bell","01234567908"],["EN-013","Peter Grant","01234567902"]]}},{"id":94,"description":"Retrieve person_id, name, and office_room for teachers who have held both the course \"Logical Database Design\" and the course \"Database Methodology\"!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605]]}},{"id":95,"description":"Retrieve name and capacity for rooms that have been used by both teacher Mary Wilson and teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Orion",24],["Saturn",24],["Jupiter",12]]}},{"id":96,"description":"Retrieve name and capacity for rooms that have been used for both the course \"Logic\" and the course \"Java, Basic Course\"!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":97,"description":"Retrieve person_id, name, office_room, and phone for teachers who have held course instances in both room Jupiter and room Sirius!","display_sequence":"F","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-005","Sarah Taylor",622,"01234567894"],["EN-004","Mary Wilson",605,"01234567893"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Retrieve person_id, name, and phone for students who have participated in course instances led by all teachers from Oxford!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"]]}},{"id":99,"description":"Retrieve name and capacity for rooms that have been used by all teachers from Oxford!","display_sequence":"B","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":100,"description":"Retrieve name and capacity for rooms that have been used for all courses that cost under 5000!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":101,"description":"Retrieve person_id, name, and phone for teachers who have held course instances of all courses of at most three weeks!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-005","Sarah Taylor","01234567894"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Retrieve the number of students from Bristol for each course instance! Show course_code, start_date, and the number of students! Course instances without students from Bristol should also be shown!","display_sequence":"A","result":{"columns":["course","start_date","count"],"values":[["LDD","2008-09-02",1],["Java1","2008-03-06",0],["Java1","2008-05-06",2],["LOG1","2010-05-01",0],["LDD","2010-09-24",0],["Java2","2010-04-11",2],["LDD","2008-05-06",1],["DBM1","2008-04-02",1],["Java2","2008-04-16",3],["Java1","2010-05-01",2],["LOG1","2009-09-17",1],["DBM1","2010-09-22",1],["DBM1","2009-01-28",0],["Java2","2009-01-22",1],["Java1","2011-05-01",0],["LOG1","2008-04-16",1],["Java1","2008-09-02",0],["DBM1","2010-02-15",1],["PDB","2008-05-09",1]]}},{"id":103,"description":"Retrieve the number of times each student has participated in the course \"Logical Database Design\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"B","result":{"columns":["person_id","name","city","count"],"values":[["EN-006","John Clark","Brighton",1],["EN-007","Anne Hall","Bristol",0],["EN-013","Peter Grant","Bath",2],["EN-009","Robert Stone","Bristol",0],["EN-019","Jane Bell","York",0],["EN-012","George Hunt","Bristol",1],["EN-011","Susan Blair","Bristol",1],["EN-014","Jane Bell","York",0],["EN-010","David Wood","York",0],["EN-015","Laura Miles","Cambridge",0],["EN-020","David Wood","York",0],["EN-001","James Smith","Guildford",0],["EN-021","John Clark","Cambridge",0],["EN-017","Jane Palmer","Oxford",1],["EN-018","William White","York",1],["EN-016","Alice Cole","Oxford",1],["EN-008","Emily Ford","Brighton",0]]}},{"id":104,"description":"Retrieve the number of times each student has participated in the course \"Java, Basic Course\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"C","result":{"columns":["person_id","name","city","count"],"values":[["EN-006","John Clark","Brighton",2],["EN-007","Anne Hall","Bristol",1],["EN-013","Peter Grant","Bath",2],["EN-009","Robert Stone","Bristol",0],["EN-019","Jane Bell","York",1],["EN-012","George Hunt","Bristol",1],["EN-011","Susan Blair","Bristol",2],["EN-014","Jane Bell","York",2],["EN-010","David Wood","York",0],["EN-015","Laura Miles","Cambridge",1],["EN-020","David Wood","York",0],["EN-001","James Smith","Guildford",0],["EN-021","John Clark","Cambridge",0],["EN-017","Jane Palmer","Oxford",0],["EN-018","William White","York",0],["EN-016","Alice Cole","Oxford",0],["EN-008","Emily Ford","Brighton",1]]}},{"id":105,"description":"Retrieve the number of times each teacher has held the course \"Java, Basic Course\"! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held the course should also be shown!","display_sequence":"D","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-018","William White","01234567907",558,0],["EN-002","John Brown","01234567891",604,3],["EN-004","Mary Wilson","01234567893",605,0],["EN-005","Sarah Taylor","01234567894",622,0],["EN-001","James Smith","01234567890",634,0],["EN-003","William Jones","01234567892",603,2]]}},{"id":106,"description":"Retrieve the number of course instances each teacher has held with a start date during the year 2010! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held course instances during 2010 should also be shown!","display_sequence":"E","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-018","William White","01234567907",558,2],["EN-002","John Brown","01234567891",604,1],["EN-004","Mary Wilson","01234567893",605,2],["EN-005","Sarah Taylor","01234567894",622,0],["EN-001","James Smith","01234567890",634,0],["EN-003","William Jones","01234567892",603,1]]}},{"id":107,"description":"Retrieve the number of course instances with a start date during the year 2008 for each student! Show the student's person_id, name, phone, and city as well as the count! Students who have not participated in course instances during 2008 should also be shown!","display_sequence":"F","result":{"columns":["person_id","name","phone","city","count"],"values":[["EN-006","John Clark","01234567895","Brighton",4],["EN-007","Anne Hall","01234567896","Bristol",3],["EN-013","Peter Grant","01234567902","Bath",3],["EN-009","Robert Stone","01234567898","Bristol",1],["EN-019","Jane Bell","01234567908","York",1],["EN-012","George Hunt","01234567901","Bristol",2],["EN-011","Susan Blair","01234567900","Bristol",4],["EN-014","Jane Bell","01234567903","York",2],["EN-010","David Wood","01234567899","York",2],["EN-015","Laura Miles","01234567904","Cambridge",2],["EN-020","David Wood","01234567909","York",0],["EN-001","James Smith","01234567890","Guildford",0],["EN-021","John Clark","01234567910","Cambridge",0],["EN-017","Jane Palmer","01234567906","Oxford",0],["EN-018","William White","01234567907","York",1],["EN-016","Alice Cole","01234567905","Oxford",0],["EN-008","Emily Ford","01234567897","Brighton",3]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Retrieve information about course instances in which no student from York has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"A","result":{"columns":["course","name","start_date","name","name"],"values":[["DBM1","Sarah Taylor","2008-04-02","Sarah Taylor","Sarah Taylor"],["Java1","John Brown","2008-05-06","John Brown","John Brown"],["LDD","Sarah Taylor","2008-05-06","Sarah Taylor","Sarah Taylor"],["PDB","Mary Wilson","2008-05-09","Mary Wilson","Mary Wilson"],["LOG1","Sarah Taylor","2009-09-17","Sarah Taylor","Sarah Taylor"],["DBM1","Mary Wilson","2010-02-15","Mary Wilson","Mary Wilson"],["Java1","William Jones","2010-05-01","William Jones","William Jones"],["LDD","Mary Wilson","2010-09-24","Mary Wilson","Mary Wilson"],["Java1","John Brown","2011-05-01","John Brown","John Brown"]]}},{"id":109,"description":"Retrieve information about course instances in which no student from Bristol has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"B","result":{"columns":["course","name","start_date","name","name"],"values":[["Java1","John Brown","2008-03-06","John Brown","John Brown"],["Java1","William Jones","2008-09-02","William Jones","William Jones"],["DBM1","Sarah Taylor","2009-01-28","Sarah Taylor","Sarah Taylor"],["LOG1","John Brown","2010-05-01","John Brown","John Brown"],["LDD","Mary Wilson","2010-09-24","Mary Wilson","Mary Wilson"],["Java1","John Brown","2011-05-01","John Brown","John Brown"]]}},{"id":110,"description":"Retrieve information about course instances in which no student with a disability has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"C","result":{"columns":["course","name","start_date","name","name"],"values":[["LOG1","William Jones","2008-04-16","William Jones","William Jones"],["LDD","Sarah Taylor","2008-05-06","Sarah Taylor","Sarah Taylor"],["Java1","William Jones","2008-09-02","William Jones","William Jones"],["DBM1","Sarah Taylor","2009-01-28","Sarah Taylor","Sarah Taylor"],["LOG1","John Brown","2010-05-01","John Brown","John Brown"],["Java1","John Brown","2011-05-01","John Brown","John Brown"]]}}]}]} \ No newline at end of file diff --git a/public/languages/en/questionpool.json b/public/languages/en/questionpool.json index 0f876f9..efa0ba7 100644 --- a/public/languages/en/questionpool.json +++ b/public/languages/en/questionpool.json @@ -1 +1 @@ -{"language":"en","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Retrieve person_id, name, address, and postal_code for people living in York!","display_sequence":"A","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-010","David Wood","School Lane 3","BA1 1AJ"],["EN-014","Jane Bell","Bridge Street 5","BA2 5AN"],["EN-018","William White","Station Road 4","BA3 9AR"],["EN-019","Jane Bell","School Lane 12","BA4 1AS"],["EN-020","David Wood","School Lane 1","BA5 2AT"]]}},{"id":2,"description":"Retrieve person_id, name, and phone for people living in York!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":3,"description":"Retrieve person_id, name, address, and postal_code for people living in Bristol!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-007","Anne Hall","Elm Drive 3","YO1 7AG"],["EN-009","Robert Stone","Lake Drive 1","YO2 9AI"],["EN-011","Susan Blair","Birch Way 2","YO3 2AK"],["EN-012","George Hunt","Birch Way 24","YO4 3AL"]]}},{"id":4,"description":"Retrieve person_id, name, and phone for people living in Bristol!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":5,"description":"Retrieve name, address, postal_code, and phone for people living in York!","display_sequence":"E","result":{"columns":["name","address","postal_code","phone"],"values":[["David Wood","School Lane 3","BA1 1AJ","01234567899"],["Jane Bell","Bridge Street 5","BA2 5AN","01234567903"],["William White","Station Road 4","BA3 9AR","01234567907"],["Jane Bell","School Lane 12","BA4 1AS","01234567908"],["David Wood","School Lane 1","BA5 2AT","01234567909"]]}},{"id":6,"description":"Retrieve name, address, postal_code, and phone for people living in Bristol!","display_sequence":"F","result":{"columns":["name","address","postal_code","phone"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","01234567896"],["Robert Stone","Lake Drive 1","YO2 9AI","01234567898"],["Susan Blair","Birch Way 2","YO3 2AK","01234567900"],["George Hunt","Birch Way 24","YO4 3AL","01234567901"]]}},{"id":7,"description":"Retrieve course_code, name, and price for courses of 3 weeks!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["LOG1","Logic",4500]]}},{"id":8,"description":"Retrieve course_code, name, and price for courses of 4 weeks!","display_sequence":"H","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Retrieve person_id, name, and city for people living in York or in Bristol!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-014","Jane Bell","York"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"]]}},{"id":10,"description":"Retrieve person_id, name, and phone for people living in York or in Brighton!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":11,"description":"Retrieve person_id, name, and city for people living in Bristol or in Cambridge!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-021","John Clark","Cambridge"]]}},{"id":12,"description":"Retrieve person_id, name, and phone for people living in Bristol or in Brighton!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-007","Anne Hall","01234567896"],["EN-008","Emily Ford","01234567897"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":13,"description":"Retrieve name, address, postal_code, and city for people living in York or in Cambridge!","display_sequence":"E","result":{"columns":["name","address","postal_code","city"],"values":[["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"],["John Clark","High Street 63","CB4 3AU","Cambridge"]]}},{"id":14,"description":"Retrieve name, address, postal_code, and city for people living in Bristol or in York!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":15,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Brighton!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-004","Mary Wilson","01234567893"],["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-015","Laura Miles","01234567904"],["EN-021","John Clark","01234567910"]]}},{"id":16,"description":"Retrieve person_id, name, and city for people living in York or in Cambridge!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-010","David Wood","York"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"],["EN-021","John Clark","Cambridge"]]}},{"id":17,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Oxford!","display_sequence":"I","result":{"columns":["person_id","name","phone"],"values":[["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-015","Laura Miles","01234567904"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-021","John Clark","01234567910"]]}},{"id":18,"description":"Retrieve name, address, postal_code, and city for people living in York or in Oxford!","display_sequence":"J","result":{"columns":["name","address","postal_code","city"],"values":[["William Jones","Park Road 3","OX1 3AC","Oxford"],["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":19,"description":"Retrieve course_code, name, and price for courses of 3 or 4 weeks!","display_sequence":"K","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000],["LOG1","Logic",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Retrieve person_id for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["teacher"],"values":[["EN-003"],["EN-002"],["EN-004"],["EN-005"]]}},{"id":21,"description":"Retrieve person_id for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["teacher"],"values":[["EN-002"],["EN-004"]]}},{"id":22,"description":"Retrieve person_id for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"],["EN-018"]]}},{"id":23,"description":"Retrieve person_id for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["teacher"],"values":[["EN-002"],["EN-003"]]}},{"id":24,"description":"Retrieve person_id for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["teacher"],"values":[["EN-002"],["EN-018"]]}},{"id":25,"description":"Retrieve person_id for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"],["EN-018"]]}},{"id":26,"description":"Retrieve person_id for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"]]}},{"id":27,"description":"Retrieve person_id for students who have participated in the course with code Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["EN-006"],["EN-008"],["EN-013"],["EN-014"],["EN-015"],["EN-019"],["EN-007"],["EN-011"],["EN-012"]]}},{"id":28,"description":"Retrieve person_id for students who have participated in the course with code DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-015"],["EN-010"],["EN-019"],["EN-011"],["EN-009"],["EN-016"],["EN-017"],["EN-020"]]}},{"id":29,"description":"Retrieve person_id for students who have participated in the course with code Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["EN-007"],["EN-010"],["EN-011"],["EN-012"],["EN-014"],["EN-015"],["EN-019"],["EN-009"]]}},{"id":30,"description":"Retrieve person_id for students who have participated in the course with code LDD!","display_sequence":"K","result":{"columns":["student"],"values":[["EN-006"],["EN-012"],["EN-011"],["EN-013"],["EN-018"],["EN-016"],["EN-017"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Retrieve person_id, name, and phone for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-003","William Jones","01234567892"],["EN-002","John Brown","01234567891"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":32,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":33,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-005","Sarah Taylor","Mill Lane 5","BN1 5AE"],["EN-004","Mary Wilson","High Street 4","CB2 4AD"],["EN-018","William White","Station Road 4","BA3 9AR"]]}},{"id":34,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"]]}},{"id":35,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-018","William White","York"]]}},{"id":36,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["William White","Station Road 4","BA3 9AR","York"]]}},{"id":37,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-005","Sarah Taylor","01234567894"],["EN-004","Mary Wilson","01234567893"]]}},{"id":38,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 1!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-003","William Jones","Oxford"],["EN-002","John Brown","Reading"],["EN-004","Mary Wilson","Cambridge"],["EN-005","Sarah Taylor","Oxford"]]}},{"id":39,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 2!","display_sequence":"I","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-002","John Brown","Oak Avenue 2","RG1 2AB"],["EN-004","Mary Wilson","High Street 4","CB2 4AD"]]}},{"id":40,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java2!","display_sequence":"J","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-018","William White","01234567907"]]}},{"id":41,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code LDD!","display_sequence":"K","result":{"columns":["person_id","name","city"],"values":[["EN-005","Sarah Taylor","Oxford"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":42,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code Java1!","display_sequence":"L","result":{"columns":["name","address","postal_code","city"],"values":[["John Brown","Oak Avenue 2","RG1 2AB","Reading"],["William Jones","Park Road 3","OX1 3AC","Oxford"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","office_room"],"values":[["EN-003","William Jones",603],["EN-002","John Brown",604],["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}},{"id":44,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-004","Mary Wilson",605]]}},{"id":45,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605],["EN-018","William White",558]]}},{"id":46,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-003","William Jones",603]]}},{"id":47,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-018","William White",558]]}},{"id":48,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605],["EN-018","William White",558]]}},{"id":49,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Retrieve person_id for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["EN-009"],["EN-010"],["EN-007"],["EN-011"],["EN-008"],["EN-013"],["EN-019"],["EN-017"],["EN-016"],["EN-021"]]}},{"id":51,"description":"Retrieve person_id for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["EN-007"],["EN-010"],["EN-011"],["EN-012"],["EN-013"],["EN-018"],["EN-014"],["EN-015"],["EN-019"]]}},{"id":52,"description":"Retrieve person_id for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["EN-006"],["EN-008"],["EN-013"],["EN-014"],["EN-015"],["EN-019"],["EN-009"],["EN-010"],["EN-012"]]}},{"id":53,"description":"Retrieve person_id for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-015"],["EN-012"],["EN-011"],["EN-009"],["EN-016"],["EN-017"],["EN-020"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-007","Anne Hall","01234567896"],["EN-011","Susan Blair","01234567900"],["EN-008","Emily Ford","01234567897"],["EN-013","Peter Grant","01234567902"],["EN-019","Jane Bell","01234567908"],["EN-017","Jane Palmer","01234567906"],["EN-016","Alice Cole","01234567905"],["EN-021","John Clark","01234567910"]]}},{"id":55,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-018","William White","York"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-019","Jane Bell","York"]]}},{"id":56,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-013","Peter Grant","01234567902"],["EN-014","Jane Bell","01234567903"],["EN-015","Laura Miles","01234567904"],["EN-019","Jane Bell","01234567908"],["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"]]}},{"id":57,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-015","Laura Miles","Cambridge"],["EN-012","George Hunt","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-020","David Wood","York"]]}},{"id":58,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"E","result":{"columns":["name","address","city","phone"],"values":[["Robert Stone","Lake Drive 1","Bristol","01234567898"],["David Wood","School Lane 3","York","01234567899"],["Anne Hall","Elm Drive 3","Bristol","01234567896"],["Susan Blair","Birch Way 2","Bristol","01234567900"],["Emily Ford","River Road 4","Brighton","01234567897"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Jane Bell","School Lane 12","York","01234567908"],["Jane Palmer","Harbour Lane 15b","Oxford","01234567906"],["Alice Cole","King Street 44","Oxford","01234567905"],["John Clark","High Street 63","Cambridge","01234567910"]]}},{"id":59,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Orion!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Peter Grant","Hill Road 112","OX2 4AM","Bath"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["Jane Bell","School Lane 12","BA4 1AS","York"]]}},{"id":60,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Tellus!","display_sequence":"G","result":{"columns":["name","address","city","phone"],"values":[["John Clark","Church Road 6","Brighton","01234567895"],["Emily Ford","River Road 4","Brighton","01234567897"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Jane Bell","Bridge Street 5","York","01234567903"],["Laura Miles","Valley Road 3","Cambridge","01234567904"],["Jane Bell","School Lane 12","York","01234567908"],["Robert Stone","Lake Drive 1","Bristol","01234567898"],["David Wood","School Lane 3","York","01234567899"],["George Hunt","Birch Way 24","Bristol","01234567901"]]}},{"id":61,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Sirius!","display_sequence":"H","result":{"columns":["name","address","postal_code","city"],"values":[["John Clark","Church Road 6","BS1 6AF","Brighton"],["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["Emily Ford","River Road 4","BS2 8AH","Brighton"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["David Wood","School Lane 1","BA5 2AT","York"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Retrieve the number of course instances each student has participated in! Show the student's person_id, the student's name, and the count!","display_sequence":"A","result":{"columns":["person_id","name","count"],"values":[["EN-001","James Smith",0],["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-008","Emily Ford",5],["EN-009","Robert Stone",4],["EN-010","David Wood",5],["EN-011","Susan Blair",6],["EN-012","George Hunt",4],["EN-013","Peter Grant",6],["EN-014","Jane Bell",3],["EN-015","Laura Miles",5],["EN-016","Alice Cole",3],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-019","Jane Bell",3],["EN-020","David Wood",1],["EN-021","John Clark",1]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-008","Emily Ford",5],["EN-009","Robert Stone",4],["EN-010","David Wood",5],["EN-011","Susan Blair",6],["EN-012","George Hunt",4],["EN-013","Peter Grant",6],["EN-014","Jane Bell",3],["EN-015","Laura Miles",5],["EN-016","Alice Cole",3],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-019","Jane Bell",3],["EN-020","David Wood",1],["EN-021","John Clark",1]]}]},{"id":63,"description":"Retrieve the number of course instances each room has been used for! Show the room's name, the room's capacity, and the count!","display_sequence":"B","result":{"columns":["name","capacity","count"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturn",24,3]]},"alternative_results":[{"columns":["name","capacity","count"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturn",24,3]]}]},{"id":64,"description":"Retrieve the number of course instances each course has had! Show the course's code, the course's name, and the count!","display_sequence":"C","result":{"columns":["course_code","name","count"],"values":[["DBM1","Database Methodology",4],["Java1","Java, Basic Course",5],["Java2","Java, Advanced Course",3],["LDD","Logical Database Design",3],["LOG1","Logic",3],["PDB","Physical Database Design",1]]},"alternative_results":[{"columns":["course_code","name","count"],"values":[["DBM1","Database Methodology",4],["Java1","Java, Basic Course",5],["Java2","Java, Advanced Course",3],["LDD","Logical Database Design",3],["LOG1","Logic",3],["PDB","Physical Database Design",1]]}]},{"id":65,"description":"Retrieve the number of course instances each teacher has held! Show the teacher's person_id, the teacher's name, and the count!","display_sequence":"D","result":{"columns":["person_id","name","count"],"values":[["EN-001","James Smith",0],["EN-002","John Brown",6],["EN-003","William Jones",3],["EN-004","Mary Wilson",4],["EN-005","Sarah Taylor",4],["EN-018","William White",2]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-002","John Brown",6],["EN-003","William Jones",3],["EN-004","Mary Wilson",4],["EN-005","Sarah Taylor",4],["EN-018","William White",2]]}]},{"id":66,"description":"Retrieve the number of participating students for each course instance! Show the course instance's course_code and course name, the course instance's start_date, and the number of participating students!","display_sequence":"E","result":{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z",4],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z",3],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z",3],["DBM1","Database Methodology","2010-09-22T00:00:00.000Z",4],["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z",6],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z",2],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z",2],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z",3],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z",0],["Java2","Java, Advanced Course","2008-04-16T00:00:00.000Z",4],["Java2","Java, Advanced Course","2009-01-22T00:00:00.000Z",4],["Java2","Java, Advanced Course","2010-04-11T00:00:00.000Z",4],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z",2],["LDD","Logical Database Design","2008-09-02T00:00:00.000Z",3],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z",3],["LOG1","Logic","2008-04-16T00:00:00.000Z",2],["LOG1","Logic","2009-09-17T00:00:00.000Z",2],["LOG1","Logic","2010-05-01T00:00:00.000Z",4],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z",3]]},"alternative_results":[{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z",4],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z",3],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z",3],["DBM1","Database Methodology","2010-09-22T00:00:00.000Z",4],["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z",6],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z",2],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z",2],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z",3],["Java2","Java, Advanced Course","2008-04-16T00:00:00.000Z",4],["Java2","Java, Advanced Course","2009-01-22T00:00:00.000Z",4],["Java2","Java, Advanced Course","2010-04-11T00:00:00.000Z",4],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z",2],["LDD","Logical Database Design","2008-09-02T00:00:00.000Z",3],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z",3],["LOG1","Logic","2008-04-16T00:00:00.000Z",2],["LOG1","Logic","2009-09-17T00:00:00.000Z",2],["LOG1","Logic","2010-05-01T00:00:00.000Z",4],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z",3]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances each teacher has held! II. Retrieve person_id, name, and phone for the teacher who has held the most course instances!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"]]}},{"id":68,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances per student! II. Retrieve person_id, name, and phone for the student who has participated in the most course instances!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-011","Susan Blair","01234567900"],["EN-013","Peter Grant","01234567902"]]}},{"id":69,"description":"I. Retrieve course_code, name, and duration as well as the number of course instances per course! II. Retrieve course_code, name, and duration for the course that has had the most course instances!","display_sequence":"C","result":{"columns":["course_code","name","duration"],"values":[["Java1","Java, Basic Course",5]]}},{"id":70,"description":"I. Retrieve name and capacity as well as the number of course instances per room! II. Retrieve name and capacity for the room that has been used for the most course instances!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Retrieve course_code, course name, and start_date as well as the number of participants per course instance! II. Retrieve course_code, course name, and start_date for the course instance that has had the most participants!","display_sequence":"E","result":{"columns":["course","name","start_date"],"values":[["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Retrieve person_id, name, and city for students who have participated in at least three DIFFERENT courses!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-019","Jane Bell","York"]]}},{"id":73,"description":"Retrieve person_id, name, and phone for teachers who have held at least three DIFFERENT courses!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":74,"description":"Retrieve person_id, name, and city for teachers who have held course instances in at least three DIFFERENT rooms!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-003","William Jones","Oxford"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":75,"description":"Retrieve name and capacity for rooms that have been used by at least three DIFFERENT teachers!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturn",24]]}},{"id":76,"description":"Retrieve name and capacity for rooms that have been used for course instances of at least three DIFFERENT courses!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":77,"description":"Retrieve code, name, and price for courses that have been held in at least three DIFFERENT rooms!","display_sequence":"F","result":{"columns":["course_code","name","price"],"values":[["Java1","Java, Basic Course",6700],["LDD","Logical Database Design",6000]]}},{"id":78,"description":"Retrieve code, name, and price for courses that have been held by at least three DIFFERENT teachers!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["DBM1","Database Methodology",2800],["LOG1","Logic",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Retrieve name and city for people who are not teachers!","display_sequence":"A","result":{"columns":["name","city"],"values":[["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Robert Stone","Bristol"],["David Wood","York"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Peter Grant","Bath"],["Jane Bell","York"],["Laura Miles","Cambridge"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["Jane Bell","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":80,"description":"Retrieve name and city for students who did not participate in any course instance with a start date during 2010!","display_sequence":"B","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Jane Bell","York"],["William White","York"],["Jane Bell","York"]]}},{"id":81,"description":"Retrieve name and city for students who have not taken the course with code Java1!","display_sequence":"C","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["Robert Stone","Bristol"],["David Wood","York"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["William White","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":82,"description":"Retrieve name and city for students who have not taken the course with code LOG1!","display_sequence":"D","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Jane Bell","York"],["Laura Miles","Cambridge"],["William White","York"],["Jane Bell","York"],["David Wood","York"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Retrieve person_id, name, office_room, and phone for teachers who have never held a course instance for the course \"Java, Advanced Course\"!","display_sequence":"A","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-001","James Smith",634,"01234567890"],["EN-003","William Jones",603,"01234567892"],["EN-004","Mary Wilson",605,"01234567893"],["EN-005","Sarah Taylor",622,"01234567894"]]}},{"id":84,"description":"Retrieve person_id, name, and city for students with disabilities who have not taken the course \"Logical Database Design\"!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-020","David Wood","York"]]}},{"id":85,"description":"Retrieve name and capacity for rooms that have never been used for a course instance of the course \"Logical Database Design\"!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Retrieve name and capacity for rooms that have never been used by the teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Retrieve person_id, name, and phone for students who have taken the course \"Physical Database Design\" or the course \"Logical Database Design\" (or both)!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"],["EN-013","Peter Grant","01234567902"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-018","William White","01234567907"]]}},{"id":88,"description":"Retrieve person_id, name, and city for students who have participated in course instances in room Orion or in room Jupiter (or in both)!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-021","John Clark","Cambridge"]]}},{"id":89,"description":"Retrieve person_id, name, and phone for teachers who have held the course \"Logic\" or the course \"Database Methodology\" (or both)!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-018","William White","01234567907"]]}},{"id":90,"description":"Retrieve name and capacity for rooms that have been used by teacher John Brown or by teacher William Jones (or by both)!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Saturn",24],["Tellus",32]]}},{"id":91,"description":"Retrieve name and capacity for rooms that have been used for the course \"Logic\" or for the course \"Java, Advanced Course\" (or for both)!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Retrieve person_id, name, and city for students who have taken both the course \"Physical Database Design\" and the course \"Logical Database Design\"!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-011","Susan Blair","Bristol"],["EN-013","Peter Grant","Bath"]]}},{"id":93,"description":"Retrieve person_id, name, and phone for students who have participated in course instances in both room Orion and room Jupiter!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-010","David Wood","01234567899"],["EN-011","Susan Blair","01234567900"],["EN-013","Peter Grant","01234567902"],["EN-019","Jane Bell","01234567908"]]}},{"id":94,"description":"Retrieve person_id, name, and office_room for teachers who have held both the course \"Logical Database Design\" and the course \"Database Methodology\"!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}},{"id":95,"description":"Retrieve name and capacity for rooms that have been used by both teacher Mary Wilson and teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Saturn",24]]}},{"id":96,"description":"Retrieve name and capacity for rooms that have been used for both the course \"Logic\" and the course \"Java, Basic Course\"!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":97,"description":"Retrieve person_id, name, office_room, and phone for teachers who have held course instances in both room Jupiter and room Sirius!","display_sequence":"F","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-004","Mary Wilson",605,"01234567893"],["EN-005","Sarah Taylor",622,"01234567894"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Retrieve person_id, name, and phone for students who have participated in course instances led by all teachers from Oxford!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"],["EN-009","Robert Stone","01234567898"]]}},{"id":99,"description":"Retrieve name and capacity for rooms that have been used by all teachers from Oxford!","display_sequence":"B","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":100,"description":"Retrieve name and capacity for rooms that have been used for all courses that cost under 5000!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":101,"description":"Retrieve person_id, name, and phone for teachers who have held course instances of all courses of at most three weeks!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-005","Sarah Taylor","01234567894"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Retrieve the number of students from Bristol for each course instance! Show course_code, start_date, and the number of students! Course instances without students from Bristol should also be shown!","display_sequence":"A","result":{"columns":["course","start_date","count"],"values":[["DBM1","2008-04-02T00:00:00.000Z",1],["DBM1","2009-01-28T00:00:00.000Z",0],["DBM1","2010-02-15T00:00:00.000Z",1],["DBM1","2010-09-22T00:00:00.000Z",1],["Java1","2008-03-06T00:00:00.000Z",0],["Java1","2008-05-06T00:00:00.000Z",2],["Java1","2008-09-02T00:00:00.000Z",0],["Java1","2010-05-01T00:00:00.000Z",2],["Java1","2011-05-01T00:00:00.000Z",0],["Java2","2008-04-16T00:00:00.000Z",3],["Java2","2009-01-22T00:00:00.000Z",1],["Java2","2010-04-11T00:00:00.000Z",2],["LDD","2008-05-06T00:00:00.000Z",1],["LDD","2008-09-02T00:00:00.000Z",1],["LDD","2010-09-24T00:00:00.000Z",0],["LOG1","2008-04-16T00:00:00.000Z",1],["LOG1","2009-09-17T00:00:00.000Z",1],["LOG1","2010-05-01T00:00:00.000Z",0],["PDB","2008-05-09T00:00:00.000Z",1]]}},{"id":103,"description":"Retrieve the number of times each student has participated in the course \"Logical Database Design\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"B","result":{"columns":["person_id","name","city","count"],"values":[["EN-001","James Smith","Guildford",0],["EN-006","John Clark","Brighton",1],["EN-007","Anne Hall","Bristol",0],["EN-008","Emily Ford","Brighton",0],["EN-009","Robert Stone","Bristol",0],["EN-010","David Wood","York",0],["EN-011","Susan Blair","Bristol",1],["EN-012","George Hunt","Bristol",1],["EN-013","Peter Grant","Bath",2],["EN-014","Jane Bell","York",0],["EN-015","Laura Miles","Cambridge",0],["EN-016","Alice Cole","Oxford",1],["EN-017","Jane Palmer","Oxford",1],["EN-018","William White","York",1],["EN-019","Jane Bell","York",0],["EN-020","David Wood","York",0],["EN-021","John Clark","Cambridge",0]]}},{"id":104,"description":"Retrieve the number of times each student has participated in the course \"Java, Basic Course\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"C","result":{"columns":["person_id","name","city","count"],"values":[["EN-001","James Smith","Guildford",0],["EN-006","John Clark","Brighton",2],["EN-007","Anne Hall","Bristol",1],["EN-008","Emily Ford","Brighton",1],["EN-009","Robert Stone","Bristol",0],["EN-010","David Wood","York",0],["EN-011","Susan Blair","Bristol",2],["EN-012","George Hunt","Bristol",1],["EN-013","Peter Grant","Bath",2],["EN-014","Jane Bell","York",2],["EN-015","Laura Miles","Cambridge",1],["EN-016","Alice Cole","Oxford",0],["EN-017","Jane Palmer","Oxford",0],["EN-018","William White","York",0],["EN-019","Jane Bell","York",1],["EN-020","David Wood","York",0],["EN-021","John Clark","Cambridge",0]]}},{"id":105,"description":"Retrieve the number of times each teacher has held the course \"Java, Basic Course\"! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held the course should also be shown!","display_sequence":"D","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-001","James Smith","01234567890",634,0],["EN-002","John Brown","01234567891",604,3],["EN-003","William Jones","01234567892",603,2],["EN-004","Mary Wilson","01234567893",605,0],["EN-005","Sarah Taylor","01234567894",622,0],["EN-018","William White","01234567907",558,0]]}},{"id":106,"description":"Retrieve the number of course instances each teacher has held with a start date during the year 2010! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held course instances during 2010 should also be shown!","display_sequence":"E","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-001","James Smith","01234567890",634,0],["EN-002","John Brown","01234567891",604,1],["EN-003","William Jones","01234567892",603,1],["EN-004","Mary Wilson","01234567893",605,2],["EN-005","Sarah Taylor","01234567894",622,0],["EN-018","William White","01234567907",558,2]]}},{"id":107,"description":"Retrieve the number of course instances with a start date during the year 2008 for each student! Show the student's person_id, name, phone, and city as well as the count! Students who have not participated in course instances during 2008 should also be shown!","display_sequence":"F","result":{"columns":["person_id","name","phone","city","count"],"values":[["EN-001","James Smith","01234567890","Guildford",0],["EN-006","John Clark","01234567895","Brighton",4],["EN-007","Anne Hall","01234567896","Bristol",3],["EN-008","Emily Ford","01234567897","Brighton",3],["EN-009","Robert Stone","01234567898","Bristol",1],["EN-010","David Wood","01234567899","York",2],["EN-011","Susan Blair","01234567900","Bristol",4],["EN-012","George Hunt","01234567901","Bristol",2],["EN-013","Peter Grant","01234567902","Bath",3],["EN-014","Jane Bell","01234567903","York",2],["EN-015","Laura Miles","01234567904","Cambridge",2],["EN-016","Alice Cole","01234567905","Oxford",0],["EN-017","Jane Palmer","01234567906","Oxford",0],["EN-018","William White","01234567907","York",1],["EN-019","Jane Bell","01234567908","York",1],["EN-020","David Wood","01234567909","York",0],["EN-021","John Clark","01234567910","Cambridge",0]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Retrieve information about course instances in which no student from York has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"A","result":{"columns":["course","name","start_date","name","name"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z","Sirius","Sarah Taylor"],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z","Jupiter","John Brown"],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z","Sirius","Sarah Taylor"],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z","Jupiter","Mary Wilson"],["LOG1","Logic","2009-09-17T00:00:00.000Z","Jupiter","Sarah Taylor"],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z","Sirius","Mary Wilson"],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z","Saturn","William Jones"],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z","Saturn","Mary Wilson"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}},{"id":109,"description":"Retrieve information about course instances in which no student from Bristol has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"B","result":{"columns":["course","name","start_date","name","name"],"values":[["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z","Tellus","John Brown"],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z","Tellus","William Jones"],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z","Jupiter","Sarah Taylor"],["LOG1","Logic","2010-05-01T00:00:00.000Z","Jupiter","John Brown"],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z","Saturn","Mary Wilson"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}},{"id":110,"description":"Retrieve information about course instances in which no student with a disability has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"C","result":{"columns":["course","name","start_date","name","name"],"values":[["LOG1","Logic","2008-04-16T00:00:00.000Z","Jupiter","William Jones"],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z","Sirius","Sarah Taylor"],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z","Tellus","William Jones"],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z","Jupiter","Sarah Taylor"],["LOG1","Logic","2010-05-01T00:00:00.000Z","Jupiter","John Brown"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}}]}]} \ No newline at end of file +{"language":"en","engine":"sqlite","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Retrieve person_id, name, address, and postal_code for people living in York!","display_sequence":"A","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-010","David Wood","School Lane 3","BA1 1AJ"],["EN-014","Jane Bell","Bridge Street 5","BA2 5AN"],["EN-018","William White","Station Road 4","BA3 9AR"],["EN-019","Jane Bell","School Lane 12","BA4 1AS"],["EN-020","David Wood","School Lane 1","BA5 2AT"]]}},{"id":2,"description":"Retrieve person_id, name, and phone for people living in York!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":3,"description":"Retrieve person_id, name, address, and postal_code for people living in Bristol!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-007","Anne Hall","Elm Drive 3","YO1 7AG"],["EN-009","Robert Stone","Lake Drive 1","YO2 9AI"],["EN-011","Susan Blair","Birch Way 2","YO3 2AK"],["EN-012","George Hunt","Birch Way 24","YO4 3AL"]]}},{"id":4,"description":"Retrieve person_id, name, and phone for people living in Bristol!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":5,"description":"Retrieve name, address, postal_code, and phone for people living in York!","display_sequence":"E","result":{"columns":["name","address","postal_code","phone"],"values":[["David Wood","School Lane 3","BA1 1AJ","01234567899"],["Jane Bell","Bridge Street 5","BA2 5AN","01234567903"],["William White","Station Road 4","BA3 9AR","01234567907"],["Jane Bell","School Lane 12","BA4 1AS","01234567908"],["David Wood","School Lane 1","BA5 2AT","01234567909"]]}},{"id":6,"description":"Retrieve name, address, postal_code, and phone for people living in Bristol!","display_sequence":"F","result":{"columns":["name","address","postal_code","phone"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","01234567896"],["Robert Stone","Lake Drive 1","YO2 9AI","01234567898"],["Susan Blair","Birch Way 2","YO3 2AK","01234567900"],["George Hunt","Birch Way 24","YO4 3AL","01234567901"]]}},{"id":7,"description":"Retrieve course_code, name, and price for courses of 3 weeks!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["LOG1","Logic",4500]]}},{"id":8,"description":"Retrieve course_code, name, and price for courses of 4 weeks!","display_sequence":"H","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Retrieve person_id, name, and city for people living in York or in Bristol!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-014","Jane Bell","York"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"]]}},{"id":10,"description":"Retrieve person_id, name, and phone for people living in York or in Brighton!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-010","David Wood","01234567899"],["EN-014","Jane Bell","01234567903"],["EN-018","William White","01234567907"],["EN-019","Jane Bell","01234567908"],["EN-020","David Wood","01234567909"]]}},{"id":11,"description":"Retrieve person_id, name, and city for people living in Bristol or in Cambridge!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-007","Anne Hall","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-021","John Clark","Cambridge"]]}},{"id":12,"description":"Retrieve person_id, name, and phone for people living in Bristol or in Brighton!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-007","Anne Hall","01234567896"],["EN-008","Emily Ford","01234567897"],["EN-009","Robert Stone","01234567898"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"]]}},{"id":13,"description":"Retrieve name, address, postal_code, and city for people living in York or in Cambridge!","display_sequence":"E","result":{"columns":["name","address","postal_code","city"],"values":[["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"],["John Clark","High Street 63","CB4 3AU","Cambridge"]]}},{"id":14,"description":"Retrieve name, address, postal_code, and city for people living in Bristol or in York!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":15,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Brighton!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-004","Mary Wilson","01234567893"],["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-015","Laura Miles","01234567904"],["EN-021","John Clark","01234567910"]]}},{"id":16,"description":"Retrieve person_id, name, and city for people living in York or in Cambridge!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-004","Mary Wilson","Cambridge"],["EN-010","David Wood","York"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-020","David Wood","York"],["EN-021","John Clark","Cambridge"]]}},{"id":17,"description":"Retrieve person_id, name, and phone for people living in Cambridge or in Oxford!","display_sequence":"I","result":{"columns":["person_id","name","phone"],"values":[["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-015","Laura Miles","01234567904"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-021","John Clark","01234567910"]]}},{"id":18,"description":"Retrieve name, address, postal_code, and city for people living in York or in Oxford!","display_sequence":"J","result":{"columns":["name","address","postal_code","city"],"values":[["William Jones","Park Road 3","OX1 3AC","Oxford"],["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["David Wood","School Lane 3","BA1 1AJ","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","School Lane 12","BA4 1AS","York"],["David Wood","School Lane 1","BA5 2AT","York"]]}},{"id":19,"description":"Retrieve course_code, name, and price for courses of 3 or 4 weeks!","display_sequence":"K","result":{"columns":["course_code","name","price"],"values":[["Java2","Java, Advanced Course",6000],["LDD","Logical Database Design",6000],["LOG1","Logic",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Retrieve person_id for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["teacher"],"values":[["EN-003"],["EN-002"],["EN-004"],["EN-005"]]}},{"id":21,"description":"Retrieve person_id for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["teacher"],"values":[["EN-002"],["EN-004"]]}},{"id":22,"description":"Retrieve person_id for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"],["EN-018"]]}},{"id":23,"description":"Retrieve person_id for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["teacher"],"values":[["EN-002"],["EN-003"]]}},{"id":24,"description":"Retrieve person_id for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["teacher"],"values":[["EN-002"],["EN-018"]]}},{"id":25,"description":"Retrieve person_id for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"],["EN-018"]]}},{"id":26,"description":"Retrieve person_id for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["teacher"],"values":[["EN-005"],["EN-004"]]}},{"id":27,"description":"Retrieve person_id for students who have participated in the course with code Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["EN-006"],["EN-008"],["EN-013"],["EN-014"],["EN-015"],["EN-019"],["EN-007"],["EN-011"],["EN-012"]]}},{"id":28,"description":"Retrieve person_id for students who have participated in the course with code DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-015"],["EN-010"],["EN-019"],["EN-011"],["EN-009"],["EN-016"],["EN-017"],["EN-020"]]}},{"id":29,"description":"Retrieve person_id for students who have participated in the course with code Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["EN-007"],["EN-010"],["EN-011"],["EN-012"],["EN-014"],["EN-015"],["EN-019"],["EN-009"]]}},{"id":30,"description":"Retrieve person_id for students who have participated in the course with code LDD!","display_sequence":"K","result":{"columns":["student"],"values":[["EN-006"],["EN-012"],["EN-011"],["EN-013"],["EN-018"],["EN-016"],["EN-017"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Retrieve person_id, name, and phone for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-003","William Jones","01234567892"],["EN-002","John Brown","01234567891"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":32,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":33,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-005","Sarah Taylor","Mill Lane 5","BN1 5AE"],["EN-004","Mary Wilson","High Street 4","CB2 4AD"],["EN-018","William White","Station Road 4","BA3 9AR"]]}},{"id":34,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"]]}},{"id":35,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-018","William White","York"]]}},{"id":36,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Sarah Taylor","Mill Lane 5","BN1 5AE","Oxford"],["Mary Wilson","High Street 4","CB2 4AD","Cambridge"],["William White","Station Road 4","BA3 9AR","York"]]}},{"id":37,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","phone"],"values":[["EN-005","Sarah Taylor","01234567894"],["EN-004","Mary Wilson","01234567893"]]}},{"id":38,"description":"Retrieve person_id, name, and city for teachers who have taught in the room with id 1!","display_sequence":"H","result":{"columns":["person_id","name","city"],"values":[["EN-003","William Jones","Oxford"],["EN-002","John Brown","Reading"],["EN-004","Mary Wilson","Cambridge"],["EN-005","Sarah Taylor","Oxford"]]}},{"id":39,"description":"Retrieve person_id, name, address, and postal_code for teachers who have taught in the room with id 2!","display_sequence":"I","result":{"columns":["person_id","name","address","postal_code"],"values":[["EN-002","John Brown","Oak Avenue 2","RG1 2AB"],["EN-004","Mary Wilson","High Street 4","CB2 4AD"]]}},{"id":40,"description":"Retrieve person_id, name, and phone for teachers who have taught on the course with code Java2!","display_sequence":"J","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-018","William White","01234567907"]]}},{"id":41,"description":"Retrieve person_id, name, and city for teachers who have taught on the course with code LDD!","display_sequence":"K","result":{"columns":["person_id","name","city"],"values":[["EN-005","Sarah Taylor","Oxford"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":42,"description":"Retrieve name, address, postal_code, and city for teachers who have taught on the course with code Java1!","display_sequence":"L","result":{"columns":["name","address","postal_code","city"],"values":[["John Brown","Oak Avenue 2","RG1 2AB","Reading"],["William Jones","Park Road 3","OX1 3AC","Oxford"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 1!","display_sequence":"A","result":{"columns":["person_id","name","office_room"],"values":[["EN-003","William Jones",603],["EN-002","John Brown",604],["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}},{"id":44,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 2!","display_sequence":"B","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-004","Mary Wilson",605]]}},{"id":45,"description":"Retrieve person_id, name, and office_room for teachers who have taught in the room with id 3!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605],["EN-018","William White",558]]}},{"id":46,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java1!","display_sequence":"D","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-003","William Jones",603]]}},{"id":47,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code Java2!","display_sequence":"E","result":{"columns":["person_id","name","office_room"],"values":[["EN-002","John Brown",604],["EN-018","William White",558]]}},{"id":48,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code DBM1!","display_sequence":"F","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605],["EN-018","William White",558]]}},{"id":49,"description":"Retrieve person_id, name, and office_room for teachers who have taught on the course with code LDD!","display_sequence":"G","result":{"columns":["person_id","name","office_room"],"values":[["EN-005","Sarah Taylor",622],["EN-004","Mary Wilson",605]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Retrieve person_id for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["EN-009"],["EN-010"],["EN-007"],["EN-011"],["EN-008"],["EN-013"],["EN-019"],["EN-017"],["EN-016"],["EN-021"]]}},{"id":51,"description":"Retrieve person_id for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["EN-007"],["EN-010"],["EN-011"],["EN-012"],["EN-013"],["EN-018"],["EN-014"],["EN-015"],["EN-019"]]}},{"id":52,"description":"Retrieve person_id for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["EN-006"],["EN-008"],["EN-013"],["EN-014"],["EN-015"],["EN-019"],["EN-009"],["EN-010"],["EN-012"]]}},{"id":53,"description":"Retrieve person_id for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["EN-006"],["EN-007"],["EN-008"],["EN-015"],["EN-012"],["EN-011"],["EN-009"],["EN-016"],["EN-017"],["EN-020"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-007","Anne Hall","01234567896"],["EN-011","Susan Blair","01234567900"],["EN-008","Emily Ford","01234567897"],["EN-013","Peter Grant","01234567902"],["EN-019","Jane Bell","01234567908"],["EN-017","Jane Palmer","01234567906"],["EN-016","Alice Cole","01234567905"],["EN-021","John Clark","01234567910"]]}},{"id":55,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Orion!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-018","William White","York"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-019","Jane Bell","York"]]}},{"id":56,"description":"Retrieve person_id, name, and phone for students who have participated in a course instance in Tellus!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-013","Peter Grant","01234567902"],["EN-014","Jane Bell","01234567903"],["EN-015","Laura Miles","01234567904"],["EN-019","Jane Bell","01234567908"],["EN-009","Robert Stone","01234567898"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"]]}},{"id":57,"description":"Retrieve person_id, name, and city for students who have participated in a course instance in Sirius!","display_sequence":"D","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-015","Laura Miles","Cambridge"],["EN-012","George Hunt","Bristol"],["EN-011","Susan Blair","Bristol"],["EN-009","Robert Stone","Bristol"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-020","David Wood","York"]]}},{"id":58,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Jupiter!","display_sequence":"E","result":{"columns":["name","address","city","phone"],"values":[["Robert Stone","Lake Drive 1","Bristol","01234567898"],["David Wood","School Lane 3","York","01234567899"],["Anne Hall","Elm Drive 3","Bristol","01234567896"],["Susan Blair","Birch Way 2","Bristol","01234567900"],["Emily Ford","River Road 4","Brighton","01234567897"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Jane Bell","School Lane 12","York","01234567908"],["Jane Palmer","Harbour Lane 15b","Oxford","01234567906"],["Alice Cole","King Street 44","Oxford","01234567905"],["John Clark","High Street 63","Cambridge","01234567910"]]}},{"id":59,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Orion!","display_sequence":"F","result":{"columns":["name","address","postal_code","city"],"values":[["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["David Wood","School Lane 3","BA1 1AJ","York"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Peter Grant","Hill Road 112","OX2 4AM","Bath"],["William White","Station Road 4","BA3 9AR","York"],["Jane Bell","Bridge Street 5","BA2 5AN","York"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["Jane Bell","School Lane 12","BA4 1AS","York"]]}},{"id":60,"description":"Retrieve name, address, city, and phone for students who have participated in a course instance in Tellus!","display_sequence":"G","result":{"columns":["name","address","city","phone"],"values":[["John Clark","Church Road 6","Brighton","01234567895"],["Emily Ford","River Road 4","Brighton","01234567897"],["Peter Grant","Hill Road 112","Bath","01234567902"],["Jane Bell","Bridge Street 5","York","01234567903"],["Laura Miles","Valley Road 3","Cambridge","01234567904"],["Jane Bell","School Lane 12","York","01234567908"],["Robert Stone","Lake Drive 1","Bristol","01234567898"],["David Wood","School Lane 3","York","01234567899"],["George Hunt","Birch Way 24","Bristol","01234567901"]]}},{"id":61,"description":"Retrieve name, address, postal_code, and city for students who have participated in a course instance in Sirius!","display_sequence":"H","result":{"columns":["name","address","postal_code","city"],"values":[["John Clark","Church Road 6","BS1 6AF","Brighton"],["Anne Hall","Elm Drive 3","YO1 7AG","Bristol"],["Emily Ford","River Road 4","BS2 8AH","Brighton"],["Laura Miles","Valley Road 3","CB3 6AO","Cambridge"],["George Hunt","Birch Way 24","YO4 3AL","Bristol"],["Susan Blair","Birch Way 2","YO3 2AK","Bristol"],["Robert Stone","Lake Drive 1","YO2 9AI","Bristol"],["Alice Cole","King Street 44","OX3 7AP","Oxford"],["Jane Palmer","Harbour Lane 15b","OX4 8AQ","Oxford"],["David Wood","School Lane 1","BA5 2AT","York"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Retrieve the number of course instances each student has participated in! Show the student's person_id, the student's name, and the count!","display_sequence":"A","result":{"columns":["person_id","name","count"],"values":[["EN-001","James Smith",0],["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-008","Emily Ford",5],["EN-009","Robert Stone",4],["EN-010","David Wood",5],["EN-011","Susan Blair",6],["EN-012","George Hunt",4],["EN-013","Peter Grant",6],["EN-014","Jane Bell",3],["EN-015","Laura Miles",5],["EN-016","Alice Cole",3],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-019","Jane Bell",3],["EN-020","David Wood",1],["EN-021","John Clark",1]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-006","John Clark",4],["EN-007","Anne Hall",4],["EN-008","Emily Ford",5],["EN-009","Robert Stone",4],["EN-010","David Wood",5],["EN-011","Susan Blair",6],["EN-012","George Hunt",4],["EN-013","Peter Grant",6],["EN-014","Jane Bell",3],["EN-015","Laura Miles",5],["EN-016","Alice Cole",3],["EN-017","Jane Palmer",3],["EN-018","William White",1],["EN-019","Jane Bell",3],["EN-020","David Wood",1],["EN-021","John Clark",1]]}]},{"id":63,"description":"Retrieve the number of course instances each room has been used for! Show the room's name, the room's capacity, and the count!","display_sequence":"B","result":{"columns":["name","capacity","count"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturn",24,3]]},"alternative_results":[{"columns":["name","capacity","count"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturn",24,3]]}]},{"id":64,"description":"Retrieve the number of course instances each course has had! Show the course's code, the course's name, and the count!","display_sequence":"C","result":{"columns":["course_code","name","count"],"values":[["DBM1","Database Methodology",4],["Java1","Java, Basic Course",5],["Java2","Java, Advanced Course",3],["LDD","Logical Database Design",3],["LOG1","Logic",3],["PDB","Physical Database Design",1]]},"alternative_results":[{"columns":["course_code","name","count"],"values":[["DBM1","Database Methodology",4],["Java1","Java, Basic Course",5],["Java2","Java, Advanced Course",3],["LDD","Logical Database Design",3],["LOG1","Logic",3],["PDB","Physical Database Design",1]]}]},{"id":65,"description":"Retrieve the number of course instances each teacher has held! Show the teacher's person_id, the teacher's name, and the count!","display_sequence":"D","result":{"columns":["person_id","name","count"],"values":[["EN-001","James Smith",0],["EN-002","John Brown",6],["EN-003","William Jones",3],["EN-004","Mary Wilson",4],["EN-005","Sarah Taylor",4],["EN-018","William White",2]]},"alternative_results":[{"columns":["person_id","name","count"],"values":[["EN-002","John Brown",6],["EN-003","William Jones",3],["EN-004","Mary Wilson",4],["EN-005","Sarah Taylor",4],["EN-018","William White",2]]}]},{"id":66,"description":"Retrieve the number of participating students for each course instance! Show the course instance's course_code and course name, the course instance's start_date, and the number of participating students!","display_sequence":"E","result":{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z",4],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z",3],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z",3],["DBM1","Database Methodology","2010-09-22T00:00:00.000Z",4],["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z",6],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z",2],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z",2],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z",3],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z",0],["Java2","Java, Advanced Course","2008-04-16T00:00:00.000Z",4],["Java2","Java, Advanced Course","2009-01-22T00:00:00.000Z",4],["Java2","Java, Advanced Course","2010-04-11T00:00:00.000Z",4],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z",2],["LDD","Logical Database Design","2008-09-02T00:00:00.000Z",3],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z",3],["LOG1","Logic","2008-04-16T00:00:00.000Z",2],["LOG1","Logic","2009-09-17T00:00:00.000Z",2],["LOG1","Logic","2010-05-01T00:00:00.000Z",4],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z",3]]},"alternative_results":[{"columns":["course","name","start_date","count"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z",4],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z",3],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z",3],["DBM1","Database Methodology","2010-09-22T00:00:00.000Z",4],["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z",6],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z",2],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z",2],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z",3],["Java2","Java, Advanced Course","2008-04-16T00:00:00.000Z",4],["Java2","Java, Advanced Course","2009-01-22T00:00:00.000Z",4],["Java2","Java, Advanced Course","2010-04-11T00:00:00.000Z",4],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z",2],["LDD","Logical Database Design","2008-09-02T00:00:00.000Z",3],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z",3],["LOG1","Logic","2008-04-16T00:00:00.000Z",2],["LOG1","Logic","2009-09-17T00:00:00.000Z",2],["LOG1","Logic","2010-05-01T00:00:00.000Z",4],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z",3]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances each teacher has held! II. Retrieve person_id, name, and phone for the teacher who has held the most course instances!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"]]}},{"id":68,"description":"I. Retrieve person_id, name, and phone as well as the number of course instances per student! II. Retrieve person_id, name, and phone for the student who has participated in the most course instances!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-011","Susan Blair","01234567900"],["EN-013","Peter Grant","01234567902"]]}},{"id":69,"description":"I. Retrieve course_code, name, and duration as well as the number of course instances per course! II. Retrieve course_code, name, and duration for the course that has had the most course instances!","display_sequence":"C","result":{"columns":["course_code","name","duration"],"values":[["Java1","Java, Basic Course",5]]}},{"id":70,"description":"I. Retrieve name and capacity as well as the number of course instances per room! II. Retrieve name and capacity for the room that has been used for the most course instances!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Retrieve course_code, course name, and start_date as well as the number of participants per course instance! II. Retrieve course_code, course name, and start_date for the course instance that has had the most participants!","display_sequence":"E","result":{"columns":["course","name","start_date"],"values":[["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Retrieve person_id, name, and city for students who have participated in at least three DIFFERENT courses!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-006","John Clark","Brighton"],["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-019","Jane Bell","York"]]}},{"id":73,"description":"Retrieve person_id, name, and phone for teachers who have held at least three DIFFERENT courses!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"]]}},{"id":74,"description":"Retrieve person_id, name, and city for teachers who have held course instances in at least three DIFFERENT rooms!","display_sequence":"C","result":{"columns":["person_id","name","city"],"values":[["EN-002","John Brown","Reading"],["EN-003","William Jones","Oxford"],["EN-004","Mary Wilson","Cambridge"]]}},{"id":75,"description":"Retrieve name and capacity for rooms that have been used by at least three DIFFERENT teachers!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturn",24]]}},{"id":76,"description":"Retrieve name and capacity for rooms that have been used for course instances of at least three DIFFERENT courses!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":77,"description":"Retrieve code, name, and price for courses that have been held in at least three DIFFERENT rooms!","display_sequence":"F","result":{"columns":["course_code","name","price"],"values":[["Java1","Java, Basic Course",6700],["LDD","Logical Database Design",6000]]}},{"id":78,"description":"Retrieve code, name, and price for courses that have been held by at least three DIFFERENT teachers!","display_sequence":"G","result":{"columns":["course_code","name","price"],"values":[["DBM1","Database Methodology",2800],["LOG1","Logic",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Retrieve name and city for people who are not teachers!","display_sequence":"A","result":{"columns":["name","city"],"values":[["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Robert Stone","Bristol"],["David Wood","York"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Peter Grant","Bath"],["Jane Bell","York"],["Laura Miles","Cambridge"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["Jane Bell","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":80,"description":"Retrieve name and city for students who did not participate in any course instance with a start date during 2010!","display_sequence":"B","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Jane Bell","York"],["William White","York"],["Jane Bell","York"]]}},{"id":81,"description":"Retrieve name and city for students who have not taken the course with code Java1!","display_sequence":"C","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["Robert Stone","Bristol"],["David Wood","York"],["Alice Cole","Oxford"],["Jane Palmer","Oxford"],["William White","York"],["David Wood","York"],["John Clark","Cambridge"]]}},{"id":82,"description":"Retrieve name and city for students who have not taken the course with code LOG1!","display_sequence":"D","result":{"columns":["name","city"],"values":[["James Smith","Guildford"],["John Clark","Brighton"],["Anne Hall","Bristol"],["Emily Ford","Brighton"],["Susan Blair","Bristol"],["George Hunt","Bristol"],["Jane Bell","York"],["Laura Miles","Cambridge"],["William White","York"],["Jane Bell","York"],["David Wood","York"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Retrieve person_id, name, office_room, and phone for teachers who have never held a course instance for the course \"Java, Advanced Course\"!","display_sequence":"A","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-001","James Smith",634,"01234567890"],["EN-003","William Jones",603,"01234567892"],["EN-004","Mary Wilson",605,"01234567893"],["EN-005","Sarah Taylor",622,"01234567894"]]}},{"id":84,"description":"Retrieve person_id, name, and city for students with disabilities who have not taken the course \"Logical Database Design\"!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-015","Laura Miles","Cambridge"],["EN-020","David Wood","York"]]}},{"id":85,"description":"Retrieve name and capacity for rooms that have never been used for a course instance of the course \"Logical Database Design\"!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Retrieve name and capacity for rooms that have never been used by the teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Retrieve person_id, name, and phone for students who have taken the course \"Physical Database Design\" or the course \"Logical Database Design\" (or both)!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-008","Emily Ford","01234567897"],["EN-011","Susan Blair","01234567900"],["EN-012","George Hunt","01234567901"],["EN-013","Peter Grant","01234567902"],["EN-016","Alice Cole","01234567905"],["EN-017","Jane Palmer","01234567906"],["EN-018","William White","01234567907"]]}},{"id":88,"description":"Retrieve person_id, name, and city for students who have participated in course instances in room Orion or in room Jupiter (or in both)!","display_sequence":"B","result":{"columns":["person_id","name","city"],"values":[["EN-007","Anne Hall","Bristol"],["EN-008","Emily Ford","Brighton"],["EN-009","Robert Stone","Bristol"],["EN-010","David Wood","York"],["EN-011","Susan Blair","Bristol"],["EN-012","George Hunt","Bristol"],["EN-013","Peter Grant","Bath"],["EN-014","Jane Bell","York"],["EN-015","Laura Miles","Cambridge"],["EN-016","Alice Cole","Oxford"],["EN-017","Jane Palmer","Oxford"],["EN-018","William White","York"],["EN-019","Jane Bell","York"],["EN-021","John Clark","Cambridge"]]}},{"id":89,"description":"Retrieve person_id, name, and phone for teachers who have held the course \"Logic\" or the course \"Database Methodology\" (or both)!","display_sequence":"C","result":{"columns":["person_id","name","phone"],"values":[["EN-002","John Brown","01234567891"],["EN-003","William Jones","01234567892"],["EN-004","Mary Wilson","01234567893"],["EN-005","Sarah Taylor","01234567894"],["EN-018","William White","01234567907"]]}},{"id":90,"description":"Retrieve name and capacity for rooms that have been used by teacher John Brown or by teacher William Jones (or by both)!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Saturn",24],["Tellus",32]]}},{"id":91,"description":"Retrieve name and capacity for rooms that have been used for the course \"Logic\" or for the course \"Java, Advanced Course\" (or for both)!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Retrieve person_id, name, and city for students who have taken both the course \"Physical Database Design\" and the course \"Logical Database Design\"!","display_sequence":"A","result":{"columns":["person_id","name","city"],"values":[["EN-011","Susan Blair","Bristol"],["EN-013","Peter Grant","Bath"]]}},{"id":93,"description":"Retrieve person_id, name, and phone for students who have participated in course instances in both room Orion and room Jupiter!","display_sequence":"B","result":{"columns":["person_id","name","phone"],"values":[["EN-007","Anne Hall","01234567896"],["EN-010","David Wood","01234567899"],["EN-011","Susan Blair","01234567900"],["EN-013","Peter Grant","01234567902"],["EN-019","Jane Bell","01234567908"]]}},{"id":94,"description":"Retrieve person_id, name, and office_room for teachers who have held both the course \"Logical Database Design\" and the course \"Database Methodology\"!","display_sequence":"C","result":{"columns":["person_id","name","office_room"],"values":[["EN-004","Mary Wilson",605],["EN-005","Sarah Taylor",622]]}},{"id":95,"description":"Retrieve name and capacity for rooms that have been used by both teacher Mary Wilson and teacher John Brown!","display_sequence":"D","result":{"columns":["name","capacity"],"values":[["Jupiter",12],["Orion",24],["Saturn",24]]}},{"id":96,"description":"Retrieve name and capacity for rooms that have been used for both the course \"Logic\" and the course \"Java, Basic Course\"!","display_sequence":"E","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":97,"description":"Retrieve person_id, name, office_room, and phone for teachers who have held course instances in both room Jupiter and room Sirius!","display_sequence":"F","result":{"columns":["person_id","name","office_room","phone"],"values":[["EN-004","Mary Wilson",605,"01234567893"],["EN-005","Sarah Taylor",622,"01234567894"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Retrieve person_id, name, and phone for students who have participated in course instances led by all teachers from Oxford!","display_sequence":"A","result":{"columns":["person_id","name","phone"],"values":[["EN-006","John Clark","01234567895"],["EN-010","David Wood","01234567899"],["EN-012","George Hunt","01234567901"],["EN-009","Robert Stone","01234567898"]]}},{"id":99,"description":"Retrieve name and capacity for rooms that have been used by all teachers from Oxford!","display_sequence":"B","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":100,"description":"Retrieve name and capacity for rooms that have been used for all courses that cost under 5000!","display_sequence":"C","result":{"columns":["name","capacity"],"values":[["Jupiter",12]]}},{"id":101,"description":"Retrieve person_id, name, and phone for teachers who have held course instances of all courses of at most three weeks!","display_sequence":"D","result":{"columns":["person_id","name","phone"],"values":[["EN-005","Sarah Taylor","01234567894"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Retrieve the number of students from Bristol for each course instance! Show course_code, start_date, and the number of students! Course instances without students from Bristol should also be shown!","display_sequence":"A","result":{"columns":["course","start_date","count"],"values":[["DBM1","2008-04-02T00:00:00.000Z",1],["DBM1","2009-01-28T00:00:00.000Z",0],["DBM1","2010-02-15T00:00:00.000Z",1],["DBM1","2010-09-22T00:00:00.000Z",1],["Java1","2008-03-06T00:00:00.000Z",0],["Java1","2008-05-06T00:00:00.000Z",2],["Java1","2008-09-02T00:00:00.000Z",0],["Java1","2010-05-01T00:00:00.000Z",2],["Java1","2011-05-01T00:00:00.000Z",0],["Java2","2008-04-16T00:00:00.000Z",3],["Java2","2009-01-22T00:00:00.000Z",1],["Java2","2010-04-11T00:00:00.000Z",2],["LDD","2008-05-06T00:00:00.000Z",1],["LDD","2008-09-02T00:00:00.000Z",1],["LDD","2010-09-24T00:00:00.000Z",0],["LOG1","2008-04-16T00:00:00.000Z",1],["LOG1","2009-09-17T00:00:00.000Z",1],["LOG1","2010-05-01T00:00:00.000Z",0],["PDB","2008-05-09T00:00:00.000Z",1]]}},{"id":103,"description":"Retrieve the number of times each student has participated in the course \"Logical Database Design\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"B","result":{"columns":["person_id","name","city","count"],"values":[["EN-001","James Smith","Guildford",0],["EN-006","John Clark","Brighton",1],["EN-007","Anne Hall","Bristol",0],["EN-008","Emily Ford","Brighton",0],["EN-009","Robert Stone","Bristol",0],["EN-010","David Wood","York",0],["EN-011","Susan Blair","Bristol",1],["EN-012","George Hunt","Bristol",1],["EN-013","Peter Grant","Bath",2],["EN-014","Jane Bell","York",0],["EN-015","Laura Miles","Cambridge",0],["EN-016","Alice Cole","Oxford",1],["EN-017","Jane Palmer","Oxford",1],["EN-018","William White","York",1],["EN-019","Jane Bell","York",0],["EN-020","David Wood","York",0],["EN-021","John Clark","Cambridge",0]]}},{"id":104,"description":"Retrieve the number of times each student has participated in the course \"Java, Basic Course\"! Show the student's person_id, name, and city as well as the count! Students who have not participated in the course should also be shown!","display_sequence":"C","result":{"columns":["person_id","name","city","count"],"values":[["EN-001","James Smith","Guildford",0],["EN-006","John Clark","Brighton",2],["EN-007","Anne Hall","Bristol",1],["EN-008","Emily Ford","Brighton",1],["EN-009","Robert Stone","Bristol",0],["EN-010","David Wood","York",0],["EN-011","Susan Blair","Bristol",2],["EN-012","George Hunt","Bristol",1],["EN-013","Peter Grant","Bath",2],["EN-014","Jane Bell","York",2],["EN-015","Laura Miles","Cambridge",1],["EN-016","Alice Cole","Oxford",0],["EN-017","Jane Palmer","Oxford",0],["EN-018","William White","York",0],["EN-019","Jane Bell","York",1],["EN-020","David Wood","York",0],["EN-021","John Clark","Cambridge",0]]}},{"id":105,"description":"Retrieve the number of times each teacher has held the course \"Java, Basic Course\"! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held the course should also be shown!","display_sequence":"D","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-001","James Smith","01234567890",634,0],["EN-002","John Brown","01234567891",604,3],["EN-003","William Jones","01234567892",603,2],["EN-004","Mary Wilson","01234567893",605,0],["EN-005","Sarah Taylor","01234567894",622,0],["EN-018","William White","01234567907",558,0]]}},{"id":106,"description":"Retrieve the number of course instances each teacher has held with a start date during the year 2010! Show the teacher's person_id, name, phone, and office_room as well as the count! Teachers who have not held course instances during 2010 should also be shown!","display_sequence":"E","result":{"columns":["person_id","name","phone","office_room","count"],"values":[["EN-001","James Smith","01234567890",634,0],["EN-002","John Brown","01234567891",604,1],["EN-003","William Jones","01234567892",603,1],["EN-004","Mary Wilson","01234567893",605,2],["EN-005","Sarah Taylor","01234567894",622,0],["EN-018","William White","01234567907",558,2]]}},{"id":107,"description":"Retrieve the number of course instances with a start date during the year 2008 for each student! Show the student's person_id, name, phone, and city as well as the count! Students who have not participated in course instances during 2008 should also be shown!","display_sequence":"F","result":{"columns":["person_id","name","phone","city","count"],"values":[["EN-001","James Smith","01234567890","Guildford",0],["EN-006","John Clark","01234567895","Brighton",4],["EN-007","Anne Hall","01234567896","Bristol",3],["EN-008","Emily Ford","01234567897","Brighton",3],["EN-009","Robert Stone","01234567898","Bristol",1],["EN-010","David Wood","01234567899","York",2],["EN-011","Susan Blair","01234567900","Bristol",4],["EN-012","George Hunt","01234567901","Bristol",2],["EN-013","Peter Grant","01234567902","Bath",3],["EN-014","Jane Bell","01234567903","York",2],["EN-015","Laura Miles","01234567904","Cambridge",2],["EN-016","Alice Cole","01234567905","Oxford",0],["EN-017","Jane Palmer","01234567906","Oxford",0],["EN-018","William White","01234567907","York",1],["EN-019","Jane Bell","01234567908","York",1],["EN-020","David Wood","01234567909","York",0],["EN-021","John Clark","01234567910","Cambridge",0]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Retrieve information about course instances in which no student from York has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"A","result":{"columns":["course","name","start_date","name","name"],"values":[["DBM1","Database Methodology","2008-04-02T00:00:00.000Z","Sirius","Sarah Taylor"],["Java1","Java, Basic Course","2008-05-06T00:00:00.000Z","Jupiter","John Brown"],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z","Sirius","Sarah Taylor"],["PDB","Physical Database Design","2008-05-09T00:00:00.000Z","Jupiter","Mary Wilson"],["LOG1","Logic","2009-09-17T00:00:00.000Z","Jupiter","Sarah Taylor"],["DBM1","Database Methodology","2010-02-15T00:00:00.000Z","Sirius","Mary Wilson"],["Java1","Java, Basic Course","2010-05-01T00:00:00.000Z","Saturn","William Jones"],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z","Saturn","Mary Wilson"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}},{"id":109,"description":"Retrieve information about course instances in which no student from Bristol has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"B","result":{"columns":["course","name","start_date","name","name"],"values":[["Java1","Java, Basic Course","2008-03-06T00:00:00.000Z","Tellus","John Brown"],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z","Tellus","William Jones"],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z","Jupiter","Sarah Taylor"],["LOG1","Logic","2010-05-01T00:00:00.000Z","Jupiter","John Brown"],["LDD","Logical Database Design","2010-09-24T00:00:00.000Z","Saturn","Mary Wilson"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}},{"id":110,"description":"Retrieve information about course instances in which no student with a disability has participated! Show course_code, course name, start_date, room name, and teacher name!","display_sequence":"C","result":{"columns":["course","name","start_date","name","name"],"values":[["LOG1","Logic","2008-04-16T00:00:00.000Z","Jupiter","William Jones"],["LDD","Logical Database Design","2008-05-06T00:00:00.000Z","Sirius","Sarah Taylor"],["Java1","Java, Basic Course","2008-09-02T00:00:00.000Z","Tellus","William Jones"],["DBM1","Database Methodology","2009-01-28T00:00:00.000Z","Jupiter","Sarah Taylor"],["LOG1","Logic","2010-05-01T00:00:00.000Z","Jupiter","John Brown"],["Java1","Java, Basic Course","2011-05-01T00:00:00.000Z","Saturn","John Brown"]]}}]}]} \ No newline at end of file diff --git a/public/languages/sv-pg/data.sql b/public/languages/sv-pg/data.sql new file mode 100644 index 0000000..b29e12e --- /dev/null +++ b/public/languages/sv-pg/data.sql @@ -0,0 +1,141 @@ +CREATE TABLE Person (personnummer TEXT NOT NULL, namn TEXT NOT NULL, adress TEXT NOT NULL, postnr TEXT NOT NULL, ort TEXT NOT NULL, telefon TEXT NOT NULL, PRIMARY KEY (personnummer)); +CREATE TABLE Rum (id INTEGER NOT NULL, namn TEXT NOT NULL, antalplatser INTEGER NOT NULL, PRIMARY KEY (id)); +CREATE UNIQUE INDEX rum_namn ON Rum (namn); +CREATE TABLE Student (personnummer TEXT NOT NULL, funktionshindrad BOOLEAN NOT NULL, CONSTRAINT STUDENT_PK PRIMARY KEY (personnummer), CONSTRAINT Student_Person_FK FOREIGN KEY (personnummer) REFERENCES Person (personnummer) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Lärare (personnummer TEXT NOT NULL, tjänsterum INTEGER NOT NULL, CONSTRAINT teacher_pk PRIMARY KEY (personnummer), CONSTRAINT fk_teacher_person FOREIGN KEY (personnummer) REFERENCES Person (personnummer) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Kurs (kurskod TEXT NOT NULL, namn TEXT NOT NULL, längd INTEGER NOT NULL, pris INTEGER NOT NULL, beskrivning TEXT NOT NULL, PRIMARY KEY (kurskod)); +CREATE UNIQUE INDEX kurs_ben ON Kurs (namn); +CREATE TABLE Kurstillfälle (kurs TEXT NOT NULL, startdatum DATE NOT NULL, lärare TEXT NOT NULL, rum INTEGER NOT NULL, CONSTRAINT courseinstance_pk PRIMARY KEY (kurs, startdatum), CONSTRAINT fk_courseinstance_course FOREIGN KEY (kurs) REFERENCES Kurs (kurskod) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_teacher FOREIGN KEY (lärare) REFERENCES Lärare (personnummer) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_room FOREIGN KEY (rum) REFERENCES Rum (id) ON DELETE RESTRICT ON UPDATE CASCADE); +CREATE TABLE Deltagande (student TEXT NOT NULL, kurs TEXT NOT NULL, startdatum DATE NOT NULL, CONSTRAINT participation_pk PRIMARY KEY (kurs, startdatum, student), CONSTRAINT fk_participation_courseinstance FOREIGN KEY (kurs, startdatum) REFERENCES Kurstillfälle (kurs, startdatum) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_participation_student FOREIGN KEY (student) REFERENCES Student (personnummer) ON DELETE RESTRICT ON UPDATE CASCADE); +INSERT INTO Person VALUES ('111017-0001', 'Anders Ödman', 'Centralvägen 1', '19122', 'Sollentuna', '151576'); +INSERT INTO Person VALUES ('111017-0002', 'Bo Åkerman', 'Villagatan 2', '18491', 'Åkersberga', '151526'); +INSERT INTO Person VALUES ('111017-0003', 'Carl Nordin', 'Dalgatan 3', '15106', 'Södertälje', '151553'); +INSERT INTO Person VALUES ('111017-0004', 'Lena Svensson', 'Nygatan 4', '18754', 'Täby', '151556'); +INSERT INTO Person VALUES ('111017-0005', 'Sofia Wilsson', 'Björkgatan 5', '15136', 'Södertälje', '151585'); +INSERT INTO Person VALUES ('121017-0001', 'Bo Dahl', 'Ahlgatan 6', '16102', 'Bromma', '163578'); +INSERT INTO Person VALUES ('121017-0002', 'Ann Stål', 'Lindvägen 3', '16429', 'Kista', '373789'); +INSERT INTO Person VALUES ('121017-0003', 'Ebba Ryd', 'Ankvägen 4', '16107', 'Bromma', '325305'); +INSERT INTO Person VALUES ('121017-0004', 'Robert Ahl', 'Ekvägen 1', '16425', 'Kista', '123435'); +INSERT INTO Person VALUES ('121017-0005', 'Lars Holm', 'Skolgatan 3', '16966', 'Solna', '203045'); +INSERT INTO Person VALUES ('121017-0006', 'Siw Björk', 'Bokvägen 2', '16431', 'Kista', '452678'); +INSERT INTO Person VALUES ('121017-0007', 'Sigge Ehn', 'Bokvägen 24', '16429', 'Kista', '245578'); +INSERT INTO Person VALUES ('121017-0008', 'Kurt Grahn', 'Byvägen 112', '19735', 'Bro', '192292'); +INSERT INTO Person VALUES ('121017-0009', 'Eva Jung', 'Storgatan 5', '16966', 'Solna', '131187'); +INSERT INTO Person VALUES ('121017-0010', 'Lola Frid', 'Lillgatan 3', '18754', 'Täby', '723384'); +INSERT INTO Person VALUES ('121018-0040', 'Britt Maj', 'Karlvägen 44', '15106', 'Södertälje', '982355'); +INSERT INTO Person VALUES ('121117-0041', 'Eva Berglund', 'Hamngatan 15b', '15107', 'Södertälje', '774355'); +INSERT INTO Person VALUES ('131017-0050', 'Carl Nilsson', 'Strogatan 4', '16966', 'Solna', '332356'); +INSERT INTO Person VALUES ('131017-0051', 'Eva Jung', 'Skolgatan 12', '16966', 'Solna', '449867'); +INSERT INTO Person VALUES ('131017-0052', 'Lars Holm', 'Skolgatan 1', '16966', 'Solna', '665844'); +INSERT INTO Person VALUES ('131017-0053', 'Bo Dahl', 'Nygatan 63', '18753', 'Täby', '770691'); +INSERT INTO Rum VALUES (1, 'Jupiter', 12); +INSERT INTO Rum VALUES (2, 'Orion', 24); +INSERT INTO Rum VALUES (3, 'Sirius', 16); +INSERT INTO Rum VALUES (4, 'Tellus', 32); +INSERT INTO Rum VALUES (5, 'Saturnus', 24); +INSERT INTO Student VALUES ('111017-0001', FALSE); +INSERT INTO Student VALUES ('121017-0001', FALSE); +INSERT INTO Student VALUES ('121017-0002', TRUE); +INSERT INTO Student VALUES ('121017-0003', FALSE); +INSERT INTO Student VALUES ('121017-0004', FALSE); +INSERT INTO Student VALUES ('121017-0005', FALSE); +INSERT INTO Student VALUES ('121017-0006', TRUE); +INSERT INTO Student VALUES ('121017-0007', FALSE); +INSERT INTO Student VALUES ('121017-0008', FALSE); +INSERT INTO Student VALUES ('121017-0009', FALSE); +INSERT INTO Student VALUES ('121017-0010', TRUE); +INSERT INTO Student VALUES ('121018-0040', FALSE); +INSERT INTO Student VALUES ('121117-0041', TRUE); +INSERT INTO Student VALUES ('131017-0050', FALSE); +INSERT INTO Student VALUES ('131017-0051', FALSE); +INSERT INTO Student VALUES ('131017-0052', TRUE); +INSERT INTO Student VALUES ('131017-0053', FALSE); +INSERT INTO Lärare VALUES ('111017-0001', 634); +INSERT INTO Lärare VALUES ('111017-0002', 604); +INSERT INTO Lärare VALUES ('111017-0003', 603); +INSERT INTO Lärare VALUES ('111017-0004', 605); +INSERT INTO Lärare VALUES ('111017-0005', 622); +INSERT INTO Lärare VALUES ('131017-0050', 558); +INSERT INTO Kurs VALUES ('DBM1', 'Databasmetodik', 2, 2800, 'DB-grunder, normalisering, modellering, SQL'); +INSERT INTO Kurs VALUES ('FDBD', 'Fysisk databasdesign', 5, 7200, 'Lagringsstrukturer, index, denormalisering'); +INSERT INTO Kurs VALUES ('Java1', 'Java, grundkurs', 5, 6700, 'Grundläggande programmering med Java'); +INSERT INTO Kurs VALUES ('Java2', 'Java, fortsättningskurs', 4, 6000, 'Avancerad programmering med Java'); +INSERT INTO Kurs VALUES ('LDBD', 'Logisk databasdesign', 4, 6000, 'Avancerad design, analysmönster, metamodellering'); +INSERT INTO Kurs VALUES ('Log1', 'Logik', 3, 4500, 'Första ordnings logik, predikatlogik, satslogik'); +INSERT INTO Kurstillfälle VALUES ('Java1', '2008-03-06T00:00:00.000Z', '111017-0002', 4); +INSERT INTO Kurstillfälle VALUES ('DBM1', '2008-04-02T00:00:00.000Z', '111017-0005', 3); +INSERT INTO Kurstillfälle VALUES ('Java2', '2008-04-16T00:00:00.000Z', '111017-0002', 2); +INSERT INTO Kurstillfälle VALUES ('Log1', '2008-04-16T00:00:00.000Z', '111017-0003', 1); +INSERT INTO Kurstillfälle VALUES ('Java1', '2008-05-06T00:00:00.000Z', '111017-0002', 1); +INSERT INTO Kurstillfälle VALUES ('LDBD', '2008-05-06T00:00:00.000Z', '111017-0005', 3); +INSERT INTO Kurstillfälle VALUES ('FDBD', '2008-05-09T00:00:00.000Z', '111017-0004', 1); +INSERT INTO Kurstillfälle VALUES ('Java1', '2008-09-02T00:00:00.000Z', '111017-0003', 4); +INSERT INTO Kurstillfälle VALUES ('LDBD', '2008-09-02T00:00:00.000Z', '111017-0004', 2); +INSERT INTO Kurstillfälle VALUES ('Java2', '2009-01-22T00:00:00.000Z', '111017-0002', 2); +INSERT INTO Kurstillfälle VALUES ('DBM1', '2009-01-28T00:00:00.000Z', '111017-0005', 1); +INSERT INTO Kurstillfälle VALUES ('Log1', '2009-09-17T00:00:00.000Z', '111017-0005', 1); +INSERT INTO Kurstillfälle VALUES ('DBM1', '2010-02-15T00:00:00.000Z', '111017-0004', 3); +INSERT INTO Kurstillfälle VALUES ('Java2', '2010-04-11T00:00:00.000Z', '131017-0050', 4); +INSERT INTO Kurstillfälle VALUES ('Java1', '2010-05-01T00:00:00.000Z', '111017-0003', 5); +INSERT INTO Kurstillfälle VALUES ('Log1', '2010-05-01T00:00:00.000Z', '111017-0002', 1); +INSERT INTO Kurstillfälle VALUES ('DBM1', '2010-09-22T00:00:00.000Z', '131017-0050', 3); +INSERT INTO Kurstillfälle VALUES ('LDBD', '2010-09-24T00:00:00.000Z', '111017-0004', 5); +INSERT INTO Kurstillfälle VALUES ('Java1', '2011-05-01T00:00:00.000Z', '111017-0002', 5); +INSERT INTO Deltagande VALUES ('121017-0001', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0003', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0009', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0010', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0051', 'Java1', '2008-03-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0001', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0002', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0003', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0010', 'DBM1', '2008-04-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0002', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0005', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0007', 'Java2', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0004', 'Log1', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0005', 'Log1', '2008-04-16T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0002', 'Java1', '2008-05-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'Java1', '2008-05-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0001', 'LDBD', '2008-05-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0007', 'LDBD', '2008-05-06T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0003', 'FDBD', '2008-05-09T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'FDBD', '2008-05-09T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'FDBD', '2008-05-09T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0001', 'Java1', '2008-09-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0009', 'Java1', '2008-09-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'LDBD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'LDBD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0050', 'LDBD', '2008-09-02T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0002', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0009', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0010', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0051', 'Java2', '2009-01-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0003', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0005', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0051', 'DBM1', '2009-01-28T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0004', 'Log1', '2009-09-17T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121117-0041', 'Log1', '2009-09-17T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0003', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0010', 'DBM1', '2010-02-15T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0004', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0005', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0007', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0010', 'Java2', '2010-04-11T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0006', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0007', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'Java1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0005', 'Log1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'Log1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121018-0040', 'Log1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0053', 'Log1', '2010-05-01T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0004', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121018-0040', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121117-0041', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('131017-0052', 'DBM1', '2010-09-22T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121017-0008', 'LDBD', '2010-09-24T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121018-0040', 'LDBD', '2010-09-24T00:00:00.000Z'); +INSERT INTO Deltagande VALUES ('121117-0041', 'LDBD', '2010-09-24T00:00:00.000Z'); \ No newline at end of file diff --git a/public/languages/sv-pg/db_layout_dark.svg b/public/languages/sv-pg/db_layout_dark.svg new file mode 100644 index 0000000..5cf62b5 --- /dev/null +++ b/public/languages/sv-pg/db_layout_dark.svg @@ -0,0 +1,263 @@ + + + + +SQLiteLayout + + +deltagande + + + +deltagande + + +student +      +TEXT + + +kurs +         +TEXT + + +startdatum +   +DATE + + +    kurs, kurs, startdatum, startdatum     + + + + +kurstillfälle + + + +kurstillfälle + + +kurs +         +TEXT + + +startdatum +   +DATE + + +lärare +       +TEXT + + +rum +          +INTEGER + + +    kurs, startdatum, kurs, startdatum     + + + + +deltagande:f4->kurstillfälle:f15 + + +1 + + + + +student + + + +student + + +personnummer +       +TEXT + + +funktionshindrad +   +BOOLEAN + + + + +deltagande:f1->student:f28 + + +1 + + + + +kurs + + + +kurs + + +kurskod +       +TEXT + + +namn +          +TEXT + + +längd +         +INTEGER + + +pris +          +INTEGER + + +beskrivning +   +TEXT + + +    namn (INDEX, UNIQUE)     + + + + +kurstillfälle:f11->kurs:f5 + + +1 + + + + +lärare + + + +lärare + + +personnummer +   +TEXT + + +tjänsterum +     +INTEGER + + + + +kurstillfälle:f13->lärare:f16 + + +1 + + + + +rum + + + +rum + + +id +             +INTEGER + + +namn +           +TEXT + + +antalplatser +   +INTEGER + + +    namn (INDEX, UNIQUE)     + + + + +kurstillfälle:f14->rum:f24 + + +1 + + + + +person + + + +person + + +personnummer +   +TEXT + + +namn +           +TEXT + + +adress +         +TEXT + + +postnr +         +TEXT + + +ort +            +TEXT + + +telefon +        +TEXT + + + + +lärare:f16->person:f18 + + +1 +1 + + + +student:f28->person:f18 + + +1 +1 + + + \ No newline at end of file diff --git a/public/languages/sv-pg/db_layout_light.svg b/public/languages/sv-pg/db_layout_light.svg new file mode 100644 index 0000000..2ca71d9 --- /dev/null +++ b/public/languages/sv-pg/db_layout_light.svg @@ -0,0 +1,263 @@ + + + + +SQLiteLayout + + +deltagande + + + +deltagande + + +student +      +TEXT + + +kurs +         +TEXT + + +startdatum +   +DATE + + +    kurs, kurs, startdatum, startdatum     + + + + +kurstillfälle + + + +kurstillfälle + + +kurs +         +TEXT + + +startdatum +   +DATE + + +lärare +       +TEXT + + +rum +          +INTEGER + + +    kurs, startdatum, kurs, startdatum     + + + + +deltagande:f4->kurstillfälle:f15 + + +1 + + + + +student + + + +student + + +personnummer +       +TEXT + + +funktionshindrad +   +BOOLEAN + + + + +deltagande:f1->student:f28 + + +1 + + + + +kurs + + + +kurs + + +kurskod +       +TEXT + + +namn +          +TEXT + + +längd +         +INTEGER + + +pris +          +INTEGER + + +beskrivning +   +TEXT + + +    namn (INDEX, UNIQUE)     + + + + +kurstillfälle:f11->kurs:f5 + + +1 + + + + +lärare + + + +lärare + + +personnummer +   +TEXT + + +tjänsterum +     +INTEGER + + + + +kurstillfälle:f13->lärare:f16 + + +1 + + + + +rum + + + +rum + + +id +             +INTEGER + + +namn +           +TEXT + + +antalplatser +   +INTEGER + + +    namn (INDEX, UNIQUE)     + + + + +kurstillfälle:f14->rum:f24 + + +1 + + + + +person + + + +person + + +personnummer +   +TEXT + + +namn +           +TEXT + + +adress +         +TEXT + + +postnr +         +TEXT + + +ort +            +TEXT + + +telefon +        +TEXT + + + + +lärare:f16->person:f18 + + +1 +1 + + + +student:f28->person:f18 + + +1 +1 + + + \ No newline at end of file diff --git a/public/languages/sv-pg/questionpool.json b/public/languages/sv-pg/questionpool.json new file mode 100644 index 0000000..f7df9c4 --- /dev/null +++ b/public/languages/sv-pg/questionpool.json @@ -0,0 +1 @@ +{"language":"sv","engine":"postgresql","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Solna!","display_sequence":"A","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0005","Lars Holm","Skolgatan 3","16966"],["121017-0009","Eva Jung","Storgatan 5","16966"],["131017-0050","Carl Nilsson","Strogatan 4","16966"],["131017-0051","Eva Jung","Skolgatan 12","16966"],["131017-0052","Lars Holm","Skolgatan 1","16966"]]}},{"id":2,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":3,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Kista!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0002","Ann Stål","Lindvägen 3","16429"],["121017-0004","Robert Ahl","Ekvägen 1","16425"],["121017-0006","Siw Björk","Bokvägen 2","16431"],["121017-0007","Sigge Ehn","Bokvägen 24","16429"]]}},{"id":4,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":5,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Solna!","display_sequence":"E","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Lars Holm","Skolgatan 3","16966","203045"],["Eva Jung","Storgatan 5","16966","131187"],["Carl Nilsson","Strogatan 4","16966","332356"],["Eva Jung","Skolgatan 12","16966","449867"],["Lars Holm","Skolgatan 1","16966","665844"]]}},{"id":6,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Kista!","display_sequence":"F","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Ann Stål","Lindvägen 3","16429","373789"],["Robert Ahl","Ekvägen 1","16425","123435"],["Siw Björk","Bokvägen 2","16431","452678"],["Sigge Ehn","Bokvägen 24","16429","245578"]]}},{"id":7,"description":"Ta fram kurskod, namn och pris på kurser på 3 veckor!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["Log1","Logik",4500]]}},{"id":8,"description":"Ta fram kurskod, namn och pris på kurser på 4 veckor!","display_sequence":"H","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Kista!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0009","Eva Jung","Solna"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"]]}},{"id":10,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna eller i Bromma!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":11,"description":"Ta fram personnummer, namn och ort på personer som bor i Kista eller i Täby!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0053","Bo Dahl","Täby"]]}},{"id":12,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista eller i Bromma!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0002","Ann Stål","373789"],["121017-0003","Ebba Ryd","325305"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":13,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Täby!","display_sequence":"E","result":{"columns":["namn","adress","postnr","ort"],"values":[["Lena Svensson","Nygatan 4","18754","Täby"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"],["Bo Dahl","Nygatan 63","18753","Täby"]]}},{"id":14,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Kista eller i Solna!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Lars Holm","Skolgatan 3","16966","Solna"],["Siw Björk","Bokvägen 2","16431","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Eva Jung","Storgatan 5","16966","Solna"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":15,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Bromma!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0004","Lena Svensson","151556"],["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0010","Lola Frid","723384"],["131017-0053","Bo Dahl","770691"]]}},{"id":16,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Täby!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0005","Lars Holm","Solna"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":17,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Södertälje!","display_sequence":"I","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["121017-0010","Lola Frid","723384"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0053","Bo Dahl","770691"]]}},{"id":18,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Södertälje!","display_sequence":"J","result":{"columns":["namn","adress","postnr","ort"],"values":[["Carl Nordin","Dalgatan 3","15106","Södertälje"],["Sofia Wilsson","Björkgatan 5","15136","Södertälje"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":19,"description":"Ta fram kurskod, namn och pris på kurser på 3 eller 4 veckor!","display_sequence":"K","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000],["Log1","Logik",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0003"],["111017-0004"],["111017-0005"]]}},{"id":21,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0004"]]}},{"id":22,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["lärare"],"values":[["111017-0004"],["111017-0005"],["131017-0050"]]}},{"id":23,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0003"]]}},{"id":24,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["lärare"],"values":[["111017-0002"],["131017-0050"]]}},{"id":25,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["lärare"],"values":[["111017-0004"],["111017-0005"],["131017-0050"]]}},{"id":26,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["lärare"],"values":[["111017-0004"],["111017-0005"]]}},{"id":27,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0006"],["121017-0007"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"]]}},{"id":28,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0004"],["121017-0005"],["121017-0006"],["121017-0010"],["121018-0040"],["121117-0041"],["131017-0051"],["131017-0052"]]}},{"id":29,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["121017-0002"],["121017-0004"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0009"],["121017-0010"],["131017-0051"]]}},{"id":30,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod LDBD!","display_sequence":"K","result":{"columns":["student"],"values":[["121017-0001"],["121017-0006"],["121017-0007"],["121017-0008"],["121018-0040"],["121117-0041"],["131017-0050"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":32,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0004","Lena Svensson","Täby"]]}},{"id":33,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0004","Lena Svensson","Nygatan 4","18754"],["111017-0005","Sofia Wilsson","Björkgatan 5","15136"],["131017-0050","Carl Nilsson","Strogatan 4","16966"]]}},{"id":34,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"]]}},{"id":35,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["131017-0050","Carl Nilsson","Solna"]]}},{"id":36,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Carl Nilsson","Strogatan 4","16966","Solna"],["Lena Svensson","Nygatan 4","18754","Täby"],["Sofia Wilsson","Björkgatan 5","15136","Södertälje"]]}},{"id":37,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":38,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 1!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0003","Carl Nordin","Södertälje"],["111017-0004","Lena Svensson","Täby"],["111017-0005","Sofia Wilsson","Södertälje"]]}},{"id":39,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"I","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0002","Bo Åkerman","Villagatan 2","18491"],["111017-0004","Lena Svensson","Nygatan 4","18754"]]}},{"id":40,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java2!","display_sequence":"J","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["131017-0050","Carl Nilsson","332356"]]}},{"id":41,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"K","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["111017-0005","Sofia Wilsson","Södertälje"]]}},{"id":42,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod Java1!","display_sequence":"L","result":{"columns":["namn","adress","postnr","ort"],"values":[["Bo Åkerman","Villagatan 2","18491","Åkersberga"],["Carl Nordin","Dalgatan 3","15106","Södertälje"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0003","Carl Nordin",603],["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}},{"id":44,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0004","Lena Svensson",605]]}},{"id":45,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622],["131017-0050","Carl Nilsson",558]]}},{"id":46,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0003","Carl Nordin",603]]}},{"id":47,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["131017-0050","Carl Nilsson",558]]}},{"id":48,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622],["131017-0050","Carl Nilsson",558]]}},{"id":49,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["121017-0002"],["121017-0003"],["121017-0004"],["121017-0005"],["121017-0006"],["121017-0008"],["121018-0040"],["121117-0041"],["131017-0051"],["131017-0053"]]}},{"id":51,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["121017-0002"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0050"],["131017-0051"]]}},{"id":52,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["121017-0001"],["121017-0003"],["121017-0004"],["121017-0005"],["121017-0007"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"]]}},{"id":53,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0004"],["121017-0006"],["121017-0007"],["121017-0010"],["121018-0040"],["121117-0041"],["131017-0052"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0003","Ebba Ryd","325305"],["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0051","Eva Jung","449867"],["131017-0053","Bo Dahl","770691"]]}},{"id":55,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"]]}},{"id":56,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"],["121017-0008","Kurt Grahn","192292"],["121017-0009","Eva Jung","131187"],["121017-0010","Lola Frid","723384"],["131017-0051","Eva Jung","449867"]]}},{"id":57,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0052","Lars Holm","Solna"]]}},{"id":58,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"E","result":{"columns":["namn","adress","ort","telefon"],"values":[["Ann Stål","Lindvägen 3","Kista","373789"],["Bo Dahl","Nygatan 63","Täby","770691"],["Britt Maj","Karlvägen 44","Södertälje","982355"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Eva Berglund","Hamngatan 15b","Södertälje","774355"],["Eva Jung","Skolgatan 12","Solna","449867"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Lars Holm","Skolgatan 3","Solna","203045"],["Robert Ahl","Ekvägen 1","Kista","123435"],["Siw Björk","Bokvägen 2","Kista","452678"]]}},{"id":59,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Kurt Grahn","Byvägen 112","19735","Bro"],["Lars Holm","Skolgatan 3","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Siw Björk","Bokvägen 2","16431","Kista"]]}},{"id":60,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"G","result":{"columns":["namn","adress","ort","telefon"],"values":[["Bo Dahl","Ahlgatan 6","Bromma","163578"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Eva Jung","Skolgatan 12","Solna","449867"],["Eva Jung","Storgatan 5","Solna","131187"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Lars Holm","Skolgatan 3","Solna","203045"],["Lola Frid","Lillgatan 3","Täby","723384"],["Robert Ahl","Ekvägen 1","Kista","123435"],["Sigge Ehn","Bokvägen 24","Kista","245578"]]}},{"id":61,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"H","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Bo Dahl","Ahlgatan 6","16102","Bromma"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Ebba Ryd","Ankvägen 4","16107","Bromma"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Lars Holm","Skolgatan 1","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Siw Björk","Bokvägen 2","16431","Kista"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Ta fram antal kurstillfällen som varje student har deltagit i! Visa studentens personnummer, studentens namn och antalet!","display_sequence":"A","result":{"columns":["personnummer","namn","count"],"values":[["121017-0006","Siw Björk",6],["121017-0003","Ebba Ryd",5],["121117-0041","Eva Berglund",3],["131017-0053","Bo Dahl",1],["121017-0004","Robert Ahl",4],["131017-0051","Eva Jung",3],["121017-0002","Ann Stål",4],["121017-0001","Bo Dahl",4],["121017-0008","Kurt Grahn",6],["121018-0040","Britt Maj",3],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121017-0005","Lars Holm",5],["131017-0052","Lars Holm",1],["111017-0001","Anders Ödman",0],["121017-0007","Sigge Ehn",4],["131017-0050","Carl Nilsson",1]]},"alternative_results":[{"columns":["personnummer","namn","count"],"values":[["121017-0006","Siw Björk",6],["121017-0003","Ebba Ryd",5],["121117-0041","Eva Berglund",3],["131017-0053","Bo Dahl",1],["121017-0004","Robert Ahl",4],["131017-0051","Eva Jung",3],["121017-0002","Ann Stål",4],["121017-0001","Bo Dahl",4],["121017-0008","Kurt Grahn",6],["121018-0040","Britt Maj",3],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121017-0005","Lars Holm",5],["131017-0052","Lars Holm",1],["121017-0007","Sigge Ehn",4],["131017-0050","Carl Nilsson",1]]}]},{"id":63,"description":"Ta fram antal kurstillfällen som varje rum har använts till! Visa rummets namn, rummets kapacitet och antalet!","display_sequence":"B","result":{"columns":["namn","antalplatser","count"],"values":[["Orion",24,3],["Sirius",16,4],["Saturnus",24,3],["Tellus",32,3],["Jupiter",12,6]]},"alternative_results":[{"columns":["namn","antalplatser","count"],"values":[["Tellus",32,3],["Orion",24,3],["Sirius",16,4],["Saturnus",24,3],["Jupiter",12,6]]}]},{"id":64,"description":"Ta fram antal kurstillfällen som varje kurs har haft! Visa kursens kod, kursens namn och antalet!","display_sequence":"C","result":{"columns":["kurskod","namn","count"],"values":[["Java2","Java, fortsättningskurs",3],["FDBD","Fysisk databasdesign",1],["DBM1","Databasmetodik",4],["Log1","Logik",3],["LDBD","Logisk databasdesign",3],["Java1","Java, grundkurs",5]]},"alternative_results":[{"columns":["kurskod","namn","count"],"values":[["Java2","Java, fortsättningskurs",3],["FDBD","Fysisk databasdesign",1],["DBM1","Databasmetodik",4],["Log1","Logik",3],["LDBD","Logisk databasdesign",3],["Java1","Java, grundkurs",5]]}]},{"id":65,"description":"Ta fram antal kurstillfällen som varje lärare har hållit! Visa lärarens personnummer, lärarens namn och antalet!","display_sequence":"D","result":{"columns":["personnummer","namn","count"],"values":[["111017-0005","Sofia Wilsson",4],["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["111017-0001","Anders Ödman",0],["131017-0050","Carl Nilsson",2]]},"alternative_results":[{"columns":["personnummer","namn","count"],"values":[["111017-0005","Sofia Wilsson",4],["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["131017-0050","Carl Nilsson",2]]}]},{"id":66,"description":"Ta fram antal deltagande studenter för varje kurstillfälle! Visa kurstillfällets kurskod och kursnamn, kurstillfällets startdatum och antalet deltagande studenter!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum","count"],"values":[["DBM1","Databasmetodik","2010-09-22",4],["DBM1","Databasmetodik","2008-04-02",4],["LDBD","Logisk databasdesign","2008-05-06",2],["Java2","Java, fortsättningskurs","2010-04-11",4],["Java1","Java, grundkurs","2010-05-01",3],["Java1","Java, grundkurs","2008-05-06",2],["FDBD","Fysisk databasdesign","2008-05-09",3],["Log1","Logik","2008-04-16",2],["Java1","Java, grundkurs","2008-03-06",6],["Java2","Java, fortsättningskurs","2008-04-16",4],["LDBD","Logisk databasdesign","2008-09-02",3],["Log1","Logik","2010-05-01",4],["Java2","Java, fortsättningskurs","2009-01-22",4],["Java1","Java, grundkurs","2008-09-02",2],["Java1","Java, grundkurs","2011-05-01",0],["Log1","Logik","2009-09-17",2],["LDBD","Logisk databasdesign","2010-09-24",3],["DBM1","Databasmetodik","2009-01-28",3],["DBM1","Databasmetodik","2010-02-15",3]]},"alternative_results":[{"columns":["kurs","namn","startdatum","count"],"values":[["DBM1","Databasmetodik","2010-09-22",4],["DBM1","Databasmetodik","2008-04-02",4],["LDBD","Logisk databasdesign","2008-05-06",2],["Java2","Java, fortsättningskurs","2010-04-11",4],["Java1","Java, grundkurs","2010-05-01",3],["Java1","Java, grundkurs","2008-05-06",2],["FDBD","Fysisk databasdesign","2008-05-09",3],["Log1","Logik","2008-04-16",2],["Java1","Java, grundkurs","2008-03-06",6],["Java2","Java, fortsättningskurs","2008-04-16",4],["LDBD","Logisk databasdesign","2008-09-02",3],["Log1","Logik","2010-05-01",4],["Java2","Java, fortsättningskurs","2009-01-22",4],["Java1","Java, grundkurs","2008-09-02",2],["Log1","Logik","2009-09-17",2],["LDBD","Logisk databasdesign","2010-09-24",3],["DBM1","Databasmetodik","2009-01-28",3],["DBM1","Databasmetodik","2010-02-15",3]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen varje lärare har hållit! II. Ta fram personnummer, namn och telefon på läraren som har hållit flest kurstillfällen!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"]]}},{"id":68,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen per student! II. Ta fram personnummer, namn och telefon på den student som har deltagit i flest kurstillfällen!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"]]}},{"id":69,"description":"I. Ta fram kurskod, namn och längd samt antal kurstillfällen per kurs! II. Ta fram kurskod, namn och längd på den kurs som har haft flest kurstillfällen!","display_sequence":"C","result":{"columns":["kurskod","namn","längd"],"values":[["Java1","Java, grundkurs",5]]}},{"id":70,"description":"I. Ta fram namn och kapacitet samt antal kurstillfällen per rum! II. Ta fram namn och kapacitet på det rum som har använts för flest kurstillfällen!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Ta fram kurskod, kursnamn och startdatum samt antal deltagare per kurstillfälle! II. Ta fram kurskod, kursnamn och startdatum på kurstillfället som har haft flest deltagare!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum"],"values":[["Java1","Java, grundkurs","2008-03-06"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i minst tre OLIKA kurser!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0051","Eva Jung","Solna"]]}},{"id":73,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit minst tre OLIKA kurser!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":74,"description":"Ta fram personnummer, namn och ort på lärare som har hållit kurstillfällen i minst tre OLIKA rum!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0003","Carl Nordin","Södertälje"],["111017-0004","Lena Svensson","Täby"]]}},{"id":75,"description":"Ta fram namn och kapacitet på rum som har använts av minst tre OLIKA lärare!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturnus",24]]}},{"id":76,"description":"Ta fram namn och kapacitet på rum som har använts för kurstillfällen av minst tre OLIKA kurser!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":77,"description":"Ta fram kod, namn och pris på kurser som har hållits i minst tre OLIKA rum!","display_sequence":"F","result":{"columns":["kurskod","namn","pris"],"values":[["Java1","Java, grundkurs",6700],["LDBD","Logisk databasdesign",6000]]}},{"id":78,"description":"Ta fram kod, namn och pris på kurser som har hållits av minst tre OLIKA lärare!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["DBM1","Databasmetodik",2800],["Log1","Logik",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Ta fram namn och ort på personer som inte är lärare!","display_sequence":"A","result":{"columns":["namn","ort"],"values":[["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Kurt Grahn","Bro"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Eva Jung","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":80,"description":"Ta fram namn och ort på studenter som inte deltog i något kurstillfälle med startdatum under 2010!","display_sequence":"B","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Eva Jung","Solna"],["Carl Nilsson","Solna"],["Eva Jung","Solna"]]}},{"id":81,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Java1!","display_sequence":"C","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Carl Nilsson","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":82,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Log1!","display_sequence":"D","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Carl Nilsson","Solna"],["Eva Jung","Solna"],["Lars Holm","Solna"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som aldrig har hållit ett kurstillfälle för kursen \"Java, fortsättningskurs\"!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0001","Anders Ödman",634,"151576"],["111017-0003","Carl Nordin",603,"151553"],["111017-0004","Lena Svensson",605,"151556"],["111017-0005","Sofia Wilsson",622,"151585"]]}},{"id":84,"description":"Ta fram personnummer, namn och ort på funktionshindrade studenter som inte har gått kursen \"Logisk databasdesign\"!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0052","Lars Holm","Solna"]]}},{"id":85,"description":"Ta fram namn och kapacitet på rum som aldrig har använts för ett kurstillfälle av kursen \"Logisk databasdesign\"!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Ta fram namn och kapacitet på rum som aldrig har använts av läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Ta fram personnummer, namn och telefon på studenter som har läst kursen \"Fysisk databasdesign\" eller kursen \"Logisk databasdesign\" (eller båda)!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"],["121017-0008","Kurt Grahn","192292"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0050","Carl Nilsson","332356"]]}},{"id":88,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i kurstillfällen i rum Orion eller i rum Jupiter (eller i båda)!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":89,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kursen \"Logik\" eller kursen \"Databasmetodik\" (eller båda)!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["131017-0050","Carl Nilsson","332356"]]}},{"id":90,"description":"Ta fram namn och kapacitet på rum som har använts av läraren Bo Åkerman eller av läraren Carl Nordin (eller av båda)!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Saturnus",24],["Tellus",32]]}},{"id":91,"description":"Ta fram namn och kapacitet på rum som har använts för kursen \"Logik\" eller för kursen \"Java, fortsättningskurs\" (eller för båda)!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Ta fram personnummer, namn och ort på studenter som har läst både kursen \"Fysisk databasdesign\" och kursen \"Logisk databasdesign\"!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0008","Kurt Grahn","Bro"],["121017-0006","Siw Björk","Kista"]]}},{"id":93,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen både i rum Orion och i rum Jupiter!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0006","Siw Björk","452678"],["121017-0005","Lars Holm","203045"],["131017-0051","Eva Jung","449867"],["121017-0008","Kurt Grahn","192292"]]}},{"id":94,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har hållit både kursen \"Logisk databasdesign\" och kursen \"Databasmetodik\"!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605]]}},{"id":95,"description":"Ta fram namn och kapacitet på rum som har använts av både läraren Lena Svensson och läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Orion",24],["Saturnus",24],["Jupiter",12]]}},{"id":96,"description":"Ta fram namn och kapacitet på rum som har använts för både kursen \"Logik\" och kursen \"Java, grundkurs\"!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":97,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som har hållit kurstillfällen både i rum Jupiter och i rum Sirius!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0005","Sofia Wilsson",622,"151585"],["111017-0004","Lena Svensson",605,"151556"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen ledda av samtliga lärare från Södertälje!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"]]}},{"id":99,"description":"Ta fram namn och kapacitet på rum som har använts av samtliga lärare från Södertälje!","display_sequence":"B","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":100,"description":"Ta fram namn och kapacitet på rum som har använts för samtliga kurser som kostar under 5000!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":101,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kurstillfällen av samtliga kurser på högst tre veckor!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0005","Sofia Wilsson","151585"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Ta fram antal studenter från Kista för varje kurstillfälle! Visa kurskod, startdatum och antal studenter! Även kurstillfällen utan studenter från Kista ska visas!","display_sequence":"A","result":{"columns":["kurs","startdatum","count"],"values":[["Java1","2008-03-06",0],["LDBD","2010-09-24",0],["LDBD","2008-09-02",1],["Java1","2008-05-06",2],["Log1","2009-09-17",1],["Java2","2010-04-11",2],["FDBD","2008-05-09",1],["DBM1","2008-04-02",1],["Java2","2008-04-16",3],["LDBD","2008-05-06",1],["Log1","2010-05-01",0],["Java1","2010-05-01",2],["DBM1","2010-09-22",1],["DBM1","2009-01-28",0],["Java2","2009-01-22",1],["Java1","2011-05-01",0],["Log1","2008-04-16",1],["Java1","2008-09-02",0],["DBM1","2010-02-15",1]]}},{"id":103,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Logisk databasdesign\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"B","result":{"columns":["personnummer","namn","ort","count"],"values":[["121017-0006","Siw Björk","Kista",1],["121017-0003","Ebba Ryd","Bromma",0],["121117-0041","Eva Berglund","Södertälje",1],["131017-0053","Bo Dahl","Täby",0],["121017-0004","Robert Ahl","Kista",0],["131017-0051","Eva Jung","Solna",0],["121017-0002","Ann Stål","Kista",0],["121017-0001","Bo Dahl","Bromma",1],["121017-0008","Kurt Grahn","Bro",2],["121018-0040","Britt Maj","Södertälje",1],["121017-0009","Eva Jung","Solna",0],["121017-0010","Lola Frid","Täby",0],["121017-0005","Lars Holm","Solna",0],["131017-0052","Lars Holm","Solna",0],["111017-0001","Anders Ödman","Sollentuna",0],["121017-0007","Sigge Ehn","Kista",1],["131017-0050","Carl Nilsson","Solna",1]]}},{"id":104,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Java, grundkurs\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"C","result":{"columns":["personnummer","namn","ort","count"],"values":[["121017-0006","Siw Björk","Kista",2],["121017-0003","Ebba Ryd","Bromma",1],["121117-0041","Eva Berglund","Södertälje",0],["131017-0053","Bo Dahl","Täby",0],["121017-0004","Robert Ahl","Kista",0],["131017-0051","Eva Jung","Solna",1],["121017-0002","Ann Stål","Kista",1],["121017-0001","Bo Dahl","Bromma",2],["121017-0008","Kurt Grahn","Bro",2],["121018-0040","Britt Maj","Södertälje",0],["121017-0009","Eva Jung","Solna",2],["121017-0010","Lola Frid","Täby",1],["121017-0005","Lars Holm","Solna",0],["131017-0052","Lars Holm","Solna",0],["111017-0001","Anders Ödman","Sollentuna",0],["121017-0007","Sigge Ehn","Kista",1],["131017-0050","Carl Nilsson","Solna",0]]}},{"id":105,"description":"Ta fram antal gånger som varje lärare har hållit kursen \"Java, grundkurs\"! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kursen ska visas!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon","tjänsterum","count"],"values":[["131017-0050","Carl Nilsson","332356",558,0],["111017-0003","Carl Nordin","151553",603,2],["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,3],["111017-0005","Sofia Wilsson","151585",622,0],["111017-0004","Lena Svensson","151556",605,0]]}},{"id":106,"description":"Ta fram antal kurstillfällen som varje lärare har hållit med start under år 2010! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kurstillfällen under 2010 ska visas!","display_sequence":"E","result":{"columns":["personnummer","namn","telefon","tjänsterum","count"],"values":[["131017-0050","Carl Nilsson","332356",558,2],["111017-0003","Carl Nordin","151553",603,1],["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,1],["111017-0005","Sofia Wilsson","151585",622,0],["111017-0004","Lena Svensson","151556",605,2]]}},{"id":107,"description":"Ta fram antal kurstillfällen med start under år 2008 för varje student! Visa studentens personnummer, namn, telefon och ort samt antalet! Även studenter som inte har deltagit i kurstillfällen under 2008 ska visas!","display_sequence":"F","result":{"columns":["personnummer","namn","telefon","ort","count"],"values":[["121017-0006","Siw Björk","452678","Kista",4],["121017-0003","Ebba Ryd","325305","Bromma",3],["121117-0041","Eva Berglund","774355","Södertälje",0],["131017-0053","Bo Dahl","770691","Täby",0],["121017-0004","Robert Ahl","123435","Kista",1],["131017-0051","Eva Jung","449867","Solna",1],["121017-0002","Ann Stål","373789","Kista",3],["121017-0001","Bo Dahl","163578","Bromma",4],["121017-0008","Kurt Grahn","192292","Bro",3],["121018-0040","Britt Maj","982355","Södertälje",0],["121017-0009","Eva Jung","131187","Solna",2],["121017-0010","Lola Frid","723384","Täby",2],["121017-0005","Lars Holm","203045","Solna",2],["131017-0052","Lars Holm","665844","Solna",0],["111017-0001","Anders Ödman","151576","Sollentuna",0],["121017-0007","Sigge Ehn","245578","Kista",2],["131017-0050","Carl Nilsson","332356","Solna",1]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Ta fram information om kurstillfällen som ingen student från Solna har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"A","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["DBM1","Sofia Wilsson","2008-04-02","Sofia Wilsson","Sofia Wilsson"],["Java1","Bo Åkerman","2008-05-06","Bo Åkerman","Bo Åkerman"],["LDBD","Sofia Wilsson","2008-05-06","Sofia Wilsson","Sofia Wilsson"],["FDBD","Lena Svensson","2008-05-09","Lena Svensson","Lena Svensson"],["Log1","Sofia Wilsson","2009-09-17","Sofia Wilsson","Sofia Wilsson"],["DBM1","Lena Svensson","2010-02-15","Lena Svensson","Lena Svensson"],["Java1","Carl Nordin","2010-05-01","Carl Nordin","Carl Nordin"],["LDBD","Lena Svensson","2010-09-24","Lena Svensson","Lena Svensson"],["Java1","Bo Åkerman","2011-05-01","Bo Åkerman","Bo Åkerman"]]}},{"id":109,"description":"Ta fram information om kurstillfällen som ingen student från Kista har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"B","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Java1","Bo Åkerman","2008-03-06","Bo Åkerman","Bo Åkerman"],["Java1","Carl Nordin","2008-09-02","Carl Nordin","Carl Nordin"],["DBM1","Sofia Wilsson","2009-01-28","Sofia Wilsson","Sofia Wilsson"],["Log1","Bo Åkerman","2010-05-01","Bo Åkerman","Bo Åkerman"],["LDBD","Lena Svensson","2010-09-24","Lena Svensson","Lena Svensson"],["Java1","Bo Åkerman","2011-05-01","Bo Åkerman","Bo Åkerman"]]}},{"id":110,"description":"Ta fram information om kurstillfällen som ingen funktionshindrad student har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"C","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Log1","Carl Nordin","2008-04-16","Carl Nordin","Carl Nordin"],["LDBD","Sofia Wilsson","2008-05-06","Sofia Wilsson","Sofia Wilsson"],["Java1","Carl Nordin","2008-09-02","Carl Nordin","Carl Nordin"],["DBM1","Sofia Wilsson","2009-01-28","Sofia Wilsson","Sofia Wilsson"],["Log1","Bo Åkerman","2010-05-01","Bo Åkerman","Bo Åkerman"],["Java1","Bo Åkerman","2011-05-01","Bo Åkerman","Bo Åkerman"]]}}]}]} \ No newline at end of file diff --git a/public/languages/sv/questionpool.json b/public/languages/sv/questionpool.json index 3c51416..e1d39ff 100644 --- a/public/languages/sv/questionpool.json +++ b/public/languages/sv/questionpool.json @@ -1 +1 @@ -{"language":"sv","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Solna!","display_sequence":"A","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0005","Lars Holm","Skolgatan 3","16966"],["121017-0009","Eva Jung","Storgatan 5","16966"],["131017-0050","Carl Nilsson","Strogatan 4","16966"],["131017-0051","Eva Jung","Skolgatan 12","16966"],["131017-0052","Lars Holm","Skolgatan 1","16966"]]}},{"id":2,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":3,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Kista!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0002","Ann Stål","Lindvägen 3","16429"],["121017-0004","Robert Ahl","Ekvägen 1","16425"],["121017-0006","Siw Björk","Bokvägen 2","16431"],["121017-0007","Sigge Ehn","Bokvägen 24","16429"]]}},{"id":4,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":5,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Solna!","display_sequence":"E","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Lars Holm","Skolgatan 3","16966","203045"],["Eva Jung","Storgatan 5","16966","131187"],["Carl Nilsson","Strogatan 4","16966","332356"],["Eva Jung","Skolgatan 12","16966","449867"],["Lars Holm","Skolgatan 1","16966","665844"]]}},{"id":6,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Kista!","display_sequence":"F","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Ann Stål","Lindvägen 3","16429","373789"],["Robert Ahl","Ekvägen 1","16425","123435"],["Siw Björk","Bokvägen 2","16431","452678"],["Sigge Ehn","Bokvägen 24","16429","245578"]]}},{"id":7,"description":"Ta fram kurskod, namn och pris på kurser på 3 veckor!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["Log1","Logik",4500]]}},{"id":8,"description":"Ta fram kurskod, namn och pris på kurser på 4 veckor!","display_sequence":"H","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Kista!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0009","Eva Jung","Solna"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"]]}},{"id":10,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna eller i Bromma!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":11,"description":"Ta fram personnummer, namn och ort på personer som bor i Kista eller i Täby!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0053","Bo Dahl","Täby"]]}},{"id":12,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista eller i Bromma!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0002","Ann Stål","373789"],["121017-0003","Ebba Ryd","325305"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":13,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Täby!","display_sequence":"E","result":{"columns":["namn","adress","postnr","ort"],"values":[["Lena Svensson","Nygatan 4","18754","Täby"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"],["Bo Dahl","Nygatan 63","18753","Täby"]]}},{"id":14,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Kista eller i Solna!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Lars Holm","Skolgatan 3","16966","Solna"],["Siw Björk","Bokvägen 2","16431","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Eva Jung","Storgatan 5","16966","Solna"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":15,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Bromma!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0004","Lena Svensson","151556"],["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0010","Lola Frid","723384"],["131017-0053","Bo Dahl","770691"]]}},{"id":16,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Täby!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0005","Lars Holm","Solna"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":17,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Södertälje!","display_sequence":"I","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["121017-0010","Lola Frid","723384"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0053","Bo Dahl","770691"]]}},{"id":18,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Södertälje!","display_sequence":"J","result":{"columns":["namn","adress","postnr","ort"],"values":[["Carl Nordin","Dalgatan 3","15106","Södertälje"],["Sofia Wilsson","Björkgatan 5","15136","Södertälje"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":19,"description":"Ta fram kurskod, namn och pris på kurser på 3 eller 4 veckor!","display_sequence":"K","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000],["Log1","Logik",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["lärare"],"values":[["111017-0003"],["111017-0002"],["111017-0004"],["111017-0005"]]}},{"id":21,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0004"]]}},{"id":22,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"],["131017-0050"]]}},{"id":23,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0003"]]}},{"id":24,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["lärare"],"values":[["111017-0002"],["131017-0050"]]}},{"id":25,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"],["131017-0050"]]}},{"id":26,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"]]}},{"id":27,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["121017-0001"],["121017-0003"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0002"],["121017-0006"],["121017-0007"]]}},{"id":28,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0010"],["121017-0005"],["131017-0051"],["121017-0006"],["121017-0004"],["121018-0040"],["121117-0041"],["131017-0052"]]}},{"id":29,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["121017-0002"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0004"]]}},{"id":30,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod LDBD!","display_sequence":"K","result":{"columns":["student"],"values":[["121017-0001"],["121017-0007"],["121017-0006"],["121017-0008"],["131017-0050"],["121018-0040"],["121117-0041"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0003","Carl Nordin","151553"],["111017-0002","Bo Åkerman","151526"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":32,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0004","Lena Svensson","Täby"]]}},{"id":33,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0005","Sofia Wilsson","Björkgatan 5","15136"],["111017-0004","Lena Svensson","Nygatan 4","18754"],["131017-0050","Carl Nilsson","Strogatan 4","16966"]]}},{"id":34,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"]]}},{"id":35,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["131017-0050","Carl Nilsson","Solna"]]}},{"id":36,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Sofia Wilsson","Björkgatan 5","15136","Södertälje"],["Lena Svensson","Nygatan 4","18754","Täby"],["Carl Nilsson","Strogatan 4","16966","Solna"]]}},{"id":37,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0005","Sofia Wilsson","151585"],["111017-0004","Lena Svensson","151556"]]}},{"id":38,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 1!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0003","Carl Nordin","Södertälje"],["111017-0002","Bo Åkerman","Åkersberga"],["111017-0004","Lena Svensson","Täby"],["111017-0005","Sofia Wilsson","Södertälje"]]}},{"id":39,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"I","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0002","Bo Åkerman","Villagatan 2","18491"],["111017-0004","Lena Svensson","Nygatan 4","18754"]]}},{"id":40,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java2!","display_sequence":"J","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["131017-0050","Carl Nilsson","332356"]]}},{"id":41,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"K","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0005","Sofia Wilsson","Södertälje"],["111017-0004","Lena Svensson","Täby"]]}},{"id":42,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod Java1!","display_sequence":"L","result":{"columns":["namn","adress","postnr","ort"],"values":[["Bo Åkerman","Villagatan 2","18491","Åkersberga"],["Carl Nordin","Dalgatan 3","15106","Södertälje"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0003","Carl Nordin",603],["111017-0002","Bo Åkerman",604],["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}},{"id":44,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0004","Lena Svensson",605]]}},{"id":45,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605],["131017-0050","Carl Nilsson",558]]}},{"id":46,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0003","Carl Nordin",603]]}},{"id":47,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["131017-0050","Carl Nilsson",558]]}},{"id":48,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605],["131017-0050","Carl Nilsson",558]]}},{"id":49,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["121017-0004"],["121017-0005"],["121017-0002"],["121017-0006"],["121017-0003"],["121017-0008"],["131017-0051"],["121117-0041"],["121018-0040"],["131017-0053"]]}},{"id":51,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["121017-0002"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0008"],["131017-0050"],["121017-0009"],["121017-0010"],["131017-0051"]]}},{"id":52,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["121017-0001"],["121017-0003"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0004"],["121017-0005"],["121017-0007"]]}},{"id":53,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0010"],["121017-0007"],["121017-0006"],["121017-0004"],["121018-0040"],["121117-0041"],["131017-0052"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0002","Ann Stål","373789"],["121017-0006","Siw Björk","452678"],["121017-0003","Ebba Ryd","325305"],["121017-0008","Kurt Grahn","192292"],["131017-0051","Eva Jung","449867"],["121117-0041","Eva Berglund","774355"],["121018-0040","Britt Maj","982355"],["131017-0053","Bo Dahl","770691"]]}},{"id":55,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["131017-0050","Carl Nilsson","Solna"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0051","Eva Jung","Solna"]]}},{"id":56,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0008","Kurt Grahn","192292"],["121017-0009","Eva Jung","131187"],["121017-0010","Lola Frid","723384"],["131017-0051","Eva Jung","449867"],["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"]]}},{"id":57,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0010","Lola Frid","Täby"],["121017-0007","Sigge Ehn","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0004","Robert Ahl","Kista"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0052","Lars Holm","Solna"]]}},{"id":58,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"E","result":{"columns":["namn","adress","ort","telefon"],"values":[["Robert Ahl","Ekvägen 1","Kista","123435"],["Lars Holm","Skolgatan 3","Solna","203045"],["Ann Stål","Lindvägen 3","Kista","373789"],["Siw Björk","Bokvägen 2","Kista","452678"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Eva Jung","Skolgatan 12","Solna","449867"],["Eva Berglund","Hamngatan 15b","Södertälje","774355"],["Britt Maj","Karlvägen 44","Södertälje","982355"],["Bo Dahl","Nygatan 63","Täby","770691"]]}},{"id":59,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Lars Holm","Skolgatan 3","16966","Solna"],["Siw Björk","Bokvägen 2","16431","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Kurt Grahn","Byvägen 112","19735","Bro"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Eva Jung","Skolgatan 12","16966","Solna"]]}},{"id":60,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"G","result":{"columns":["namn","adress","ort","telefon"],"values":[["Bo Dahl","Ahlgatan 6","Bromma","163578"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Eva Jung","Storgatan 5","Solna","131187"],["Lola Frid","Lillgatan 3","Täby","723384"],["Eva Jung","Skolgatan 12","Solna","449867"],["Robert Ahl","Ekvägen 1","Kista","123435"],["Lars Holm","Skolgatan 3","Solna","203045"],["Sigge Ehn","Bokvägen 24","Kista","245578"]]}},{"id":61,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"H","result":{"columns":["namn","adress","postnr","ort"],"values":[["Bo Dahl","Ahlgatan 6","16102","Bromma"],["Ann Stål","Lindvägen 3","16429","Kista"],["Ebba Ryd","Ankvägen 4","16107","Bromma"],["Lola Frid","Lillgatan 3","18754","Täby"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Siw Björk","Bokvägen 2","16431","Kista"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Lars Holm","Skolgatan 1","16966","Solna"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Ta fram antal kurstillfällen som varje student har deltagit i! Visa studentens personnummer, studentens namn och antalet!","display_sequence":"A","result":{"columns":["personnummer","namn","antal"],"values":[["111017-0001","Anders Ödman",0],["121017-0001","Bo Dahl",4],["121017-0002","Ann Stål",4],["121017-0003","Ebba Ryd",5],["121017-0004","Robert Ahl",4],["121017-0005","Lars Holm",5],["121017-0006","Siw Björk",6],["121017-0007","Sigge Ehn",4],["121017-0008","Kurt Grahn",6],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121018-0040","Britt Maj",3],["121117-0041","Eva Berglund",3],["131017-0050","Carl Nilsson",1],["131017-0051","Eva Jung",3],["131017-0052","Lars Holm",1],["131017-0053","Bo Dahl",1]]},"alternative_results":[{"columns":["personnummer","namn","antal"],"values":[["121017-0001","Bo Dahl",4],["121017-0002","Ann Stål",4],["121017-0003","Ebba Ryd",5],["121017-0004","Robert Ahl",4],["121017-0005","Lars Holm",5],["121017-0006","Siw Björk",6],["121017-0007","Sigge Ehn",4],["121017-0008","Kurt Grahn",6],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121018-0040","Britt Maj",3],["121117-0041","Eva Berglund",3],["131017-0050","Carl Nilsson",1],["131017-0051","Eva Jung",3],["131017-0052","Lars Holm",1],["131017-0053","Bo Dahl",1]]}]},{"id":63,"description":"Ta fram antal kurstillfällen som varje rum har använts till! Visa rummets namn, rummets kapacitet och antalet!","display_sequence":"B","result":{"columns":["namn","antalplatser","antal"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturnus",24,3]]},"alternative_results":[{"columns":["namn","antalplatser","antal"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturnus",24,3]]}]},{"id":64,"description":"Ta fram antal kurstillfällen som varje kurs har haft! Visa kursens kod, kursens namn och antalet!","display_sequence":"C","result":{"columns":["kurskod","namn","antal"],"values":[["DBM1","Databasmetodik",4],["FDBD","Fysisk databasdesign",1],["Java1","Java, grundkurs",5],["Java2","Java, fortsättningskurs",3],["LDBD","Logisk databasdesign",3],["Log1","Logik",3]]},"alternative_results":[{"columns":["kurskod","namn","antal"],"values":[["DBM1","Databasmetodik",4],["FDBD","Fysisk databasdesign",1],["Java1","Java, grundkurs",5],["Java2","Java, fortsättningskurs",3],["LDBD","Logisk databasdesign",3],["Log1","Logik",3]]}]},{"id":65,"description":"Ta fram antal kurstillfällen som varje lärare har hållit! Visa lärarens personnummer, lärarens namn och antalet!","display_sequence":"D","result":{"columns":["personnummer","namn","antal"],"values":[["111017-0001","Anders Ödman",0],["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["111017-0005","Sofia Wilsson",4],["131017-0050","Carl Nilsson",2]]},"alternative_results":[{"columns":["personnummer","namn","antal"],"values":[["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["111017-0005","Sofia Wilsson",4],["131017-0050","Carl Nilsson",2]]}]},{"id":66,"description":"Ta fram antal deltagande studenter för varje kurstillfälle! Visa kurstillfällets kurskod och kursnamn, kurstillfällets startdatum och antalet deltagande studenter!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum","antal"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z",4],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-09-22T00:00:00.000Z",4],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z",3],["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z",6],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z",2],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z",2],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z",3],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z",0],["Java2","Java, fortsättningskurs","2008-04-16T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2009-01-22T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2010-04-11T00:00:00.000Z",4],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z",2],["LDBD","Logisk databasdesign","2008-09-02T00:00:00.000Z",3],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z",3],["Log1","Logik","2008-04-16T00:00:00.000Z",2],["Log1","Logik","2009-09-17T00:00:00.000Z",2],["Log1","Logik","2010-05-01T00:00:00.000Z",4]]},"alternative_results":[{"columns":["kurs","namn","startdatum","antal"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z",4],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-09-22T00:00:00.000Z",4],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z",3],["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z",6],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z",2],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z",2],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z",3],["Java2","Java, fortsättningskurs","2008-04-16T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2009-01-22T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2010-04-11T00:00:00.000Z",4],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z",2],["LDBD","Logisk databasdesign","2008-09-02T00:00:00.000Z",3],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z",3],["Log1","Logik","2008-04-16T00:00:00.000Z",2],["Log1","Logik","2009-09-17T00:00:00.000Z",2],["Log1","Logik","2010-05-01T00:00:00.000Z",4]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen varje lärare har hållit! II. Ta fram personnummer, namn och telefon på läraren som har hållit flest kurstillfällen!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"]]}},{"id":68,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen per student! II. Ta fram personnummer, namn och telefon på den student som har deltagit i flest kurstillfällen!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"]]}},{"id":69,"description":"I. Ta fram kurskod, namn och längd samt antal kurstillfällen per kurs! II. Ta fram kurskod, namn och längd på den kurs som har haft flest kurstillfällen!","display_sequence":"C","result":{"columns":["kurskod","namn","längd"],"values":[["Java1","Java, grundkurs",5]]}},{"id":70,"description":"I. Ta fram namn och kapacitet samt antal kurstillfällen per rum! II. Ta fram namn och kapacitet på det rum som har använts för flest kurstillfällen!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Ta fram kurskod, kursnamn och startdatum samt antal deltagare per kurstillfälle! II. Ta fram kurskod, kursnamn och startdatum på kurstillfället som har haft flest deltagare!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum"],"values":[["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i minst tre OLIKA kurser!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0051","Eva Jung","Solna"]]}},{"id":73,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit minst tre OLIKA kurser!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":74,"description":"Ta fram personnummer, namn och ort på lärare som har hållit kurstillfällen i minst tre OLIKA rum!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0003","Carl Nordin","Södertälje"],["111017-0004","Lena Svensson","Täby"]]}},{"id":75,"description":"Ta fram namn och kapacitet på rum som har använts av minst tre OLIKA lärare!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturnus",24]]}},{"id":76,"description":"Ta fram namn och kapacitet på rum som har använts för kurstillfällen av minst tre OLIKA kurser!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":77,"description":"Ta fram kod, namn och pris på kurser som har hållits i minst tre OLIKA rum!","display_sequence":"F","result":{"columns":["kurskod","namn","pris"],"values":[["Java1","Java, grundkurs",6700],["LDBD","Logisk databasdesign",6000]]}},{"id":78,"description":"Ta fram kod, namn och pris på kurser som har hållits av minst tre OLIKA lärare!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["DBM1","Databasmetodik",2800],["Log1","Logik",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Ta fram namn och ort på personer som inte är lärare!","display_sequence":"A","result":{"columns":["namn","ort"],"values":[["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Kurt Grahn","Bro"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Eva Jung","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":80,"description":"Ta fram namn och ort på studenter som inte deltog i något kurstillfälle med startdatum under 2010!","display_sequence":"B","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Eva Jung","Solna"],["Carl Nilsson","Solna"],["Eva Jung","Solna"]]}},{"id":81,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Java1!","display_sequence":"C","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Carl Nilsson","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":82,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Log1!","display_sequence":"D","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Carl Nilsson","Solna"],["Eva Jung","Solna"],["Lars Holm","Solna"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som aldrig har hållit ett kurstillfälle för kursen \"Java, fortsättningskurs\"!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0001","Anders Ödman",634,"151576"],["111017-0003","Carl Nordin",603,"151553"],["111017-0004","Lena Svensson",605,"151556"],["111017-0005","Sofia Wilsson",622,"151585"]]}},{"id":84,"description":"Ta fram personnummer, namn och ort på funktionshindrade studenter som inte har gått kursen \"Logisk databasdesign\"!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0052","Lars Holm","Solna"]]}},{"id":85,"description":"Ta fram namn och kapacitet på rum som aldrig har använts för ett kurstillfälle av kursen \"Logisk databasdesign\"!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Ta fram namn och kapacitet på rum som aldrig har använts av läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Ta fram personnummer, namn och telefon på studenter som har läst kursen \"Fysisk databasdesign\" eller kursen \"Logisk databasdesign\" (eller båda)!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"],["121017-0008","Kurt Grahn","192292"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0050","Carl Nilsson","332356"]]}},{"id":88,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i kurstillfällen i rum Orion eller i rum Jupiter (eller i båda)!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":89,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kursen \"Logik\" eller kursen \"Databasmetodik\" (eller båda)!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["131017-0050","Carl Nilsson","332356"]]}},{"id":90,"description":"Ta fram namn och kapacitet på rum som har använts av läraren Bo Åkerman eller av läraren Carl Nordin (eller av båda)!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Saturnus",24],["Tellus",32]]}},{"id":91,"description":"Ta fram namn och kapacitet på rum som har använts för kursen \"Logik\" eller för kursen \"Java, fortsättningskurs\" (eller för båda)!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Ta fram personnummer, namn och ort på studenter som har läst både kursen \"Fysisk databasdesign\" och kursen \"Logisk databasdesign\"!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0006","Siw Björk","Kista"],["121017-0008","Kurt Grahn","Bro"]]}},{"id":93,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen både i rum Orion och i rum Jupiter!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0005","Lars Holm","203045"],["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"],["131017-0051","Eva Jung","449867"]]}},{"id":94,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har hållit både kursen \"Logisk databasdesign\" och kursen \"Databasmetodik\"!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}},{"id":95,"description":"Ta fram namn och kapacitet på rum som har använts av både läraren Lena Svensson och läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Saturnus",24]]}},{"id":96,"description":"Ta fram namn och kapacitet på rum som har använts för både kursen \"Logik\" och kursen \"Java, grundkurs\"!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":97,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som har hållit kurstillfällen både i rum Jupiter och i rum Sirius!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0004","Lena Svensson",605,"151556"],["111017-0005","Sofia Wilsson",622,"151585"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen ledda av samtliga lärare från Södertälje!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"],["121017-0004","Robert Ahl","123435"]]}},{"id":99,"description":"Ta fram namn och kapacitet på rum som har använts av samtliga lärare från Södertälje!","display_sequence":"B","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":100,"description":"Ta fram namn och kapacitet på rum som har använts för samtliga kurser som kostar under 5000!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":101,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kurstillfällen av samtliga kurser på högst tre veckor!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0005","Sofia Wilsson","151585"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Ta fram antal studenter från Kista för varje kurstillfälle! Visa kurskod, startdatum och antal studenter! Även kurstillfällen utan studenter från Kista ska visas!","display_sequence":"A","result":{"columns":["kurs","startdatum","antal"],"values":[["DBM1","2008-04-02T00:00:00.000Z",1],["DBM1","2009-01-28T00:00:00.000Z",0],["DBM1","2010-02-15T00:00:00.000Z",1],["DBM1","2010-09-22T00:00:00.000Z",1],["FDBD","2008-05-09T00:00:00.000Z",1],["Java1","2008-03-06T00:00:00.000Z",0],["Java1","2008-05-06T00:00:00.000Z",2],["Java1","2008-09-02T00:00:00.000Z",0],["Java1","2010-05-01T00:00:00.000Z",2],["Java1","2011-05-01T00:00:00.000Z",0],["Java2","2008-04-16T00:00:00.000Z",3],["Java2","2009-01-22T00:00:00.000Z",1],["Java2","2010-04-11T00:00:00.000Z",2],["LDBD","2008-05-06T00:00:00.000Z",1],["LDBD","2008-09-02T00:00:00.000Z",1],["LDBD","2010-09-24T00:00:00.000Z",0],["Log1","2008-04-16T00:00:00.000Z",1],["Log1","2009-09-17T00:00:00.000Z",1],["Log1","2010-05-01T00:00:00.000Z",0]]}},{"id":103,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Logisk databasdesign\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"B","result":{"columns":["personnummer","namn","ort","antal"],"values":[["111017-0001","Anders Ödman","Sollentuna",0],["121017-0001","Bo Dahl","Bromma",1],["121017-0002","Ann Stål","Kista",0],["121017-0003","Ebba Ryd","Bromma",0],["121017-0004","Robert Ahl","Kista",0],["121017-0005","Lars Holm","Solna",0],["121017-0006","Siw Björk","Kista",1],["121017-0007","Sigge Ehn","Kista",1],["121017-0008","Kurt Grahn","Bro",2],["121017-0009","Eva Jung","Solna",0],["121017-0010","Lola Frid","Täby",0],["121018-0040","Britt Maj","Södertälje",1],["121117-0041","Eva Berglund","Södertälje",1],["131017-0050","Carl Nilsson","Solna",1],["131017-0051","Eva Jung","Solna",0],["131017-0052","Lars Holm","Solna",0],["131017-0053","Bo Dahl","Täby",0]]}},{"id":104,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Java, grundkurs\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"C","result":{"columns":["personnummer","namn","ort","antal"],"values":[["111017-0001","Anders Ödman","Sollentuna",0],["121017-0001","Bo Dahl","Bromma",2],["121017-0002","Ann Stål","Kista",1],["121017-0003","Ebba Ryd","Bromma",1],["121017-0004","Robert Ahl","Kista",0],["121017-0005","Lars Holm","Solna",0],["121017-0006","Siw Björk","Kista",2],["121017-0007","Sigge Ehn","Kista",1],["121017-0008","Kurt Grahn","Bro",2],["121017-0009","Eva Jung","Solna",2],["121017-0010","Lola Frid","Täby",1],["121018-0040","Britt Maj","Södertälje",0],["121117-0041","Eva Berglund","Södertälje",0],["131017-0050","Carl Nilsson","Solna",0],["131017-0051","Eva Jung","Solna",1],["131017-0052","Lars Holm","Solna",0],["131017-0053","Bo Dahl","Täby",0]]}},{"id":105,"description":"Ta fram antal gånger som varje lärare har hållit kursen \"Java, grundkurs\"! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kursen ska visas!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon","tjänsterum","antal"],"values":[["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,3],["111017-0003","Carl Nordin","151553",603,2],["111017-0004","Lena Svensson","151556",605,0],["111017-0005","Sofia Wilsson","151585",622,0],["131017-0050","Carl Nilsson","332356",558,0]]}},{"id":106,"description":"Ta fram antal kurstillfällen som varje lärare har hållit med start under år 2010! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kurstillfällen under 2010 ska visas!","display_sequence":"E","result":{"columns":["personnummer","namn","telefon","tjänsterum","antal"],"values":[["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,1],["111017-0003","Carl Nordin","151553",603,1],["111017-0004","Lena Svensson","151556",605,2],["111017-0005","Sofia Wilsson","151585",622,0],["131017-0050","Carl Nilsson","332356",558,2]]}},{"id":107,"description":"Ta fram antal kurstillfällen med start under år 2008 för varje student! Visa studentens personnummer, namn, telefon och ort samt antalet! Även studenter som inte har deltagit i kurstillfällen under 2008 ska visas!","display_sequence":"F","result":{"columns":["personnummer","namn","telefon","ort","antal"],"values":[["111017-0001","Anders Ödman","151576","Sollentuna",0],["121017-0001","Bo Dahl","163578","Bromma",4],["121017-0002","Ann Stål","373789","Kista",3],["121017-0003","Ebba Ryd","325305","Bromma",3],["121017-0004","Robert Ahl","123435","Kista",1],["121017-0005","Lars Holm","203045","Solna",2],["121017-0006","Siw Björk","452678","Kista",4],["121017-0007","Sigge Ehn","245578","Kista",2],["121017-0008","Kurt Grahn","192292","Bro",3],["121017-0009","Eva Jung","131187","Solna",2],["121017-0010","Lola Frid","723384","Täby",2],["121018-0040","Britt Maj","982355","Södertälje",0],["121117-0041","Eva Berglund","774355","Södertälje",0],["131017-0050","Carl Nilsson","332356","Solna",1],["131017-0051","Eva Jung","449867","Solna",1],["131017-0052","Lars Holm","665844","Solna",0],["131017-0053","Bo Dahl","770691","Täby",0]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Ta fram information om kurstillfällen som ingen student från Solna har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"A","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z","Sirius","Sofia Wilsson"],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z","Jupiter","Bo Åkerman"],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z","Sirius","Sofia Wilsson"],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z","Jupiter","Lena Svensson"],["Log1","Logik","2009-09-17T00:00:00.000Z","Jupiter","Sofia Wilsson"],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z","Sirius","Lena Svensson"],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z","Saturnus","Carl Nordin"],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z","Saturnus","Lena Svensson"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}},{"id":109,"description":"Ta fram information om kurstillfällen som ingen student från Kista har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"B","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z","Tellus","Bo Åkerman"],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z","Tellus","Carl Nordin"],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z","Jupiter","Sofia Wilsson"],["Log1","Logik","2010-05-01T00:00:00.000Z","Jupiter","Bo Åkerman"],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z","Saturnus","Lena Svensson"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}},{"id":110,"description":"Ta fram information om kurstillfällen som ingen funktionshindrad student har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"C","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Log1","Logik","2008-04-16T00:00:00.000Z","Jupiter","Carl Nordin"],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z","Sirius","Sofia Wilsson"],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z","Tellus","Carl Nordin"],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z","Jupiter","Sofia Wilsson"],["Log1","Logik","2010-05-01T00:00:00.000Z","Jupiter","Bo Åkerman"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}}]}]} \ No newline at end of file +{"language":"sv","engine":"sqlite","defaultQuery":"SELECT * FROM Student;","questions":[{"category_id":1,"display_number":1,"questions":[{"id":1,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Solna!","display_sequence":"A","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0005","Lars Holm","Skolgatan 3","16966"],["121017-0009","Eva Jung","Storgatan 5","16966"],["131017-0050","Carl Nilsson","Strogatan 4","16966"],["131017-0051","Eva Jung","Skolgatan 12","16966"],["131017-0052","Lars Holm","Skolgatan 1","16966"]]}},{"id":2,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":3,"description":"Ta fram personnummer, namn, adress och postnummer på personer som bor i Kista!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["121017-0002","Ann Stål","Lindvägen 3","16429"],["121017-0004","Robert Ahl","Ekvägen 1","16425"],["121017-0006","Siw Björk","Bokvägen 2","16431"],["121017-0007","Sigge Ehn","Bokvägen 24","16429"]]}},{"id":4,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":5,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Solna!","display_sequence":"E","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Lars Holm","Skolgatan 3","16966","203045"],["Eva Jung","Storgatan 5","16966","131187"],["Carl Nilsson","Strogatan 4","16966","332356"],["Eva Jung","Skolgatan 12","16966","449867"],["Lars Holm","Skolgatan 1","16966","665844"]]}},{"id":6,"description":"Ta fram namn, adress, postnummer och telefon på personer som bor i Kista!","display_sequence":"F","result":{"columns":["namn","adress","postnr","telefon"],"values":[["Ann Stål","Lindvägen 3","16429","373789"],["Robert Ahl","Ekvägen 1","16425","123435"],["Siw Björk","Bokvägen 2","16431","452678"],["Sigge Ehn","Bokvägen 24","16429","245578"]]}},{"id":7,"description":"Ta fram kurskod, namn och pris på kurser på 3 veckor!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["Log1","Logik",4500]]}},{"id":8,"description":"Ta fram kurskod, namn och pris på kurser på 4 veckor!","display_sequence":"H","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000]]}}]},{"category_id":2,"display_number":2,"questions":[{"id":9,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Kista!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0009","Eva Jung","Solna"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"]]}},{"id":10,"description":"Ta fram personnummer, namn och telefon på personer som bor i Solna eller i Bromma!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0005","Lars Holm","203045"],["121017-0009","Eva Jung","131187"],["131017-0050","Carl Nilsson","332356"],["131017-0051","Eva Jung","449867"],["131017-0052","Lars Holm","665844"]]}},{"id":11,"description":"Ta fram personnummer, namn och ort på personer som bor i Kista eller i Täby!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0002","Ann Stål","Kista"],["121017-0004","Robert Ahl","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0053","Bo Dahl","Täby"]]}},{"id":12,"description":"Ta fram personnummer, namn och telefon på personer som bor i Kista eller i Bromma!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0002","Ann Stål","373789"],["121017-0003","Ebba Ryd","325305"],["121017-0004","Robert Ahl","123435"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"]]}},{"id":13,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Täby!","display_sequence":"E","result":{"columns":["namn","adress","postnr","ort"],"values":[["Lena Svensson","Nygatan 4","18754","Täby"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"],["Bo Dahl","Nygatan 63","18753","Täby"]]}},{"id":14,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Kista eller i Solna!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Lars Holm","Skolgatan 3","16966","Solna"],["Siw Björk","Bokvägen 2","16431","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Eva Jung","Storgatan 5","16966","Solna"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":15,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Bromma!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0004","Lena Svensson","151556"],["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0010","Lola Frid","723384"],["131017-0053","Bo Dahl","770691"]]}},{"id":16,"description":"Ta fram personnummer, namn och ort på personer som bor i Solna eller i Täby!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0004","Lena Svensson","Täby"],["121017-0005","Lars Holm","Solna"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0052","Lars Holm","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":17,"description":"Ta fram personnummer, namn och telefon på personer som bor i Täby eller i Södertälje!","display_sequence":"I","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["121017-0010","Lola Frid","723384"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0053","Bo Dahl","770691"]]}},{"id":18,"description":"Ta fram namn, adress, postnummer och ort på personer som bor i Solna eller i Södertälje!","display_sequence":"J","result":{"columns":["namn","adress","postnr","ort"],"values":[["Carl Nordin","Dalgatan 3","15106","Södertälje"],["Sofia Wilsson","Björkgatan 5","15136","Södertälje"],["Lars Holm","Skolgatan 3","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Skolgatan 12","16966","Solna"],["Lars Holm","Skolgatan 1","16966","Solna"]]}},{"id":19,"description":"Ta fram kurskod, namn och pris på kurser på 3 eller 4 veckor!","display_sequence":"K","result":{"columns":["kurskod","namn","pris"],"values":[["Java2","Java, fortsättningskurs",6000],["LDBD","Logisk databasdesign",6000],["Log1","Logik",4500]]}}]},{"category_id":3,"display_number":3,"questions":[{"id":20,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["lärare"],"values":[["111017-0003"],["111017-0002"],["111017-0004"],["111017-0005"]]}},{"id":21,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0004"]]}},{"id":22,"description":"Ta fram personnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"],["131017-0050"]]}},{"id":23,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["lärare"],"values":[["111017-0002"],["111017-0003"]]}},{"id":24,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["lärare"],"values":[["111017-0002"],["131017-0050"]]}},{"id":25,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"],["131017-0050"]]}},{"id":26,"description":"Ta fram personnummer på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["lärare"],"values":[["111017-0005"],["111017-0004"]]}},{"id":27,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java1!","display_sequence":"H","result":{"columns":["student"],"values":[["121017-0001"],["121017-0003"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0002"],["121017-0006"],["121017-0007"]]}},{"id":28,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod DBM1!","display_sequence":"I","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0010"],["121017-0005"],["131017-0051"],["121017-0006"],["121017-0004"],["121018-0040"],["121117-0041"],["131017-0052"]]}},{"id":29,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod Java2!","display_sequence":"J","result":{"columns":["student"],"values":[["121017-0002"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0004"]]}},{"id":30,"description":"Ta fram personnummer på studenter som har deltagit i kursen med kod LDBD!","display_sequence":"K","result":{"columns":["student"],"values":[["121017-0001"],["121017-0007"],["121017-0006"],["121017-0008"],["131017-0050"],["121018-0040"],["121117-0041"]]}}]},{"category_id":4,"display_number":4,"questions":[{"id":31,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0003","Carl Nordin","151553"],["111017-0002","Bo Åkerman","151526"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":32,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0004","Lena Svensson","Täby"]]}},{"id":33,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0005","Sofia Wilsson","Björkgatan 5","15136"],["111017-0004","Lena Svensson","Nygatan 4","18754"],["131017-0050","Carl Nilsson","Strogatan 4","16966"]]}},{"id":34,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"]]}},{"id":35,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["131017-0050","Carl Nilsson","Solna"]]}},{"id":36,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Sofia Wilsson","Björkgatan 5","15136","Södertälje"],["Lena Svensson","Nygatan 4","18754","Täby"],["Carl Nilsson","Strogatan 4","16966","Solna"]]}},{"id":37,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0005","Sofia Wilsson","151585"],["111017-0004","Lena Svensson","151556"]]}},{"id":38,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat i rummet med id 1!","display_sequence":"H","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0003","Carl Nordin","Södertälje"],["111017-0002","Bo Åkerman","Åkersberga"],["111017-0004","Lena Svensson","Täby"],["111017-0005","Sofia Wilsson","Södertälje"]]}},{"id":39,"description":"Ta fram personnummer, namn, adress och postnummer på lärare som har undervisat i rummet med id 2!","display_sequence":"I","result":{"columns":["personnummer","namn","adress","postnr"],"values":[["111017-0002","Bo Åkerman","Villagatan 2","18491"],["111017-0004","Lena Svensson","Nygatan 4","18754"]]}},{"id":40,"description":"Ta fram personnummer, namn och telefon på lärare som har undervisat på kursen med kod Java2!","display_sequence":"J","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["131017-0050","Carl Nilsson","332356"]]}},{"id":41,"description":"Ta fram personnummer, namn och ort på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"K","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0005","Sofia Wilsson","Södertälje"],["111017-0004","Lena Svensson","Täby"]]}},{"id":42,"description":"Ta fram namn, adress, postnummer och ort på lärare som har undervisat på kursen med kod Java1!","display_sequence":"L","result":{"columns":["namn","adress","postnr","ort"],"values":[["Bo Åkerman","Villagatan 2","18491","Åkersberga"],["Carl Nordin","Dalgatan 3","15106","Södertälje"]]}}]},{"category_id":5,"display_number":5,"questions":[{"id":43,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 1!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0003","Carl Nordin",603],["111017-0002","Bo Åkerman",604],["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}},{"id":44,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 2!","display_sequence":"B","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0004","Lena Svensson",605]]}},{"id":45,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat i rummet med id 3!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605],["131017-0050","Carl Nilsson",558]]}},{"id":46,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java1!","display_sequence":"D","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["111017-0003","Carl Nordin",603]]}},{"id":47,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod Java2!","display_sequence":"E","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0002","Bo Åkerman",604],["131017-0050","Carl Nilsson",558]]}},{"id":48,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod DBM1!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605],["131017-0050","Carl Nilsson",558]]}},{"id":49,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har undervisat på kursen med kod LDBD!","display_sequence":"G","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0005","Sofia Wilsson",622],["111017-0004","Lena Svensson",605]]}}]},{"category_id":6,"display_number":6,"questions":[{"id":50,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["student"],"values":[["121017-0004"],["121017-0005"],["121017-0002"],["121017-0006"],["121017-0003"],["121017-0008"],["131017-0051"],["121117-0041"],["121018-0040"],["131017-0053"]]}},{"id":51,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["student"],"values":[["121017-0002"],["121017-0005"],["121017-0006"],["121017-0007"],["121017-0008"],["131017-0050"],["121017-0009"],["121017-0010"],["131017-0051"]]}},{"id":52,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["student"],"values":[["121017-0001"],["121017-0003"],["121017-0008"],["121017-0009"],["121017-0010"],["131017-0051"],["121017-0004"],["121017-0005"],["121017-0007"]]}},{"id":53,"description":"Ta fram personnummer på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["student"],"values":[["121017-0001"],["121017-0002"],["121017-0003"],["121017-0010"],["121017-0007"],["121017-0006"],["121017-0004"],["121018-0040"],["121117-0041"],["131017-0052"]]}}]},{"category_id":7,"display_number":7,"questions":[{"id":54,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0002","Ann Stål","373789"],["121017-0006","Siw Björk","452678"],["121017-0003","Ebba Ryd","325305"],["121017-0008","Kurt Grahn","192292"],["131017-0051","Eva Jung","449867"],["121117-0041","Eva Berglund","774355"],["121018-0040","Britt Maj","982355"],["131017-0053","Bo Dahl","770691"]]}},{"id":55,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["131017-0050","Carl Nilsson","Solna"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["131017-0051","Eva Jung","Solna"]]}},{"id":56,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0008","Kurt Grahn","192292"],["121017-0009","Eva Jung","131187"],["121017-0010","Lola Frid","723384"],["131017-0051","Eva Jung","449867"],["121017-0004","Robert Ahl","123435"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"]]}},{"id":57,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"D","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0010","Lola Frid","Täby"],["121017-0007","Sigge Ehn","Kista"],["121017-0006","Siw Björk","Kista"],["121017-0004","Robert Ahl","Kista"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0052","Lars Holm","Solna"]]}},{"id":58,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Jupiter!","display_sequence":"E","result":{"columns":["namn","adress","ort","telefon"],"values":[["Robert Ahl","Ekvägen 1","Kista","123435"],["Lars Holm","Skolgatan 3","Solna","203045"],["Ann Stål","Lindvägen 3","Kista","373789"],["Siw Björk","Bokvägen 2","Kista","452678"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Eva Jung","Skolgatan 12","Solna","449867"],["Eva Berglund","Hamngatan 15b","Södertälje","774355"],["Britt Maj","Karlvägen 44","Södertälje","982355"],["Bo Dahl","Nygatan 63","Täby","770691"]]}},{"id":59,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Orion!","display_sequence":"F","result":{"columns":["namn","adress","postnr","ort"],"values":[["Ann Stål","Lindvägen 3","16429","Kista"],["Lars Holm","Skolgatan 3","16966","Solna"],["Siw Björk","Bokvägen 2","16431","Kista"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Kurt Grahn","Byvägen 112","19735","Bro"],["Carl Nilsson","Strogatan 4","16966","Solna"],["Eva Jung","Storgatan 5","16966","Solna"],["Lola Frid","Lillgatan 3","18754","Täby"],["Eva Jung","Skolgatan 12","16966","Solna"]]}},{"id":60,"description":"Ta fram namn, adress, ort och telefon på studenter som har deltagit i något kurstillfälle i Tellus!","display_sequence":"G","result":{"columns":["namn","adress","ort","telefon"],"values":[["Bo Dahl","Ahlgatan 6","Bromma","163578"],["Ebba Ryd","Ankvägen 4","Bromma","325305"],["Kurt Grahn","Byvägen 112","Bro","192292"],["Eva Jung","Storgatan 5","Solna","131187"],["Lola Frid","Lillgatan 3","Täby","723384"],["Eva Jung","Skolgatan 12","Solna","449867"],["Robert Ahl","Ekvägen 1","Kista","123435"],["Lars Holm","Skolgatan 3","Solna","203045"],["Sigge Ehn","Bokvägen 24","Kista","245578"]]}},{"id":61,"description":"Ta fram namn, adress, postnummer och ort på studenter som har deltagit i något kurstillfälle i Sirius!","display_sequence":"H","result":{"columns":["namn","adress","postnr","ort"],"values":[["Bo Dahl","Ahlgatan 6","16102","Bromma"],["Ann Stål","Lindvägen 3","16429","Kista"],["Ebba Ryd","Ankvägen 4","16107","Bromma"],["Lola Frid","Lillgatan 3","18754","Täby"],["Sigge Ehn","Bokvägen 24","16429","Kista"],["Siw Björk","Bokvägen 2","16431","Kista"],["Robert Ahl","Ekvägen 1","16425","Kista"],["Britt Maj","Karlvägen 44","15106","Södertälje"],["Eva Berglund","Hamngatan 15b","15107","Södertälje"],["Lars Holm","Skolgatan 1","16966","Solna"]]}}]},{"category_id":8,"display_number":8,"questions":[{"id":62,"description":"Ta fram antal kurstillfällen som varje student har deltagit i! Visa studentens personnummer, studentens namn och antalet!","display_sequence":"A","result":{"columns":["personnummer","namn","antal"],"values":[["111017-0001","Anders Ödman",0],["121017-0001","Bo Dahl",4],["121017-0002","Ann Stål",4],["121017-0003","Ebba Ryd",5],["121017-0004","Robert Ahl",4],["121017-0005","Lars Holm",5],["121017-0006","Siw Björk",6],["121017-0007","Sigge Ehn",4],["121017-0008","Kurt Grahn",6],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121018-0040","Britt Maj",3],["121117-0041","Eva Berglund",3],["131017-0050","Carl Nilsson",1],["131017-0051","Eva Jung",3],["131017-0052","Lars Holm",1],["131017-0053","Bo Dahl",1]]},"alternative_results":[{"columns":["personnummer","namn","antal"],"values":[["121017-0001","Bo Dahl",4],["121017-0002","Ann Stål",4],["121017-0003","Ebba Ryd",5],["121017-0004","Robert Ahl",4],["121017-0005","Lars Holm",5],["121017-0006","Siw Björk",6],["121017-0007","Sigge Ehn",4],["121017-0008","Kurt Grahn",6],["121017-0009","Eva Jung",3],["121017-0010","Lola Frid",5],["121018-0040","Britt Maj",3],["121117-0041","Eva Berglund",3],["131017-0050","Carl Nilsson",1],["131017-0051","Eva Jung",3],["131017-0052","Lars Holm",1],["131017-0053","Bo Dahl",1]]}]},{"id":63,"description":"Ta fram antal kurstillfällen som varje rum har använts till! Visa rummets namn, rummets kapacitet och antalet!","display_sequence":"B","result":{"columns":["namn","antalplatser","antal"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturnus",24,3]]},"alternative_results":[{"columns":["namn","antalplatser","antal"],"values":[["Jupiter",12,6],["Orion",24,3],["Sirius",16,4],["Tellus",32,3],["Saturnus",24,3]]}]},{"id":64,"description":"Ta fram antal kurstillfällen som varje kurs har haft! Visa kursens kod, kursens namn och antalet!","display_sequence":"C","result":{"columns":["kurskod","namn","antal"],"values":[["DBM1","Databasmetodik",4],["FDBD","Fysisk databasdesign",1],["Java1","Java, grundkurs",5],["Java2","Java, fortsättningskurs",3],["LDBD","Logisk databasdesign",3],["Log1","Logik",3]]},"alternative_results":[{"columns":["kurskod","namn","antal"],"values":[["DBM1","Databasmetodik",4],["FDBD","Fysisk databasdesign",1],["Java1","Java, grundkurs",5],["Java2","Java, fortsättningskurs",3],["LDBD","Logisk databasdesign",3],["Log1","Logik",3]]}]},{"id":65,"description":"Ta fram antal kurstillfällen som varje lärare har hållit! Visa lärarens personnummer, lärarens namn och antalet!","display_sequence":"D","result":{"columns":["personnummer","namn","antal"],"values":[["111017-0001","Anders Ödman",0],["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["111017-0005","Sofia Wilsson",4],["131017-0050","Carl Nilsson",2]]},"alternative_results":[{"columns":["personnummer","namn","antal"],"values":[["111017-0002","Bo Åkerman",6],["111017-0003","Carl Nordin",3],["111017-0004","Lena Svensson",4],["111017-0005","Sofia Wilsson",4],["131017-0050","Carl Nilsson",2]]}]},{"id":66,"description":"Ta fram antal deltagande studenter för varje kurstillfälle! Visa kurstillfällets kurskod och kursnamn, kurstillfällets startdatum och antalet deltagande studenter!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum","antal"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z",4],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-09-22T00:00:00.000Z",4],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z",3],["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z",6],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z",2],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z",2],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z",3],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z",0],["Java2","Java, fortsättningskurs","2008-04-16T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2009-01-22T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2010-04-11T00:00:00.000Z",4],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z",2],["LDBD","Logisk databasdesign","2008-09-02T00:00:00.000Z",3],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z",3],["Log1","Logik","2008-04-16T00:00:00.000Z",2],["Log1","Logik","2009-09-17T00:00:00.000Z",2],["Log1","Logik","2010-05-01T00:00:00.000Z",4]]},"alternative_results":[{"columns":["kurs","namn","startdatum","antal"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z",4],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z",3],["DBM1","Databasmetodik","2010-09-22T00:00:00.000Z",4],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z",3],["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z",6],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z",2],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z",2],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z",3],["Java2","Java, fortsättningskurs","2008-04-16T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2009-01-22T00:00:00.000Z",4],["Java2","Java, fortsättningskurs","2010-04-11T00:00:00.000Z",4],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z",2],["LDBD","Logisk databasdesign","2008-09-02T00:00:00.000Z",3],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z",3],["Log1","Logik","2008-04-16T00:00:00.000Z",2],["Log1","Logik","2009-09-17T00:00:00.000Z",2],["Log1","Logik","2010-05-01T00:00:00.000Z",4]]}]}]},{"category_id":9,"display_number":9,"questions":[{"id":67,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen varje lärare har hållit! II. Ta fram personnummer, namn och telefon på läraren som har hållit flest kurstillfällen!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"]]}},{"id":68,"description":"I. Ta fram personnummer, namn och telefon samt antal kurstillfällen per student! II. Ta fram personnummer, namn och telefon på den student som har deltagit i flest kurstillfällen!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"]]}},{"id":69,"description":"I. Ta fram kurskod, namn och längd samt antal kurstillfällen per kurs! II. Ta fram kurskod, namn och längd på den kurs som har haft flest kurstillfällen!","display_sequence":"C","result":{"columns":["kurskod","namn","längd"],"values":[["Java1","Java, grundkurs",5]]}},{"id":70,"description":"I. Ta fram namn och kapacitet samt antal kurstillfällen per rum! II. Ta fram namn och kapacitet på det rum som har använts för flest kurstillfällen!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":71,"description":"I. Ta fram kurskod, kursnamn och startdatum samt antal deltagare per kurstillfälle! II. Ta fram kurskod, kursnamn och startdatum på kurstillfället som har haft flest deltagare!","display_sequence":"E","result":{"columns":["kurs","namn","startdatum"],"values":[["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z"]]}}]},{"category_id":10,"display_number":10,"questions":[{"id":72,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i minst tre OLIKA kurser!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0001","Bo Dahl","Bromma"],["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0051","Eva Jung","Solna"]]}},{"id":73,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit minst tre OLIKA kurser!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"]]}},{"id":74,"description":"Ta fram personnummer, namn och ort på lärare som har hållit kurstillfällen i minst tre OLIKA rum!","display_sequence":"C","result":{"columns":["personnummer","namn","ort"],"values":[["111017-0002","Bo Åkerman","Åkersberga"],["111017-0003","Carl Nordin","Södertälje"],["111017-0004","Lena Svensson","Täby"]]}},{"id":75,"description":"Ta fram namn och kapacitet på rum som har använts av minst tre OLIKA lärare!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Sirius",16],["Tellus",32],["Saturnus",24]]}},{"id":76,"description":"Ta fram namn och kapacitet på rum som har använts för kurstillfällen av minst tre OLIKA kurser!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":77,"description":"Ta fram kod, namn och pris på kurser som har hållits i minst tre OLIKA rum!","display_sequence":"F","result":{"columns":["kurskod","namn","pris"],"values":[["Java1","Java, grundkurs",6700],["LDBD","Logisk databasdesign",6000]]}},{"id":78,"description":"Ta fram kod, namn och pris på kurser som har hållits av minst tre OLIKA lärare!","display_sequence":"G","result":{"columns":["kurskod","namn","pris"],"values":[["DBM1","Databasmetodik",2800],["Log1","Logik",4500]]}}]},{"category_id":11,"display_number":11,"questions":[{"id":79,"description":"Ta fram namn och ort på personer som inte är lärare!","display_sequence":"A","result":{"columns":["namn","ort"],"values":[["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Kurt Grahn","Bro"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Eva Jung","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":80,"description":"Ta fram namn och ort på studenter som inte deltog i något kurstillfälle med startdatum under 2010!","display_sequence":"B","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Eva Jung","Solna"],["Carl Nilsson","Solna"],["Eva Jung","Solna"]]}},{"id":81,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Java1!","display_sequence":"C","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Robert Ahl","Kista"],["Lars Holm","Solna"],["Britt Maj","Södertälje"],["Eva Berglund","Södertälje"],["Carl Nilsson","Solna"],["Lars Holm","Solna"],["Bo Dahl","Täby"]]}},{"id":82,"description":"Ta fram namn och ort på studenter som inte har gått kursen med koden Log1!","display_sequence":"D","result":{"columns":["namn","ort"],"values":[["Anders Ödman","Sollentuna"],["Bo Dahl","Bromma"],["Ann Stål","Kista"],["Ebba Ryd","Bromma"],["Siw Björk","Kista"],["Sigge Ehn","Kista"],["Eva Jung","Solna"],["Lola Frid","Täby"],["Carl Nilsson","Solna"],["Eva Jung","Solna"],["Lars Holm","Solna"]]}}]},{"category_id":12,"display_number":12,"questions":[{"id":83,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som aldrig har hållit ett kurstillfälle för kursen \"Java, fortsättningskurs\"!","display_sequence":"A","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0001","Anders Ödman",634,"151576"],["111017-0003","Carl Nordin",603,"151553"],["111017-0004","Lena Svensson",605,"151556"],["111017-0005","Sofia Wilsson",622,"151585"]]}},{"id":84,"description":"Ta fram personnummer, namn och ort på funktionshindrade studenter som inte har gått kursen \"Logisk databasdesign\"!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0010","Lola Frid","Täby"],["131017-0052","Lars Holm","Solna"]]}},{"id":85,"description":"Ta fram namn och kapacitet på rum som aldrig har använts för ett kurstillfälle av kursen \"Logisk databasdesign\"!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Tellus",32]]}},{"id":86,"description":"Ta fram namn och kapacitet på rum som aldrig har använts av läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Sirius",16]]}}]},{"category_id":13,"display_number":13,"questions":[{"id":87,"description":"Ta fram personnummer, namn och telefon på studenter som har läst kursen \"Fysisk databasdesign\" eller kursen \"Logisk databasdesign\" (eller båda)!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0003","Ebba Ryd","325305"],["121017-0006","Siw Björk","452678"],["121017-0007","Sigge Ehn","245578"],["121017-0008","Kurt Grahn","192292"],["121018-0040","Britt Maj","982355"],["121117-0041","Eva Berglund","774355"],["131017-0050","Carl Nilsson","332356"]]}},{"id":88,"description":"Ta fram personnummer, namn och ort på studenter som har deltagit i kurstillfällen i rum Orion eller i rum Jupiter (eller i båda)!","display_sequence":"B","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0002","Ann Stål","Kista"],["121017-0003","Ebba Ryd","Bromma"],["121017-0004","Robert Ahl","Kista"],["121017-0005","Lars Holm","Solna"],["121017-0006","Siw Björk","Kista"],["121017-0007","Sigge Ehn","Kista"],["121017-0008","Kurt Grahn","Bro"],["121017-0009","Eva Jung","Solna"],["121017-0010","Lola Frid","Täby"],["121018-0040","Britt Maj","Södertälje"],["121117-0041","Eva Berglund","Södertälje"],["131017-0050","Carl Nilsson","Solna"],["131017-0051","Eva Jung","Solna"],["131017-0053","Bo Dahl","Täby"]]}},{"id":89,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kursen \"Logik\" eller kursen \"Databasmetodik\" (eller båda)!","display_sequence":"C","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0002","Bo Åkerman","151526"],["111017-0003","Carl Nordin","151553"],["111017-0004","Lena Svensson","151556"],["111017-0005","Sofia Wilsson","151585"],["131017-0050","Carl Nilsson","332356"]]}},{"id":90,"description":"Ta fram namn och kapacitet på rum som har använts av läraren Bo Åkerman eller av läraren Carl Nordin (eller av båda)!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Saturnus",24],["Tellus",32]]}},{"id":91,"description":"Ta fram namn och kapacitet på rum som har använts för kursen \"Logik\" eller för kursen \"Java, fortsättningskurs\" (eller för båda)!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Tellus",32]]}}]},{"category_id":14,"display_number":14,"questions":[{"id":92,"description":"Ta fram personnummer, namn och ort på studenter som har läst både kursen \"Fysisk databasdesign\" och kursen \"Logisk databasdesign\"!","display_sequence":"A","result":{"columns":["personnummer","namn","ort"],"values":[["121017-0006","Siw Björk","Kista"],["121017-0008","Kurt Grahn","Bro"]]}},{"id":93,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen både i rum Orion och i rum Jupiter!","display_sequence":"B","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0002","Ann Stål","373789"],["121017-0005","Lars Holm","203045"],["121017-0006","Siw Björk","452678"],["121017-0008","Kurt Grahn","192292"],["131017-0051","Eva Jung","449867"]]}},{"id":94,"description":"Ta fram personnummer, namn och tjänsterum på lärare som har hållit både kursen \"Logisk databasdesign\" och kursen \"Databasmetodik\"!","display_sequence":"C","result":{"columns":["personnummer","namn","tjänsterum"],"values":[["111017-0004","Lena Svensson",605],["111017-0005","Sofia Wilsson",622]]}},{"id":95,"description":"Ta fram namn och kapacitet på rum som har använts av både läraren Lena Svensson och läraren Bo Åkerman!","display_sequence":"D","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12],["Orion",24],["Saturnus",24]]}},{"id":96,"description":"Ta fram namn och kapacitet på rum som har använts för både kursen \"Logik\" och kursen \"Java, grundkurs\"!","display_sequence":"E","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":97,"description":"Ta fram personnummer, namn, tjänsterum och telefon på lärare som har hållit kurstillfällen både i rum Jupiter och i rum Sirius!","display_sequence":"F","result":{"columns":["personnummer","namn","tjänsterum","telefon"],"values":[["111017-0004","Lena Svensson",605,"151556"],["111017-0005","Sofia Wilsson",622,"151585"]]}}]},{"category_id":15,"display_number":15,"questions":[{"id":98,"description":"Ta fram personnummer, namn och telefon på studenter som har deltagit i kurstillfällen ledda av samtliga lärare från Södertälje!","display_sequence":"A","result":{"columns":["personnummer","namn","telefon"],"values":[["121017-0001","Bo Dahl","163578"],["121017-0005","Lars Holm","203045"],["121017-0007","Sigge Ehn","245578"],["121017-0004","Robert Ahl","123435"]]}},{"id":99,"description":"Ta fram namn och kapacitet på rum som har använts av samtliga lärare från Södertälje!","display_sequence":"B","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":100,"description":"Ta fram namn och kapacitet på rum som har använts för samtliga kurser som kostar under 5000!","display_sequence":"C","result":{"columns":["namn","antalplatser"],"values":[["Jupiter",12]]}},{"id":101,"description":"Ta fram personnummer, namn och telefon på lärare som har hållit kurstillfällen av samtliga kurser på högst tre veckor!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon"],"values":[["111017-0005","Sofia Wilsson","151585"]]}}]},{"category_id":16,"display_number":16,"questions":[{"id":102,"description":"Ta fram antal studenter från Kista för varje kurstillfälle! Visa kurskod, startdatum och antal studenter! Även kurstillfällen utan studenter från Kista ska visas!","display_sequence":"A","result":{"columns":["kurs","startdatum","antal"],"values":[["DBM1","2008-04-02T00:00:00.000Z",1],["DBM1","2009-01-28T00:00:00.000Z",0],["DBM1","2010-02-15T00:00:00.000Z",1],["DBM1","2010-09-22T00:00:00.000Z",1],["FDBD","2008-05-09T00:00:00.000Z",1],["Java1","2008-03-06T00:00:00.000Z",0],["Java1","2008-05-06T00:00:00.000Z",2],["Java1","2008-09-02T00:00:00.000Z",0],["Java1","2010-05-01T00:00:00.000Z",2],["Java1","2011-05-01T00:00:00.000Z",0],["Java2","2008-04-16T00:00:00.000Z",3],["Java2","2009-01-22T00:00:00.000Z",1],["Java2","2010-04-11T00:00:00.000Z",2],["LDBD","2008-05-06T00:00:00.000Z",1],["LDBD","2008-09-02T00:00:00.000Z",1],["LDBD","2010-09-24T00:00:00.000Z",0],["Log1","2008-04-16T00:00:00.000Z",1],["Log1","2009-09-17T00:00:00.000Z",1],["Log1","2010-05-01T00:00:00.000Z",0]]}},{"id":103,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Logisk databasdesign\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"B","result":{"columns":["personnummer","namn","ort","antal"],"values":[["111017-0001","Anders Ödman","Sollentuna",0],["121017-0001","Bo Dahl","Bromma",1],["121017-0002","Ann Stål","Kista",0],["121017-0003","Ebba Ryd","Bromma",0],["121017-0004","Robert Ahl","Kista",0],["121017-0005","Lars Holm","Solna",0],["121017-0006","Siw Björk","Kista",1],["121017-0007","Sigge Ehn","Kista",1],["121017-0008","Kurt Grahn","Bro",2],["121017-0009","Eva Jung","Solna",0],["121017-0010","Lola Frid","Täby",0],["121018-0040","Britt Maj","Södertälje",1],["121117-0041","Eva Berglund","Södertälje",1],["131017-0050","Carl Nilsson","Solna",1],["131017-0051","Eva Jung","Solna",0],["131017-0052","Lars Holm","Solna",0],["131017-0053","Bo Dahl","Täby",0]]}},{"id":104,"description":"Ta fram antal gånger som varje student har deltagit i kursen \"Java, grundkurs\"! Visa studentens personnummer, namn och ort samt antalet! Även studenter som inte har deltagit i kursen ska visas!","display_sequence":"C","result":{"columns":["personnummer","namn","ort","antal"],"values":[["111017-0001","Anders Ödman","Sollentuna",0],["121017-0001","Bo Dahl","Bromma",2],["121017-0002","Ann Stål","Kista",1],["121017-0003","Ebba Ryd","Bromma",1],["121017-0004","Robert Ahl","Kista",0],["121017-0005","Lars Holm","Solna",0],["121017-0006","Siw Björk","Kista",2],["121017-0007","Sigge Ehn","Kista",1],["121017-0008","Kurt Grahn","Bro",2],["121017-0009","Eva Jung","Solna",2],["121017-0010","Lola Frid","Täby",1],["121018-0040","Britt Maj","Södertälje",0],["121117-0041","Eva Berglund","Södertälje",0],["131017-0050","Carl Nilsson","Solna",0],["131017-0051","Eva Jung","Solna",1],["131017-0052","Lars Holm","Solna",0],["131017-0053","Bo Dahl","Täby",0]]}},{"id":105,"description":"Ta fram antal gånger som varje lärare har hållit kursen \"Java, grundkurs\"! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kursen ska visas!","display_sequence":"D","result":{"columns":["personnummer","namn","telefon","tjänsterum","antal"],"values":[["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,3],["111017-0003","Carl Nordin","151553",603,2],["111017-0004","Lena Svensson","151556",605,0],["111017-0005","Sofia Wilsson","151585",622,0],["131017-0050","Carl Nilsson","332356",558,0]]}},{"id":106,"description":"Ta fram antal kurstillfällen som varje lärare har hållit med start under år 2010! Visa lärarens personnummer, namn, telefon och tjänsterum samt antalet! Även lärare som inte har hållit kurstillfällen under 2010 ska visas!","display_sequence":"E","result":{"columns":["personnummer","namn","telefon","tjänsterum","antal"],"values":[["111017-0001","Anders Ödman","151576",634,0],["111017-0002","Bo Åkerman","151526",604,1],["111017-0003","Carl Nordin","151553",603,1],["111017-0004","Lena Svensson","151556",605,2],["111017-0005","Sofia Wilsson","151585",622,0],["131017-0050","Carl Nilsson","332356",558,2]]}},{"id":107,"description":"Ta fram antal kurstillfällen med start under år 2008 för varje student! Visa studentens personnummer, namn, telefon och ort samt antalet! Även studenter som inte har deltagit i kurstillfällen under 2008 ska visas!","display_sequence":"F","result":{"columns":["personnummer","namn","telefon","ort","antal"],"values":[["111017-0001","Anders Ödman","151576","Sollentuna",0],["121017-0001","Bo Dahl","163578","Bromma",4],["121017-0002","Ann Stål","373789","Kista",3],["121017-0003","Ebba Ryd","325305","Bromma",3],["121017-0004","Robert Ahl","123435","Kista",1],["121017-0005","Lars Holm","203045","Solna",2],["121017-0006","Siw Björk","452678","Kista",4],["121017-0007","Sigge Ehn","245578","Kista",2],["121017-0008","Kurt Grahn","192292","Bro",3],["121017-0009","Eva Jung","131187","Solna",2],["121017-0010","Lola Frid","723384","Täby",2],["121018-0040","Britt Maj","982355","Södertälje",0],["121117-0041","Eva Berglund","774355","Södertälje",0],["131017-0050","Carl Nilsson","332356","Solna",1],["131017-0051","Eva Jung","449867","Solna",1],["131017-0052","Lars Holm","665844","Solna",0],["131017-0053","Bo Dahl","770691","Täby",0]]}}]},{"category_id":17,"display_number":17,"questions":[{"id":108,"description":"Ta fram information om kurstillfällen som ingen student från Solna har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"A","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["DBM1","Databasmetodik","2008-04-02T00:00:00.000Z","Sirius","Sofia Wilsson"],["Java1","Java, grundkurs","2008-05-06T00:00:00.000Z","Jupiter","Bo Åkerman"],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z","Sirius","Sofia Wilsson"],["FDBD","Fysisk databasdesign","2008-05-09T00:00:00.000Z","Jupiter","Lena Svensson"],["Log1","Logik","2009-09-17T00:00:00.000Z","Jupiter","Sofia Wilsson"],["DBM1","Databasmetodik","2010-02-15T00:00:00.000Z","Sirius","Lena Svensson"],["Java1","Java, grundkurs","2010-05-01T00:00:00.000Z","Saturnus","Carl Nordin"],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z","Saturnus","Lena Svensson"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}},{"id":109,"description":"Ta fram information om kurstillfällen som ingen student från Kista har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"B","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Java1","Java, grundkurs","2008-03-06T00:00:00.000Z","Tellus","Bo Åkerman"],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z","Tellus","Carl Nordin"],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z","Jupiter","Sofia Wilsson"],["Log1","Logik","2010-05-01T00:00:00.000Z","Jupiter","Bo Åkerman"],["LDBD","Logisk databasdesign","2010-09-24T00:00:00.000Z","Saturnus","Lena Svensson"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}},{"id":110,"description":"Ta fram information om kurstillfällen som ingen funktionshindrad student har deltagit i! Visa kurskod, kursnamn, startdatum, rumsnamn och lärarnamn!","display_sequence":"C","result":{"columns":["kurs","namn","startdatum","namn","namn"],"values":[["Log1","Logik","2008-04-16T00:00:00.000Z","Jupiter","Carl Nordin"],["LDBD","Logisk databasdesign","2008-05-06T00:00:00.000Z","Sirius","Sofia Wilsson"],["Java1","Java, grundkurs","2008-09-02T00:00:00.000Z","Tellus","Carl Nordin"],["DBM1","Databasmetodik","2009-01-28T00:00:00.000Z","Jupiter","Sofia Wilsson"],["Log1","Logik","2010-05-01T00:00:00.000Z","Jupiter","Bo Åkerman"],["Java1","Java, grundkurs","2011-05-01T00:00:00.000Z","Saturnus","Bo Åkerman"]]}}]}]} \ No newline at end of file diff --git a/scripts/generate-erd.ts b/scripts/generate-erd.ts index 8d53ef0..c4028bc 100644 --- a/scripts/generate-erd.ts +++ b/scripts/generate-erd.ts @@ -1,5 +1,6 @@ /** - * ERD generator: produces light/dark SVG ERD diagrams from a .sqlite3 file. + * ERD generator: produces light/dark SVG ERD diagrams from database files. + * Supports both SQLite (.sqlite3 binary) and PostgreSQL (.sql text via PGLite). * Uses the Edwinexd/sqlite-erd package for schema extraction, DOT generation, * and themed SVG coloring, with jsdom providing browser APIs in Node.js. * @@ -11,6 +12,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from import { join } from "path"; import { JSDOM } from "jsdom"; import type { SqlJsStatic } from "sql.js"; +import type { PGlite } from "@electric-sql/pglite"; +import type { EngineType } from "../src/i18n/languages"; // ── CLI args ─────────────────────────────────────────────────── const args = process.argv.slice(2); @@ -29,24 +32,42 @@ if ((!dbPath || !outDir) && !generateAll) { process.exit(1); } -/** Discover all language directories that contain data.sqlite3 */ -function discoverLanguageDirs(): { db: string; out: string }[] { +interface Target { + db: string; + out: string; + engine: EngineType; +} + +/** Discover all language directories that contain data.sqlite3 or data.sql */ +function discoverLanguageDirs(): Target[] { const langRoot = join(__dirname, "..", "public", "languages"); if (!existsSync(langRoot)) return []; return readdirSync(langRoot, { withFileTypes: true }) .filter(d => d.isDirectory()) - .map(d => ({ - db: join(langRoot, d.name, "data.sqlite3"), - out: join(langRoot, d.name), - })) - .filter(({ db }) => existsSync(db)); + .flatMap((d): Target[] => { + const sqlitePath = join(langRoot, d.name, "data.sqlite3"); + const sqlPath = join(langRoot, d.name, "data.sql"); + if (existsSync(sqlitePath)) { + return [{ db: sqlitePath, out: join(langRoot, d.name), engine: "sqlite" }]; + } + if (existsSync(sqlPath)) { + return [{ db: sqlPath, out: join(langRoot, d.name), engine: "postgresql" }]; + } + return []; + }); } -// ── Generate ERD for a single database ──────────────────────── -async function generateErd( +type ErdDeps = { + executorToLayout: typeof import("sqlite-erd/src/utils").executorToLayout; + dotToSvg: typeof import("sqlite-erd/src/utils").dotToSvg; + colorErdSVG: typeof import("sqlite-erd/src/utils").colorErdSVG; +}; + +// ── Generate ERD for a SQLite database ───────────────────────── +async function generateErdSqlite( dbFilePath: string, outputDir: string, - deps: { SQL: SqlJsStatic; executorToLayout: typeof import("sqlite-erd/src/utils").executorToLayout; dotToSvg: typeof import("sqlite-erd/src/utils").dotToSvg; colorErdSVG: typeof import("sqlite-erd/src/utils").colorErdSVG }, + deps: ErdDeps & { SQL: SqlJsStatic }, ) { const { SQL, executorToLayout, dotToSvg, colorErdSVG } = deps; const dbData = readFileSync(dbFilePath); @@ -58,7 +79,7 @@ async function generateErd( return res[0]; }; - console.log(`Building layout from ${dbFilePath}...`); + console.log(`Building layout from ${dbFilePath} (sqlite)...`); const layout = executorToLayout(executor); const dot = layout.getDot(); @@ -79,6 +100,269 @@ async function generateErd( db.close(); } +// ── PostgreSQL PRAGMA translation layer ──────────────────────── +/** + * Pre-populates the PG executor cache with all data needed by executorToLayout. + * This is necessary because executorToLayout expects a synchronous executor, + * but PGlite is async. + */ +async function buildPgExecutorCache(pgDb: PGlite): Promise<(query: string) => { columns: string[]; values: unknown[][] }> { + const cache: Record = {}; + + function cacheResult(key: string, columns: string[], values: unknown[][]) { + cache[key] = { columns, values }; + } + + // 1. Get all table names (equivalent to sqlite_master query) + const tablesQuery = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"; + const tablesRes = await pgDb.query<{ table_name: string }>( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name" + ); + const tableNames = tablesRes.rows.map(r => r.table_name); + cacheResult(tablesQuery, ["name"], tableNames.map(n => [n])); + + for (const tableName of tableNames) { + const escapedName = `"${tableName.replace(/"/g, '""')}"`; + + // 2. PRAGMA table_info equivalent + const tableInfoKey = `PRAGMA table_info(${escapedName})`; + const colsRes = await pgDb.query<{ + column_name: string; + ordinal_position: number; + data_type: string; + is_nullable: string; + column_default: string | null; + }>( + `SELECT column_name, ordinal_position, UPPER(data_type) as data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position`, + [tableName] + ); + + // Get primary key columns for this table + const pkRes = await pgDb.query<{ column_name: string; ordinal_position: number }>( + `SELECT kcu.column_name, kcu.ordinal_position + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema = 'public' AND tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY' + ORDER BY kcu.ordinal_position`, + [tableName] + ); + const pkColumns = new Set(pkRes.rows.map(r => r.column_name)); + + // table_info columns: cid, name, type, notnull, dflt_value, pk + cacheResult(tableInfoKey, + ["cid", "name", "type", "notnull", "dflt_value", "pk"], + colsRes.rows.map((r, i) => [ + i, // cid + r.column_name, // name + r.data_type, // type + r.is_nullable === "NO" ? 1 : 0, // notnull + r.column_default, // dflt_value + pkColumns.has(r.column_name) ? 1 : 0 // pk + ]) + ); + + // 3. PRAGMA foreign_key_list equivalent + const fkListKey = `PRAGMA foreign_key_list(${escapedName})`; + const fkRes = await pgDb.query<{ + constraint_name: string; + ref_table: string; + column_name: string; + ref_column: string; + update_rule: string; + delete_rule: string; + }>( + `SELECT + tc.constraint_name, + ccu.table_name AS ref_table, + kcu.column_name, + ccu.column_name AS ref_column, + rc.update_rule, + rc.delete_rule + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema + JOIN information_schema.referential_constraints rc + ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema + WHERE tc.table_schema = 'public' AND tc.table_name = $1 AND tc.constraint_type = 'FOREIGN KEY' + ORDER BY tc.constraint_name, kcu.ordinal_position`, + [tableName] + ); + + // Group by constraint name to assign sequential IDs + const fkGroups: Record = {}; + for (const row of fkRes.rows) { + if (!fkGroups[row.constraint_name]) fkGroups[row.constraint_name] = []; + fkGroups[row.constraint_name].push(row); + } + + const fkValues: unknown[][] = []; + let fkId = 0; + for (const [, rows] of Object.entries(fkGroups)) { + for (let seq = 0; seq < rows.length; seq++) { + const r = rows[seq]; + fkValues.push([ + fkId, // id + seq, // seq + r.ref_table, // table + r.column_name, // from + r.ref_column, // to + mapPgAction(r.update_rule), // on_update + mapPgAction(r.delete_rule), // on_delete + "NONE", // match + ]); + } + fkId++; + } + cacheResult(fkListKey, + ["id", "seq", "table", "from", "to", "on_update", "on_delete", "match"], + fkValues + ); + + // 4. PRAGMA index_list equivalent + const indexListKey = `PRAGMA index_list(${escapedName})`; + const idxRes = await pgDb.query<{ + indexname: string; + indexdef: string; + }>( + "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = $1", + [tableName] + ); + + // Also check which indexes are unique and which are pk + const idxDetailRes = await pgDb.query<{ + index_name: string; + is_unique: boolean; + is_primary: boolean; + }>( + `SELECT + i.relname AS index_name, + ix.indisunique AS is_unique, + ix.indisprimary AS is_primary + FROM pg_index ix + JOIN pg_class t ON t.oid = ix.indrelid + JOIN pg_class i ON i.oid = ix.indexrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' AND t.relname = $1`, + [tableName] + ); + + const idxDetails: Record = {}; + for (const r of idxDetailRes.rows) { + idxDetails[r.index_name] = { isUnique: r.is_unique, isPrimary: r.is_primary }; + } + + const indexListValues: unknown[][] = []; + for (let seq = 0; seq < idxRes.rows.length; seq++) { + const r = idxRes.rows[seq]; + const detail = idxDetails[r.indexname] || { isUnique: false, isPrimary: false }; + indexListValues.push([ + seq, // seq + r.indexname, // name + detail.isUnique ? 1 : 0, // unique + detail.isPrimary ? "pk" : "c", // origin + 0, // partial + ]); + } + cacheResult(indexListKey, + ["seq", "name", "unique", "origin", "partial"], + indexListValues + ); + + // 5. PRAGMA index_info for each index + for (const idxRow of idxRes.rows) { + const indexName = idxRow.indexname; + const escapedIdx = `"${indexName.replace(/"/g, '""')}"`; + const indexInfoKey = `PRAGMA index_info(${escapedIdx})`; + + const indexColsRes = await pgDb.query<{ + attnum: number; + attname: string; + }>( + `SELECT a.attnum, a.attname + FROM pg_index ix + JOIN pg_class i ON i.oid = ix.indexrelid + JOIN pg_attribute a ON a.attrelid = ix.indrelid AND a.attnum = ANY(ix.indkey) + JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE n.nspname = 'public' AND i.relname = $1 + ORDER BY a.attnum`, + [indexName] + ); + + cacheResult(indexInfoKey, + ["seqno", "cid", "name"], + indexColsRes.rows.map((r, seq) => [ + seq, // seqno + r.attnum - 1, // cid (0-based) + r.attname, // name + ]) + ); + } + } + + return (query: string) => { + const cached = cache[query]; + if (cached) return cached; + // Return empty result for unknown queries + console.warn(`PG executor: uncached query, returning empty: ${query}`); + return { columns: [] as string[], values: [] as unknown[][] }; + }; +} + +/** Map PostgreSQL referential action names to SQLite-style action names */ +function mapPgAction(action: string): string { + switch (action) { + case "CASCADE": return "CASCADE"; + case "SET NULL": return "SET NULL"; + case "SET DEFAULT": return "SET DEFAULT"; + case "RESTRICT": return "RESTRICT"; + case "NO ACTION": return "NO ACTION"; + default: return action; + } +} + +// ── Generate ERD for a PostgreSQL database ───────────────────── +async function generateErdPostgresql( + sqlFilePath: string, + outputDir: string, + deps: ErdDeps, +) { + const { executorToLayout, dotToSvg, colorErdSVG } = deps; + + const { PGlite } = await import("@electric-sql/pglite"); + const pgDb = new PGlite(); + + const sqlContent = readFileSync(sqlFilePath, "utf-8"); + console.log(`Loading SQL into PGLite from ${sqlFilePath}...`); + await pgDb.exec(sqlContent); + + console.log(`Building layout from ${sqlFilePath} (postgresql)...`); + const executor = await buildPgExecutorCache(pgDb); + const layout = executorToLayout(executor); + const dot = layout.getDot(); + + const rawSvg = await dotToSvg(dot); + + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); + } + + const lightSvg = colorErdSVG(rawSvg, false); + writeFileSync(join(outputDir, "db_layout_light.svg"), lightSvg); + console.log("Wrote db_layout_light.svg"); + + const darkSvg = colorErdSVG(rawSvg, true); + writeFileSync(join(outputDir, "db_layout_dark.svg"), darkSvg); + console.log("Wrote db_layout_dark.svg"); + + await pgDb.close(); +} + // ── Main ─────────────────────────────────────────────────────── async function main() { // Polyfill browser globals via jsdom (needed by sqlite-erd's colorErdSVG) @@ -105,16 +389,25 @@ async function main() { g.SVGSVGElement = makeSvgCheck("svg"); const { executorToLayout, dotToSvg, colorErdSVG } = await import("sqlite-erd/src/utils"); - const initSqlJs = (await import("sql.js")).default; - const SQL = await initSqlJs(); - const deps = { SQL, executorToLayout, dotToSvg, colorErdSVG }; + const erdDeps = { executorToLayout, dotToSvg, colorErdSVG }; - const targets = generateAll + const targets: Target[] = generateAll ? discoverLanguageDirs() - : [{ db: dbPath!, out: outDir! }]; + : [{ db: dbPath!, out: outDir!, engine: (dbPath!.endsWith(".sql") ? "postgresql" : "sqlite") as EngineType }]; + + // Only init sql.js if we have sqlite targets + let SQL: SqlJsStatic | null = null; + if (targets.some(t => t.engine === "sqlite")) { + const initSqlJs = (await import("sql.js")).default; + SQL = await initSqlJs(); + } - for (const { db, out } of targets) { - await generateErd(db, out, deps); + for (const target of targets) { + if (target.engine === "postgresql") { + await generateErdPostgresql(target.db, target.out, erdDeps); + } else { + await generateErdSqlite(target.db, target.out, { ...erdDeps, SQL: SQL! }); + } } console.log("Done!"); diff --git a/scripts/generate-language.ts b/scripts/generate-language.ts index 9c216d1..a433c53 100644 --- a/scripts/generate-language.ts +++ b/scripts/generate-language.ts @@ -1,5 +1,8 @@ /** - * Language generator: produces per-language questionpool.json and data.sqlite3. + * Language generator: produces per-language questionpool.json and database data files. + * + * For SQLite-engine languages: outputs data.sqlite3 (binary). + * For PostgreSQL-engine languages: outputs data.sql (text dump). * * Usage: npx tsx scripts/generate-language.ts --lang [--password | --plain] * or: npx tsx scripts/generate-language.ts --all [--password | --plain] @@ -88,12 +91,11 @@ function getPersonAddress(lang: LanguageDefinition, personIndex: number): string return `${lang.streets[person.streetIndex]} ${person.streetSuffix}`; } -// ── SQL generation ───────────────────────────────────────────── +// ── SQL generation (SQLite) ───────────────────────────────────── function generateCreateTableSQL(lang: LanguageDefinition): string { const t = lang.tables; const c = lang.columns; - // We need to reproduce the exact same schema structure with translated names const statements: string[] = []; // Person @@ -122,6 +124,41 @@ function generateCreateTableSQL(lang: LanguageDefinition): string { return statements.join("\n"); } +// ── SQL generation (PostgreSQL) ───────────────────────────────── +function generateCreateTablePgSQL(lang: LanguageDefinition): string { + const t = lang.tables; + const c = lang.columns; + + // No double-quoting: PG lowercases unquoted identifiers, which matches how + // oracle queries reference tables/columns (also unquoted). + const statements: string[] = []; + + // Person + statements.push(`CREATE TABLE ${t.Person} (${c.Person.id} TEXT NOT NULL, ${c.Person.name} TEXT NOT NULL, ${c.Person.address} TEXT NOT NULL, ${c.Person.postalCode} TEXT NOT NULL, ${c.Person.city} TEXT NOT NULL, ${c.Person.phone} TEXT NOT NULL, PRIMARY KEY (${c.Person.id}));`); + + // Room + statements.push(`CREATE TABLE ${t.Room} (${c.Room.id} INTEGER NOT NULL, ${c.Room.name} TEXT NOT NULL, ${c.Room.capacity} INTEGER NOT NULL, PRIMARY KEY (${c.Room.id}));`); + statements.push(`CREATE UNIQUE INDEX rum_namn ON ${t.Room} (${c.Room.name});`); + + // Student — hasDisability uses BOOLEAN + statements.push(`CREATE TABLE ${t.Student} (${c.Student.id} TEXT NOT NULL, ${c.Student.hasDisability} BOOLEAN NOT NULL, CONSTRAINT STUDENT_PK PRIMARY KEY (${c.Student.id}), CONSTRAINT Student_Person_FK FOREIGN KEY (${c.Student.id}) REFERENCES ${t.Person} (${c.Person.id}) ON DELETE RESTRICT ON UPDATE CASCADE);`); + + // Teacher + statements.push(`CREATE TABLE ${t.Teacher} (${c.Teacher.id} TEXT NOT NULL, ${c.Teacher.officeRoom} INTEGER NOT NULL, CONSTRAINT teacher_pk PRIMARY KEY (${c.Teacher.id}), CONSTRAINT fk_teacher_person FOREIGN KEY (${c.Teacher.id}) REFERENCES ${t.Person} (${c.Person.id}) ON DELETE RESTRICT ON UPDATE CASCADE);`); + + // Course + statements.push(`CREATE TABLE ${t.Course} (${c.Course.code} TEXT NOT NULL, ${c.Course.name} TEXT NOT NULL, ${c.Course.duration} INTEGER NOT NULL, ${c.Course.price} INTEGER NOT NULL, ${c.Course.description} TEXT NOT NULL, PRIMARY KEY (${c.Course.code}));`); + statements.push(`CREATE UNIQUE INDEX kurs_ben ON ${t.Course} (${c.Course.name});`); + + // CourseInstance — startDate uses DATE + statements.push(`CREATE TABLE ${t.CourseInstance} (${c.CourseInstance.course} TEXT NOT NULL, ${c.CourseInstance.startDate} DATE NOT NULL, ${c.CourseInstance.teacher} TEXT NOT NULL, ${c.CourseInstance.room} INTEGER NOT NULL, CONSTRAINT courseinstance_pk PRIMARY KEY (${c.CourseInstance.course}, ${c.CourseInstance.startDate}), CONSTRAINT fk_courseinstance_course FOREIGN KEY (${c.CourseInstance.course}) REFERENCES ${t.Course} (${c.Course.code}) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_teacher FOREIGN KEY (${c.CourseInstance.teacher}) REFERENCES ${t.Teacher} (${c.Teacher.id}) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_courseinstance_room FOREIGN KEY (${c.CourseInstance.room}) REFERENCES ${t.Room} (${c.Room.id}) ON DELETE RESTRICT ON UPDATE CASCADE);`); + + // Participation — startDate uses DATE + statements.push(`CREATE TABLE ${t.Participation} (${c.Participation.student} TEXT NOT NULL, ${c.Participation.course} TEXT NOT NULL, ${c.Participation.startDate} DATE NOT NULL, CONSTRAINT participation_pk PRIMARY KEY (${c.Participation.course}, ${c.Participation.startDate}, ${c.Participation.student}), CONSTRAINT fk_participation_courseinstance FOREIGN KEY (${c.Participation.course}, ${c.Participation.startDate}) REFERENCES ${t.CourseInstance} (${c.CourseInstance.course}, ${c.CourseInstance.startDate}) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_participation_student FOREIGN KEY (${c.Participation.student}) REFERENCES ${t.Student} (${c.Student.id}) ON DELETE RESTRICT ON UPDATE CASCADE);`); + + return statements.join("\n"); +} + function generateInsertSQL(lang: LanguageDefinition): string { const t = lang.tables; const esc = (s: string) => s.replace(/'/g, "''"); @@ -181,6 +218,65 @@ function generateInsertSQL(lang: LanguageDefinition): string { return statements.join("\n"); } +function generateInsertPgSQL(lang: LanguageDefinition): string { + const t = lang.tables; + const esc = (s: string) => s.replace(/'/g, "''"); + const statements: string[] = []; + + // Persons + for (let i = 0; i < oracle.persons.length; i++) { + const p = oracle.persons[i]; + const name = getPersonName(lang, i); + const address = getPersonAddress(lang, i); + statements.push(`INSERT INTO ${t.Person} VALUES ('${esc(lang.personIds[i])}', '${esc(name)}', '${esc(address)}', '${esc(lang.postalCodes[i])}', '${esc(lang.cities[p.cityIndex])}', '${esc(lang.phones[i])}');`); + } + + // Rooms + for (let i = 0; i < oracle.rooms.length; i++) { + const r = oracle.rooms[i]; + statements.push(`INSERT INTO ${t.Room} VALUES (${r.id}, '${esc(lang.roomNames[i])}', ${r.capacity});`); + } + + // Students — BOOLEAN values + for (let i = 0; i < oracle.persons.length; i++) { + const p = oracle.persons[i]; + if (p.isStudent) { + statements.push(`INSERT INTO ${t.Student} VALUES ('${esc(lang.personIds[i])}', ${p.disability ? "TRUE" : "FALSE"});`); + } + } + + // Teachers + for (let i = 0; i < oracle.persons.length; i++) { + const p = oracle.persons[i]; + if (p.isTeacher) { + statements.push(`INSERT INTO ${t.Teacher} VALUES ('${esc(lang.personIds[i])}', ${p.officeRoom});`); + } + } + + // Courses + for (let i = 0; i < oracle.courses.length; i++) { + const cr = oracle.courses[i]; + const key = cr.key as keyof typeof lang.courseCodes; + statements.push(`INSERT INTO ${t.Course} VALUES ('${esc(lang.courseCodes[key])}', '${esc(lang.courseNames[key])}', ${cr.duration}, ${cr.price}, '${esc(lang.courseDescriptions[key])}');`); + } + + // Course Instances — DATE values + for (const ci of oracle.courseInstances) { + const courseKey = oracle.courses[ci.courseIndex].key as keyof typeof lang.courseCodes; + const teacherId = lang.personIds[ci.teacherPersonIndex]; + statements.push(`INSERT INTO ${t.CourseInstance} VALUES ('${esc(lang.courseCodes[courseKey])}', '${esc(ci.startDate)}', '${esc(teacherId)}', ${oracle.rooms[ci.roomIndex].id});`); + } + + // Participations — DATE values + for (const p of oracle.participations) { + const courseKey = oracle.courses[p.courseIndex].key as keyof typeof lang.courseCodes; + const studentId = lang.personIds[p.studentPersonIndex]; + statements.push(`INSERT INTO ${t.Participation} VALUES ('${esc(studentId)}', '${esc(lang.courseCodes[courseKey])}', '${esc(p.startDate)}');`); + } + + return statements.join("\n"); +} + // ── Query template resolution ────────────────────────────────── function resolveQuery(template: string, lang: LanguageDefinition): string { return template.replace(/\{\{(\w+):([^}]+)\}\}/g, (_, type: string, ref: string) => { @@ -219,20 +315,18 @@ function resolveQuery(template: string, lang: LanguageDefinition): string { }); } -// ── Generate a single language ──────────────────────────────── +// ── Generate: SQLite ──────────────────────────────────────────── type ResultData = { columns: string[]; values: (string | number | null)[][] }; -async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise { - const lang = await loadLanguage(code); - console.log(`Generating language data for: ${lang.displayName} (${lang.code})`); +async function generateSqliteLanguage(lang: LanguageDefinition, SQL: SqlJsStatic): Promise { + console.log(`Generating SQLite data for: ${lang.displayName} (${lang.code})`); const db = new SQL.Database(); - // Create schema and insert data const createSQL = generateCreateTableSQL(lang); const insertSQL = generateInsertSQL(lang); - db.run("PRAGMA foreign_keys = OFF;"); // OFF during bulk insert for performance + db.run("PRAGMA foreign_keys = OFF;"); for (const stmt of createSQL.split(";\n").filter(s => s.trim())) { db.run(stmt + ";"); } @@ -241,31 +335,14 @@ async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise new Date(date).getFullYear()); db.create_function("MONTH", (date: string) => new Date(date).getMonth() + 1); db.create_function("DAY", (date: string) => new Date(date).getDate()); - // Build question pool by running reference queries - const categoryMap = new Map; - }>(); - - let errorCount = 0; - function runQuery(queryTemplate: string): ResultData { const resolvedQuery = resolveQuery(queryTemplate, lang); const res = db.exec(resolvedQuery); if (res.length > 0) { - // Replace expression column names (e.g. "COUNT(student)") with the language's aggregate label const columns = res[0].columns.map(col => /[()]/.test(col) ? lang.aggregateLabel : col ); @@ -274,9 +351,93 @@ async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise { + console.log(`Generating PostgreSQL data for: ${lang.displayName} (${outputCode})`); + + const { PGlite } = await import("@electric-sql/pglite"); + const db = new PGlite(); + + const createSQL = generateCreateTablePgSQL(lang); + const insertSQL = generateInsertPgSQL(lang); + + const fullSQL = createSQL + "\n" + insertSQL; + await db.exec(fullSQL); + + async function runQueryAsync(queryTemplate: string): Promise { + const resolvedQuery = resolveQuery(queryTemplate, lang); + const res = await db.query(resolvedQuery); + if (res.rows.length > 0 || res.fields.length > 0) { + const columns = res.fields.map(f => + /[()]/.test(f.name) ? lang.aggregateLabel : f.name + ); + const values = res.rows.map(row => { + return columns.map((_, i) => { + const val = (row as Record)[res.fields[i].name]; + if (val instanceof Date) return val.toISOString().split("T")[0]; + if (typeof val === "boolean") return val ? 1 : 0; + if (val === null || val === undefined) return null; + return val as string | number; + }); + }); + return { columns, values }; + } + return { columns: [], values: [] }; + } + + const { categories, errorCount } = await buildQuestionPoolAsync(lang, runQueryAsync); + + const outputDir = join(__dirname, "..", "public", "languages", outputCode); + if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true }); + + writeQuestionPool(outputDir, lang, categories, errorCount, "postgresql"); + + // SQL dump file + writeFileSync(join(outputDir, "data.sql"), fullSQL, "utf-8"); + console.log(`Wrote data.sql (${fullSQL.length} bytes)`); + + await db.close(); + return errorCount === 0; +} + +// ── Shared question pool building ────────────────────────────── +interface CategoryEntry { + category_id: number; + display_number: number; + questions: Array<{ + id: number; + description: string; + display_sequence: string; + result: ResultData; + alternative_results?: ResultData[]; + }>; +} + +function buildQuestionPool( + lang: LanguageDefinition, + runQuery: (template: string) => ResultData, +): { categories: CategoryEntry[]; errorCount: number } { + const categoryMap = new Map(); + let errorCount = 0; + for (const q of oracle.questions) { const description = lang.questionDescriptions[q.id]; - if (!description) { console.error(`Missing question description for Q${q.id} in language ${lang.code}`); errorCount++; @@ -293,7 +454,6 @@ async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise 0) { alternative_results = []; @@ -302,13 +462,10 @@ async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise 0) { - console.error(`\n${errorCount} error(s) occurred. Generated data may be incomplete.`); + const categories = Array.from(categoryMap.values()).sort((a, b) => a.category_id - b.category_id); + for (const cat of categories) { + cat.questions.sort((a, b) => a.display_sequence.localeCompare(b.display_sequence)); + } + + return { categories, errorCount }; +} + +async function buildQuestionPoolAsync( + lang: LanguageDefinition, + runQuery: (template: string) => Promise, + isPg = true, +): Promise<{ categories: CategoryEntry[]; errorCount: number }> { + const categoryMap = new Map(); + let errorCount = 0; + + for (const q of oracle.questions) { + const description = lang.questionDescriptions[q.id]; + if (!description) { + console.error(`Missing question description for Q${q.id} in language ${lang.code}`); + errorCount++; + continue; + } + + const queryTemplate = (isPg && q.pgQuery) ? q.pgQuery : q.query; + let result: ResultData; + try { + result = await runQuery(queryTemplate); + } catch (e) { + console.error(`Error running query for Q${q.id}: ${(e as Error).message}`); + console.error(` Resolved query: ${resolveQuery(queryTemplate, lang)}`); + errorCount++; + continue; + } + + const altQueries = (isPg && q.pgAlternativeQueries) ? q.pgAlternativeQueries : q.alternativeQueries; + let alternative_results: ResultData[] | undefined; + if (altQueries && altQueries.length > 0) { + alternative_results = []; + for (let i = 0; i < altQueries.length; i++) { + try { + alternative_results.push(await runQuery(altQueries[i])); + } catch (e) { + console.error(`Error running alternative query ${i + 1} for Q${q.id}: ${(e as Error).message}`); + errorCount++; + } + } + if (alternative_results.length === 0) alternative_results = undefined; + } + + if (!categoryMap.has(q.categoryId)) { + categoryMap.set(q.categoryId, { + category_id: q.categoryId, + display_number: q.displayNumber, + questions: [], + }); + } + + categoryMap.get(q.categoryId)!.questions.push({ + id: q.id, + description, + display_sequence: q.displaySequence, + result, + ...(alternative_results ? { alternative_results } : {}), + }); } - // Sort categories and questions - const categories = Array.from(categoryMap.values()) - .sort((a, b) => a.category_id - b.category_id); + const categories = Array.from(categoryMap.values()).sort((a, b) => a.category_id - b.category_id); for (const cat of categories) { cat.questions.sort((a, b) => a.display_sequence.localeCompare(b.display_sequence)); } - // Write output - const outputDir = join(__dirname, "..", "public", "languages", code); - if (!existsSync(outputDir)) { - mkdirSync(outputDir, { recursive: true }); - } + return { categories, errorCount }; +} - // Question pool JSON +function writeQuestionPool( + outputDir: string, + lang: LanguageDefinition, + categories: CategoryEntry[], + errorCount: number, + engine: "sqlite" | "postgresql" = "sqlite", +): void { + const totalQuestions = categories.reduce((sum, c) => sum + c.questions.length, 0); const questionPool = { language: lang.code, + engine, defaultQuery: `SELECT * FROM ${lang.tables.Student};`, questions: categories, }; @@ -356,16 +579,7 @@ async function generateForLanguage(code: string, SQL: SqlJsStatic): Promise 0 ? `, ${errorCount} errors` : ""})`); } // ── Main ─────────────────────────────────────────────────────── @@ -377,7 +591,16 @@ async function main() { let allOk = true; for (const code of codes) { - const ok = await generateForLanguage(code, SQL); + const lang = await loadLanguage(code); + // Skip engine-variant packs (e.g. sv-pg) — we generate those automatically + if (lang.engine === "postgresql") continue; + + // Generate SQLite data + let ok = await generateSqliteLanguage(lang, SQL); + if (!ok) allOk = false; + + // Generate PostgreSQL data (into {code}-pg/ directory) + ok = await generatePgLanguage(lang, `${code}-pg`); if (!ok) allOk = false; } diff --git a/src/App.tsx b/src/App.tsx index 0f38f39..6b59cc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,24 +16,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import Editor from "react-simple-code-editor"; import "./App.css"; import ResultTable from "./ResultTable"; import ExportRenderer from "./ExportRenderer"; import QuestionSelector, { Question } from "./QuestionSelector"; -// @ts-expect-error - No types available -import { highlight, languages } from "prismjs/components/prism-core"; -import "prismjs/components/prism-sql"; -import "prismjs/themes/prism.css"; import { format } from "sql-formatter"; import initSqlJs from "sql.js"; import ViewsTable, { View } from "./ViewsTable"; import { raToSQL, RAError } from "./ra-engine/relationalAlgebra"; import RAReference from "./ra-engine/RAReference"; -import { highlightRA } from "./ra-engine/raHighlight"; import RAPreview from "./ra-engine/RAPreview"; +import SqlEditor, { type SqlEditorHandle } from "./SqlEditor"; import { Info, Settings, XCircle, CheckCircle2, Eye, EyeOff } from "lucide-react"; import ChangelogDialog from "./ChangelogDialog"; @@ -58,28 +53,42 @@ import ImportDialog, { ImportDialogHandle } from "./ImportDialog"; import { ParsedSaveData, parseImportFile, getLocalData, detectConflicts } from "./mergeUtils"; import { useLanguage, langKey, getUrlParam, setUrlParam } from "./i18n/context"; import LanguageSelector from "./LanguageSelector"; +import EngineSelector from "./EngineSelector"; import { getQuestion } from "./QuestionSelector"; +import type { DatabaseEngine } from "./database/types"; +import { SqliteEngine } from "./database/sqliteEngine"; +import { PgliteEngine } from "./database/pgliteEngine"; +import { useEditorSettings } from "./useEditorSettings"; +import EditorSettingsDialog from "./EditorSettingsDialog"; + /** Storage key namespaced by editor mode */ function modeKey(base: string, mode: "sql" | "ra"): string { return mode === "ra" ? `ra-${base}` : base; } -/** Get a JSON-parsed list from localStorage, with language and mode namespacing */ -function getStoredList(lang: string, key: string): number[] { - const raw = localStorage.getItem(langKey(lang, key)); +/** Build a localStorage key namespaced by language and engine */ +function engineKey(lang: string, engine: string, key: string): string { + return langKey(lang, `${engine}:${key}`); +} + +/** Get a JSON-parsed list from localStorage, with language+engine+mode namespacing */ +function getStoredList(lang: string, engine: string, key: string): number[] { + const raw = localStorage.getItem(engineKey(lang, engine, key)); return raw ? JSON.parse(raw) : []; } -/** Set a JSON list in localStorage, with language namespacing */ -function setStoredList(lang: string, key: string, value: number[]): void { - localStorage.setItem(langKey(lang, key), JSON.stringify(value)); +/** Set a JSON list in localStorage, with language+engine namespacing */ +function setStoredList(lang: string, engine: string, key: string, value: number[]): void { + localStorage.setItem(engineKey(lang, engine, key), JSON.stringify(value)); } function App() { - const { lang, t, questions, dbArrayBuffer, defaultQuery } = useLanguage(); + const { lang, t, questions, dbData, defaultQuery, engine, setEngine } = useLanguage(); + const { settings: editorSettings, setSettings: setEditorSettings } = useEditorSettings(); const [question, setQuestion] = useState(); - const [database, setDatabase] = useState(); + const [database, setDatabase] = useState(); + const [dbSchema, setDbSchema] = useState>({}); const [error, setError] = useState(null); // flag for correct result query being present but current (which might not be evaluated) is not the same const [correctQueryMismatch, setCorrectQueryMismatch] = useState(false); @@ -106,19 +115,22 @@ function App() { const exportRendererRef = useRef(null); - const editorRef = useRef(null); + const sqlEditorRef = useRef(null); const exportModalRef = useRef(null); const importDialogRef = useRef(null); const [pendingImportData, setPendingImportData] = useState(null); // QuestionSelector needs writtenQuestions and correctQuestions to be able to display the correct state const [writtenQuestions, setWrittenQuestions] = useState(() => - getStoredList(lang, modeKey("writtenQuestions", editorMode)) + getStoredList(lang, engine, modeKey("writtenQuestions", editorMode)) ); const [correctQuestions, setCorrectQuestions] = useState(() => - getStoredList(lang, modeKey("correctQuestions", editorMode)) + getStoredList(lang, engine, modeKey("correctQuestions", editorMode)) ); + /** sql-formatter language based on engine */ + const formatterLang = engine === "postgresql" ? "postgresql" : "sqlite"; + // One-time migration: copy old unnamespaced keys to sv: prefix useEffect(() => { if (localStorage.getItem("i18n-migrated")) return; @@ -149,8 +161,8 @@ function App() { // Reload written/correct questions when language or mode changes useEffect(() => { - setWrittenQuestions(getStoredList(lang, modeKey("writtenQuestions", editorMode))); - setCorrectQuestions(getStoredList(lang, modeKey("correctQuestions", editorMode))); + setWrittenQuestions(getStoredList(lang, engine, modeKey("writtenQuestions", editorMode))); + setCorrectQuestions(getStoredList(lang, engine, modeKey("correctQuestions", editorMode))); }, [lang, editorMode]); const resetResult = useCallback(() => { @@ -167,9 +179,9 @@ function App() { if (question && query !== undefined) { const currentKey = modeKey(`questionId-${question.id}`, editorMode); if (query === defaultQuery || query === "") { - localStorage.removeItem(langKey(lang, currentKey)); + localStorage.removeItem(engineKey(lang, engine, currentKey)); } else { - localStorage.setItem(langKey(lang, currentKey), query); + localStorage.setItem(engineKey(lang, engine, currentKey), query); } } setEditorMode(mode); @@ -179,26 +191,50 @@ function App() { // Load query for the new mode if (question) { const newKey = modeKey(`questionId-${question.id}`, mode); - setQuery(localStorage.getItem(langKey(lang, newKey)) || (mode === "ra" ? "" : defaultQuery)); + setQuery(localStorage.getItem(engineKey(lang, engine, newKey)) || (mode === "ra" ? "" : defaultQuery)); } // Reload progress for the new mode - setWrittenQuestions(getStoredList(lang, modeKey("writtenQuestions", mode))); - setCorrectQuestions(getStoredList(lang, modeKey("correctQuestions", mode))); + setWrittenQuestions(getStoredList(lang, engine, modeKey("writtenQuestions", mode))); + setCorrectQuestions(getStoredList(lang, engine, modeKey("correctQuestions", mode))); }, [resetResult, question, query, editorMode, lang, defaultQuery]); const initDb = useCallback(async () => { - if (!dbArrayBuffer) return; + if (!dbData) return; resetResult(); - const SQL = await initSqlJs({ - locateFile: (file) => `/dist/sql.js/${file}`, - }); - const db = new SQL.Database(new Uint8Array(dbArrayBuffer)); - db.create_function("YEAR", (date: string) => new Date(date).getFullYear()); - db.create_function("MONTH", (date: string) => new Date(date).getMonth() + 1); - db.create_function("DAY", (date: string) => new Date(date).getDate()); - db.exec("PRAGMA foreign_keys = ON;"); + + let db: DatabaseEngine; + if (engine === "postgresql") { + try { + const { PGlite } = await import("@electric-sql/pglite"); + const pg = new PGlite(); + await pg.exec(dbData as string); + db = new PgliteEngine(pg); + } catch (e) { + // PGlite is a multi-MB WASM bundle loaded on demand; a failed fetch/init must + // not leave the app stuck with no usable database. Surface it and fall back to + // the always-available SQLite engine. + console.error("Failed to initialize the PostgreSQL engine:", e); + setError(t("engineLoadFailed")); + setEngine("sqlite"); + return; + } + } else { + const SQL = await initSqlJs({ + locateFile: (file) => `/dist/sql.js/${file}`, + }); + const sqliteDb = new SQL.Database(new Uint8Array(dbData as ArrayBuffer)); + sqliteDb.create_function("YEAR", (date: string) => new Date(date).getFullYear()); + sqliteDb.create_function("MONTH", (date: string) => new Date(date).getMonth() + 1); + sqliteDb.create_function("DAY", (date: string) => new Date(date).getDate()); + sqliteDb.exec("PRAGMA foreign_keys = ON;"); + db = new SqliteEngine(sqliteDb); + } + + // Extract schema for autocomplete + const schema = await db.getSchema(); + setDbSchema(schema); setDatabase(db); - }, [resetResult, dbArrayBuffer]); + }, [resetResult, dbData, engine, t, setEngine]); useEffect(() => { initDb(); @@ -217,6 +253,21 @@ function App() { setUrlParam("q", null); }, [lang, resetResult]); + // On engine change, keep the question but reload query and reset results + const prevEngineRef = useRef(engine); + useEffect(() => { + if (prevEngineRef.current === engine) return; + prevEngineRef.current = engine; + // Reload the query for this question under the new engine context + if (question) { + const key = modeKey(`questionId-${question.id}`, editorMode); + setQuery(localStorage.getItem(engineKey(lang, engine, key)) || (editorMode === "ra" ? "" : defaultQuery)); + } + setViews([]); + setDisplayViewsTable(false); + resetResult(); + }, [engine, question, editorMode, lang, defaultQuery, resetResult]); + // Restore question from URL param when questions become available const urlRestoredRef = useRef(false); useEffect(() => { @@ -232,7 +283,7 @@ function App() { const resolved = getQuestion(q.id, questions); if (resolved) { setQuestion(resolved); - setQuery(localStorage.getItem(langKey(lang, modeKey(`questionId-${resolved.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); + setQuery(localStorage.getItem(engineKey(lang, engine, modeKey(`questionId-${resolved.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); } } } @@ -243,7 +294,6 @@ function App() { if (question) { setUrlParam("q", `${question.category.display_number}${question.display_sequence}`); } else if (urlRestoredRef.current) { - // Only clear ?q= after initial restore, not during it setUrlParam("q", null); } }, [question]); @@ -259,90 +309,85 @@ function App() { if (question) questionLangRef.current = lang; }, [question, lang]); + // Validate query on change useEffect(() => { if (!database || !question || query === undefined) { return; } - // Don't write to localStorage if lang changed but question hasn't been cleared yet if (questionLangRef.current !== lang) { return; } const wqStorageKey = modeKey("writtenQuestions", editorMode); - let wq = getStoredList(lang, wqStorageKey); + let wq = getStoredList(lang, engine, wqStorageKey); const initialLength = wq.length; const storageKey = modeKey(`questionId-${question.id}`, editorMode); if (query === defaultQuery || query === "") { - localStorage.removeItem(langKey(lang, storageKey)); + localStorage.removeItem(engineKey(lang, engine, storageKey)); wq = wq.filter((id: number) => id !== question.id); } else { - localStorage.setItem(langKey(lang, storageKey), query); + localStorage.setItem(engineKey(lang, engine, storageKey), query); if (!wq.includes(question.id)) { wq.push(question.id); } } if (wq.length !== initialLength) { - setStoredList(lang, wqStorageKey, wq); + setStoredList(lang, engine, wqStorageKey, wq); setWrittenQuestions(wq); } - if (editorMode === "sql") { - try { - // Check for multiple statements - let stmtCount = 0; - for (const stmt of database.iterateStatements(query)) { - stmtCount++; - stmt.free(); - if (stmtCount > 1) { - setError(t("multipleStatements")); - return; - } - } - setError(null); - } catch (e) { - // @ts-expect-error - Error.message is a string - setError(e.message); - } - } else { - // In RA mode, try to parse and validate against database - try { - raToSQL(query, database); - setError(null); - } catch (e) { - if (e instanceof RAError) { - setError(t("raParseError", { message: e.message })); + // Stale check for async validation + let stale = false; + const validate = async () => { + if (editorMode === "sql") { + const errorMsg = await database.validateStatements(query); + if (stale) return; + if (errorMsg === "multiple_statements") { + setError(t("multipleStatements")); + } else if (errorMsg) { + setError(errorMsg); } else { - // @ts-expect-error - Error.message is a string - setError(e.message); + setError(null); + } + } else { + // In RA mode, try to parse and validate against database + try { + await raToSQL(query, database); + if (stale) return; + setError(null); + } catch (e) { + if (stale) return; + if (e instanceof RAError) { + setError(t("raParseError", { message: e.message })); + } else { + setError((e as Error).message); + } } } - } + }; + validate(); + return () => { stale = true; }; }, [database, query, question, lang, defaultQuery, t, editorMode]); - const refreshViews = useCallback((upsert: boolean) => { + const refreshViews = useCallback(async (upsert: boolean) => { if (!database) { return; } - const res = database.exec('SELECT name, sql FROM sqlite_master WHERE type="view"'); - let fetchedViews: View[] = []; - if (res.length !== 0) { - const fetched = res[0].values as string[][]; - fetchedViews = fetched.map(([name, query]) => ({ name, query })); - } + const fetchedViews = await database.getViews(); if (fetchedViews.length - views.length !== 0) { setDisplayViewsTable(true); } if (upsert) { - localStorage.setItem(langKey(lang, "views"), JSON.stringify(fetchedViews)); + localStorage.setItem(engineKey(lang, engine, "views"), JSON.stringify(fetchedViews)); } setViews(fetchedViews); // Recreate missing views from localStorage - const storedViews = localStorage.getItem(langKey(lang, "views")); + const storedViews = localStorage.getItem(engineKey(lang, engine, "views")); if (storedViews) { const savedViews: View[] = JSON.parse(storedViews); const missingViews = savedViews.filter( @@ -351,7 +396,7 @@ function App() { let recreated = 0; for (const view of missingViews) { try { - database.exec(view.query); + await database.exec(view.query); recreated++; } catch (e) { console.warn(`Failed to recreate view "${view.name}":`, (e as Error).message); @@ -363,7 +408,7 @@ function App() { } }, [database, views.length, lang]); - const runQuery = useCallback(() => { + const runQuery = useCallback(async () => { if (!database || query === undefined) { return; } @@ -371,78 +416,71 @@ function App() { let sqlToRun = query; if (editorMode === "ra") { try { - sqlToRun = raToSQL(query, database); + sqlToRun = await raToSQL(query, database); } catch (e) { if (e instanceof RAError) { setError(t("raParseError", { message: e.message })); } else { - // @ts-expect-error - Error.message is a string - setError(e.message); + setError((e as Error).message); } return; } } - const res = database.exec(sqlToRun); + const res = await database.exec(sqlToRun); setIsViewResult(false); setQueryedView(null); setEvaluatedQuery(query); if (res.length !== 0) { const { columns, values } = res[0]; - setResult({ columns, data: values }); + setResult({ columns, data: values as (string | number | Uint8Array | null)[][] }); } else { setResult({columns: [], data: []}); } - refreshViews(true); + await refreshViews(true); } catch (e) { - // @ts-expect-error - Error.message is a string - setError(e.message); + setError((e as Error).message); } }, [database, query, refreshViews, editorMode, t]); - const evalSql = useCallback((sql: string): Result => { + const evalSql = useCallback(async (sql: string): Promise => { if (!database) { return { columns: [], data: [] }; } try { - const res = database.exec(sql); - let result: Result; + const res = await database.exec(sql); if (res.length !== 0) { const { columns, values } = res[0]; - result = { columns, data: values }; - } else { - result = {columns: [], data: []}; + return { columns, data: values as (string | number | Uint8Array | null)[][] }; } - return result; + return { columns: [], data: [] }; } catch (e) { - // @ts-expect-error - Error.message is a string - alert("Error occurred while evaluating SQL Query internally: " + e.message); + alert("Error occurred while evaluating SQL Query internally: " + (e as Error).message); return { columns: [], data: [] }; } }, [database]); - const getViewResult = useCallback((name: string) => { + const getViewResult = useCallback(async (name: string) => { if (!database) { return; } try { const viewQuery = `SELECT * FROM ${name}`; - const res = database.exec(viewQuery); + const res = await database.exec(viewQuery); setIsViewResult(true); setQueryedView(name); setEvaluatedQuery(viewQuery); if (res.length !== 0) { const { columns, values } = res[0]; - setResult({ columns, data: values }); + setResult({ columns, data: values as (string | number | Uint8Array | null)[][] }); } else { setResult({columns: [], data: []}); } } catch (e) { - // @ts-expect-error - Error.message is a string - setError(e.message); + setError((e as Error).message); } }, [database]); - const deleteView = useCallback((name: string) => { + const deleteView = useCallback(async (name: string) => { if (!database) { return; } @@ -451,8 +489,8 @@ function App() { return; } - database.exec(`DROP VIEW ${name}`); - refreshViews(true); + await database.exec(`DROP VIEW ${name}`); + await refreshViews(true); if (isViewResult && queryedView === name) { resetResult(); @@ -482,15 +520,15 @@ function App() { setMatchedResult(matchedAlt ?? question.evaluable_result); const cKey = modeKey(`correctQuestionId-${question.id}`, editorMode); - localStorage.setItem(langKey(lang, cKey), query); + localStorage.setItem(engineKey(lang, engine, cKey), query); setCorrectQueryMismatch(false); setLoadedQuestionCorrect(true); const cqStorageKey = modeKey("correctQuestions", editorMode); - const cq = getStoredList(lang, cqStorageKey); + const cq = getStoredList(lang, engine, cqStorageKey); if (!cq.includes(question.id)) { cq.push(question.id); - setStoredList(lang, cqStorageKey, cq); + setStoredList(lang, engine, cqStorageKey, cq); setCorrectQuestions(cq); } }, [result, question, query, evaluatedQuery, exportingStatus, lang, editorMode]); @@ -498,10 +536,9 @@ function App() { // Save query based on question const loadQuery = useCallback((_oldQuestion: Question | undefined, newQuestion: Question) => { const key = modeKey(`questionId-${newQuestion.id}`, editorMode); - setQuery(localStorage.getItem(langKey(lang, key)) || (editorMode === "ra" ? "" : defaultQuery)); - // This prevents user from ctrl-z'ing to a different question - if (editorRef.current) { - editorRef.current!.session = {history: { stack: [], offset: 0 }}; + setQuery(localStorage.getItem(engineKey(lang, engine, key)) || (editorMode === "ra" ? "" : defaultQuery)); + if (sqlEditorRef.current) { + sqlEditorRef.current.clearHistory(); } }, [setQuery, lang, defaultQuery, editorMode]); @@ -512,7 +549,7 @@ function App() { } const cKey = modeKey(`correctQuestionId-${question.id}`, editorMode); - const correctQuery = localStorage.getItem(langKey(lang, cKey)); + const correctQuery = localStorage.getItem(engineKey(lang, engine, cKey)); if (!correctQuery) { setCorrectQueryMismatch(false); setLoadedQuestionCorrect(false); @@ -522,13 +559,12 @@ function App() { setLoadedQuestionCorrect(true); if (editorMode === "ra") { - // For RA, compare raw text (trimmed) setCorrectQueryMismatch(query.trim() !== correctQuery.trim()); } else { let currentQuery = ""; try { currentQuery = format(query + (query.endsWith(";") ? "" : ";"), { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", @@ -540,7 +576,7 @@ function App() { return; } const correctQueryFormatted = format(correctQuery + (correctQuery?.endsWith(";") ? "" : ";"), { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", @@ -549,7 +585,7 @@ function App() { }); setCorrectQueryMismatch(currentQuery !== correctQueryFormatted); } - }, [database, question, query, lang, editorMode]); + }, [database, question, query, lang, editorMode, formatterLang]); const exportData = useCallback((options?: { include?: number[]}) => { if (!database) { @@ -567,18 +603,20 @@ function App() { output += "/* --- BEGIN Metadata --- */\n"; output += "/* --- BEGIN Save Format Version --- */\n"; - output += "-- 3\n"; + output += "-- 4\n"; output += "/* --- END Save Format Version --- */\n"; output += "/* --- BEGIN Save Language --- */\n"; output += `-- ${lang}\n`; output += "/* --- END Save Language --- */\n"; + output += "/* --- BEGIN Save Engine --- */\n"; + output += `-- ${engine}\n`; + output += "/* --- END Save Engine --- */\n"; output += "/* --- END Metadata --- */\n"; output += "/* --- BEGIN Validation --- */\n"; - // Cache localStorage reads used multiple times below - const storedCorrectIds = getStoredList(lang, "correctQuestions"); - const storedWrittenIds = getStoredList(lang, "writtenQuestions"); + const storedCorrectIds = getStoredList(lang, engine, "correctQuestions"); + const storedWrittenIds = getStoredList(lang, engine, "writtenQuestions"); const formatQuestionIds = (ids: number[]) => ids.map((id) => { const category = questions.find(c => c.questions.some(q => q.id === id))!; @@ -595,7 +633,7 @@ function App() { output += "/* --- BEGIN Views --- */\n"; const viewsString = views.map(view => { let out = format(view.query, { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", @@ -621,13 +659,13 @@ function App() { const questionQueries = sorted.map((id: number) => { const category = questions.find(c => c.questions.some(q => q.id === id))!; const q = category.questions.find(q => q.id === id)!; - const activeQuery = localStorage.getItem(langKey(lang, "correctQuestionId-" + id)); + const activeQuery = localStorage.getItem(engineKey(lang, engine, "correctQuestionId-" + id)); if (!activeQuery) { return ""; } let formatted = `/* --- BEGIN Question ${category.display_number}${q.display_sequence} (REFERENCE: ${q.id}) --- */\n`; formatted += format(activeQuery + (activeQuery.endsWith(";") ? "" : ";"), { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", @@ -648,7 +686,7 @@ function App() { const collectQueries = (ids: number[], keyPrefix: string) => { const queries: { [key: number]: string } = {}; for (const id of ids) { - const q = localStorage.getItem(langKey(lang, keyPrefix + id)); + const q = localStorage.getItem(engineKey(lang, engine, keyPrefix + id)); if (q) queries[id] = q; } return queries; @@ -670,7 +708,6 @@ function App() { output += "/* --- END Raw List Dumps --- */\n"; output += "/* --- END Validation --- */\n"; - // Calculate hash of everything within the validation block const hashValue = sha256(output.slice(output.indexOf("/* --- BEGIN Validation Block --- */"), output.indexOf("/* --- END Validation Block --- */"))); output += `/* --- BEGIN Hash --- */\n-- ${hashValue}\n/* --- END Hash --- */\n`; output += "/* --- END DO NOT EDIT --- */\n"; @@ -681,45 +718,47 @@ function App() { const a = document.createElement("a"); const formattedTimestamp = formatFns(new Date(), "yyyyMMdd_HHmm"); a.href = url; - a.download = `validator_${lang}_${formattedTimestamp}.sql`;; + a.download = `validator_${lang}_${engine}_${formattedTimestamp}.sql`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - }, [database, views, lang, questions]); + }, [database, views, lang, questions, formatterLang]); - const applyMergedData = useCallback((merged: ParsedSaveData) => { + const applyMergedData = useCallback(async (merged: ParsedSaveData) => { // Clear current data - getStoredList(lang, "writtenQuestions").forEach(id => localStorage.removeItem(langKey(lang, `questionId-${id}`))); - getStoredList(lang, "correctQuestions").forEach(id => localStorage.removeItem(langKey(lang, `correctQuestionId-${id}`))); - localStorage.removeItem(langKey(lang, "writtenQuestions")); - localStorage.removeItem(langKey(lang, "correctQuestions")); + getStoredList(lang, engine, "writtenQuestions").forEach(id => localStorage.removeItem(engineKey(lang, engine, `questionId-${id}`))); + getStoredList(lang, engine, "correctQuestions").forEach(id => localStorage.removeItem(engineKey(lang, engine, `correctQuestionId-${id}`))); + localStorage.removeItem(engineKey(lang, engine, "writtenQuestions")); + localStorage.removeItem(engineKey(lang, engine, "correctQuestions")); // Write merged data for (const [key, value] of Object.entries(merged.rawQueries)) { - localStorage.setItem(langKey(lang, `questionId-${key}`), value); + localStorage.setItem(engineKey(lang, engine, `questionId-${key}`), value); if (question !== undefined && Number(key) === question.id) { setQuery(value); } } for (const [key, value] of Object.entries(merged.correctQueries)) { - localStorage.setItem(langKey(lang, `correctQuestionId-${key}`), value); + localStorage.setItem(engineKey(lang, engine, `correctQuestionId-${key}`), value); } setWrittenQuestions(merged.writtenQuestionIds); setCorrectQuestions(merged.correctQuestionIds); - setStoredList(lang, "writtenQuestions", merged.writtenQuestionIds); - setStoredList(lang, "correctQuestions", merged.correctQuestionIds); + setStoredList(lang, engine, "writtenQuestions", merged.writtenQuestionIds); + setStoredList(lang, engine, "correctQuestions", merged.correctQuestionIds); // Update views in database - for (const view of views) { - database!.exec(`DROP VIEW ${view.name}`); - } - for (const view of merged.views) { - database!.exec(view.query); + if (database) { + for (const view of views) { + await database.exec(`DROP VIEW ${view.name}`); + } + for (const view of merged.views) { + await database.exec(view.query); + } + await refreshViews(true); } - refreshViews(true); }, [database, question, refreshViews, views, lang]); const importData = useCallback(() => { @@ -743,7 +782,7 @@ function App() { } const parsed = parseImportFile(data); setPendingImportData(parsed); - const local = getLocalData(lang); + const local = getLocalData(lang, engine); const analysis = detectConflicts(local, parsed); importDialogRef.current?.open(analysis); }; @@ -771,7 +810,7 @@ function App() { return; } - const toExportQuery = localStorage.getItem(langKey(lang, modeKey(`correctQuestionId-${question.id}`, editorMode))); + const toExportQuery = localStorage.getItem(engineKey(lang, engine, modeKey(`correctQuestionId-${question.id}`, editorMode))); if (!toExportQuery) { return; } @@ -798,90 +837,122 @@ function App() { setExportingStatus(1); - const triggerDownload = (dataUrl: string, filename: string) => { - const byteString = atob(dataUrl.split(",")[1]); - const mimeType = dataUrl.split(",")[0].split(":")[1].split(";")[0]; - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); + // Wait for CodeMirror to render before capturing + const capture = () => { requestAnimationFrame(() => { + if (!exportRendererRef.current) { setExportingStatus(0); return; } + + const triggerDownload = (dataUrl: string, filename: string) => { + const byteString = atob(dataUrl.split(",")[1]); + const mimeType = dataUrl.split(",")[0].split(":")[1].split(";")[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeType }); + const blobUrl = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.download = filename; + link.href = blobUrl; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + + setTimeout(() => { + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); + }, 100); + }; + + // View + if (exportView) { + toPng(exportRendererRef.current, { + canvasWidth: exportRendererRef.current.scrollWidth, + width: exportRendererRef.current.scrollWidth, + canvasHeight: exportRendererRef.current.scrollHeight, + height: exportRendererRef.current.scrollHeight, + pixelRatio: 1 + }).then((dataUrl) => { + triggerDownload(dataUrl, `validator_${exportView.name}.png`); + setExportView(undefined); + setExportingStatus(0); + }); + return; + } + + // Question + if (!question || !exportQuery || !exportQuestion) { + return; } - const blob = new Blob([ab], { type: mimeType }); - const blobUrl = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.download = filename; - link.href = blobUrl; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - - setTimeout(() => { - document.body.removeChild(link); - URL.revokeObjectURL(blobUrl); - }, 100); - }; - // View - if (exportView) { - toPng(exportRendererRef.current, { - canvasWidth: exportRendererRef.current.scrollWidth, - width: exportRendererRef.current.scrollWidth, - canvasHeight: exportRendererRef.current.scrollHeight, - height: exportRendererRef.current.scrollHeight, + const exportRenderer = exportRendererRef.current; + toPng(exportRenderer, { + canvasWidth: exportRenderer.scrollWidth, + width: exportRenderer.scrollWidth, + canvasHeight: exportRenderer.scrollHeight, + height: exportRenderer.scrollHeight, pixelRatio: 1 }).then((dataUrl) => { - triggerDownload(dataUrl, `validator_${exportView.name}.png`); - setExportView(undefined); + triggerDownload(dataUrl, `validator_${editorMode === "ra" ? "ra_" : ""}${question.id}_${question.category.display_number}${question.display_sequence}.png`); + setExportQuestion(undefined); + setExportQuery(undefined); setExportingStatus(0); }); + + }); }; // end capture + rAF + capture(); + }, [evaluatedQuery, exportQuery, exportRendererRef, getTheme, isDarkMode, exportingStatus, question, resetResult, setTheme, exportQuestion, exportView]); + + // Export renderer needs sync evalSql — we compute it eagerly when export is triggered + const [exportResult, setExportResult] = useState<{ result: Result; isCorrect: boolean } | null>(null); + const [exportViewResult, setExportViewResult] = useState(null); + + useEffect(() => { + if (!exportQuestion || !exportQuery || !database) { + setExportResult(null); return; } + const compute = async () => { + let sqlForExport = exportQuery; + if (editorMode === "ra") { + try { sqlForExport = await raToSQL(exportQuery, database); } catch { /* use raw */ } + } + const result = await evalSql(sqlForExport); + const correct = isCorrectResult(exportQuestion.evaluable_result, result) || (exportQuestion.alternative_evaluable_results?.some(alt => isCorrectResult(alt, result)) ?? false); + setExportResult({ result, isCorrect: correct }); + }; + compute(); + }, [exportQuestion, exportQuery, editorMode, database, evalSql]); - // Question - if (!question || !exportQuery || !exportQuestion) { + useEffect(() => { + if (!exportView || !database) { + setExportViewResult(null); return; } - - const exportRenderer = exportRendererRef.current; - toPng(exportRenderer, { - canvasWidth: exportRenderer.scrollWidth, - width: exportRenderer.scrollWidth, - canvasHeight: exportRenderer.scrollHeight, - height: exportRenderer.scrollHeight, - pixelRatio: 1 - }).then((dataUrl) => { - triggerDownload(dataUrl, `validator_${editorMode === "ra" ? "ra_" : ""}${question.id}_${question.category.display_number}${question.display_sequence}.png`); - setExportQuestion(undefined); - setExportQuery(undefined); - setExportingStatus(0); - }); - }, [evaluatedQuery, exportQuery, exportRendererRef, getTheme, isDarkMode, exportingStatus, question, resetResult, setTheme, exportQuestion, exportView]); + evalSql(`SELECT * FROM ${exportView.name}`).then(setExportViewResult); + }, [exportView, database, evalSql]); const exportRendererProps = useMemo(() => { - if (!exportQuestion || !exportQuery) return null; - let sqlForExport = exportQuery; - if (editorMode === "ra" && database) { - try { sqlForExport = raToSQL(exportQuery, database); } catch { /* use raw */ } - } - const exportResult = evalSql(sqlForExport); + if (!exportQuestion || !exportQuery || !exportResult) return null; return { - isCorrect: isCorrectResult(exportQuestion.evaluable_result, exportResult) || (exportQuestion.alternative_evaluable_results?.some(alt => isCorrectResult(alt, exportResult)) ?? false), + isCorrect: exportResult.isCorrect, question: exportQuestion, code: exportQuery, - result: exportResult, + result: exportResult.result, mode: editorMode, }; - }, [exportQuestion, exportQuery, editorMode, database, evalSql]); + }, [exportQuestion, exportQuery, exportResult, editorMode]); return (

{exportRendererProps && } - {exportView && } + {exportView && exportViewResult && }
+

SQL Validator

@@ -911,54 +982,60 @@ function App() {
- - - - - - exportImageQuery()} - disabled={!loadedQuestionCorrect} - > - {t("exportPng")} - - importData()}> - {t("importData")} - - exportModalRef.current?.openDialog()}> - {t("exportData")} - - - +
+ + + + + + + exportImageQuery()} + disabled={!loadedQuestionCorrect} + > + {t("exportPng")} + + importData()}> + {t("importData")} + + exportModalRef.current?.openDialog()}> + {t("exportData")} + + + +
- {query === undefined ? - null} - highlight={code => editorMode === "ra" ? highlightRA(code, isDarkMode()) : highlight(code, languages.sql)} - padding={10} - tabSize={2} - className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 min-h-32 rounded-md" - ref={editorRef} - /> - : - setQuery(code)} - highlight={code => editorMode === "ra" ? highlightRA(code, isDarkMode()) : highlight(code, languages.sql)} - padding={10} - tabSize={2} - className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 border dark:border-slate-600 border-gray-300 min-h-32 rounded-md" - ref={editorRef} + ref={sqlEditorRef} + value={query ?? (t("selectQuestionComment"))} + onChange={query !== undefined ? setQuery : undefined} + schema={dbSchema} + engine={engine} + isDarkMode={isDarkMode()} + disabled={query === undefined} + readOnly={query === undefined} + editorSettings={editorSettings} + className="font-mono w-full dark:bg-slate-800 bg-slate-100 border dark:border-slate-600 border-gray-300 min-h-32 rounded-md overflow-hidden" /> - } + ) : ( + + )} {editorMode === "ra" && query !== undefined && }
@@ -980,7 +1057,9 @@ function App() { )} {editorMode === "ra" ? - : {t("sqlReference")} + : engine === "postgresql" + ? {t("sqlReferencePostgresql")} + : {t("sqlReference")} }
@@ -991,7 +1070,7 @@ function App() { variant="outline" onClick={() => { if (!question) return; - setQuery(localStorage.getItem(langKey(lang, modeKey(`correctQuestionId-${question.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); + setQuery(localStorage.getItem(engineKey(lang, engine, modeKey(`correctQuestionId-${question.id}`, editorMode))) || (editorMode === "ra" ? "" : defaultQuery)); }} className="border-yellow-500 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-50 dark:hover:bg-yellow-900/20" > @@ -1004,7 +1083,7 @@ function App() { onClick={() => { if (!query) return; setQuery(format(query, { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", @@ -1041,7 +1120,6 @@ function App() { {/* Results Section */} {result && <> {!isViewResult ? question && <> - {/* if correct result else display wrong result */} {isCorrect ? (
@@ -1058,7 +1136,6 @@ function App() { {t("correctButVerify")}

)} - {/* Two different result tables next to each other, actual and expected */}

{t("actual")}

@@ -1076,22 +1153,20 @@ function App() { : <>

{t("viewLabel")} {queryedView}

{t("viewQueryLabel", { name: queryedView || "" })}

- view.name === queryedView) ? views.find(view => view.name === queryedView)!.query : "-- View Deleted", { - language: "sqlite", + language: formatterLang, tabWidth: 2, useTabs: false, keywordCase: "upper", dataTypeCase: "upper", functionCase: "upper", })} - onValueChange={() => null} - highlight={code => highlight(code, languages.sql)} - padding={10} - tabSize={4} - className="font-mono text-base w-full dark:bg-slate-800 bg-slate-100 max-w-4xl min-h-32 rounded-md my-2" + readOnly={true} + engine={engine} + isDarkMode={isDarkMode()} + className="font-mono w-full dark:bg-slate-800 bg-slate-100 max-w-4xl min-h-32 rounded-md my-2 overflow-hidden" />

{t("viewResultLabel", { name: queryedView || "" })}

diff --git a/src/DatabaseLayoutDialog.tsx b/src/DatabaseLayoutDialog.tsx index e30ca61..e33daf9 100644 --- a/src/DatabaseLayoutDialog.tsx +++ b/src/DatabaseLayoutDialog.tsx @@ -9,11 +9,11 @@ import dbLayoutLightFallback from "./db_layout_light.svg"; import dbLayoutLightPngFallback from "./db_layout_light_bg.png"; const DatabaseLayoutDialog = ({ isDarkMode }: { isDarkMode: () => boolean }) => { - const { lang, t } = useLanguage(); + const { lang, engine, t } = useLanguage(); const [open, setOpen] = useState(false); - // Try language-specific ERD, fallback to static imports - const basePath = `/languages/${lang}`; + // Use engine-specific ERD path (PostgreSQL ERDs are in {lang}-pg/) + const basePath = engine === "postgresql" ? `/languages/${lang}-pg` : `/languages/${lang}`; const darkSrc = `${basePath}/db_layout_dark.svg`; const lightSrc = `${basePath}/db_layout_light.svg`; const lightPngSrc = `${basePath}/db_layout_light_bg.png`; diff --git a/src/EditorSettingsDialog.tsx b/src/EditorSettingsDialog.tsx new file mode 100644 index 0000000..a74efd4 --- /dev/null +++ b/src/EditorSettingsDialog.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Checkbox } from "@radix-ui/react-checkbox"; +import { Label } from "@radix-ui/react-label"; +import { Settings2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import type { EditorSettings } from "./useEditorSettings"; +import { useLanguage } from "./i18n/context"; + +interface EditorSettingsDialogProps { + settings: EditorSettings; + onSettingsChange: (partial: Partial) => void; +} + +const EditorSettingsDialog: React.FC = ({ settings, onSettingsChange }) => { + const { t } = useLanguage(); + + return ( + + + + + + + {t("editorSettings") || "Editor Settings"} + +
+ onSettingsChange({ autocomplete: v })} + /> + onSettingsChange({ lineNumbers: v })} + /> + onSettingsChange({ highlightActiveLine: v })} + /> +
+
+
+ ); +}; + +function SettingToggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) { + const id = label.toLowerCase().replace(/\s+/g, "-"); + return ( +
+ + onChange(v === true)} + className="h-5 w-5 rounded border border-gray-300 dark:border-slate-600 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600 flex items-center justify-center" + > + {checked && } + +
+ ); +} + +export default EditorSettingsDialog; diff --git a/src/EngineSelector.tsx b/src/EngineSelector.tsx new file mode 100644 index 0000000..a50513a --- /dev/null +++ b/src/EngineSelector.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useLanguage } from "./i18n/context"; +import { AVAILABLE_ENGINES } from "./i18n/languages"; + +const EngineSelector: React.FC = () => { + const { engine, setEngine } = useLanguage(); + + return ( + + ); +}; + +export default EngineSelector; diff --git a/src/ExportRenderer.tsx b/src/ExportRenderer.tsx index 3dde3f2..cd61022 100644 --- a/src/ExportRenderer.tsx +++ b/src/ExportRenderer.tsx @@ -1,14 +1,12 @@ import React from "react"; -import Editor from "react-simple-code-editor"; import { format } from "sql-formatter"; -// @ts-expect-error - No types available -import { highlight, languages } from "prismjs/components/prism-core"; import { Result } from "./utils"; import ResultTable from "./ResultTable"; import { Question } from "./QuestionSelector"; import { View } from "./ViewsTable"; import { useLanguage } from "./i18n/context"; import { renderRAPreview } from "./ra-engine/RAPreview"; +import SqlEditor from "./SqlEditor"; interface ExportRendererProps { query?: { @@ -25,7 +23,8 @@ interface ExportRendererProps { } const ExportRenderer = React.forwardRef(({ query, view }, ref) => { - const { t } = useLanguage(); + const { t, engine } = useLanguage(); + const formatterLang = engine === "postgresql" ? "postgresql" : "sqlite"; if (!query && !view) { return null; @@ -63,23 +62,23 @@ const ExportRenderer = React.forwardRef(({ dangerouslySetInnerHTML={{ __html: renderRAPreview(query.code, true) }} /> ) : ( - null} - highlight={code => highlight(code, languages.sql)} - padding={10} - tabSize={4} - className="font-mono text-xl w-full bg-slate-200 border-2 max-w-4xl min-h-40 border-none my-2" - /> +
+ +
)} {query &&

{t("exportResultLabel")}

diff --git a/src/SqlEditor.tsx b/src/SqlEditor.tsx new file mode 100644 index 0000000..db5cc4b --- /dev/null +++ b/src/SqlEditor.tsx @@ -0,0 +1,133 @@ +import { useCallback, useImperativeHandle, useMemo, useRef, forwardRef } from "react"; +import CodeMirror, { type ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { sql, PostgreSQL, SQLite } from "@codemirror/lang-sql"; +import { oneDark } from "@codemirror/theme-one-dark"; +import { EditorView, keymap } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; +import { acceptCompletion } from "@codemirror/autocomplete"; +import type { Extension } from "@codemirror/state"; +import type { EngineType } from "./i18n/languages"; +import type { EditorSettings } from "./useEditorSettings"; +import { raStreamLanguage } from "./ra-engine/raLanguage"; + +export interface SqlEditorHandle { + clearHistory(): void; +} + +interface SqlEditorProps { + value: string; + onChange?: (value: string) => void; + schema?: Record; + engine?: EngineType; + mode?: "sql" | "ra"; + isDarkMode?: boolean; + readOnly?: boolean; + disabled?: boolean; + className?: string; + id?: string; + editorSettings?: EditorSettings; +} + +const baseStyle = EditorView.theme({ + "&.cm-editor": { + fontSize: "1rem", + backgroundColor: "transparent", + }, + ".cm-content": { + fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", + padding: "10px", + }, + ".cm-gutters": { + backgroundColor: "transparent", + border: "none", + }, + ".cm-activeLine": { + backgroundColor: "transparent", + }, + "&.cm-editor.cm-focused": { + outline: "none", + }, +}); + +const SqlEditor = forwardRef(({ + value, + onChange, + schema, + engine = "sqlite", + mode = "sql", + isDarkMode = false, + readOnly = false, + disabled = false, + className = "", + id, + editorSettings, +}, ref) => { + const cmRef = useRef(null); + + const autocomplete = engine === "postgresql" && (editorSettings?.autocomplete ?? true); + const lineNumbers = editorSettings?.lineNumbers ?? false; + const highlightActiveLine = editorSettings?.highlightActiveLine ?? true; + const tabSize = editorSettings?.tabSize ?? 2; + + useImperativeHandle(ref, () => ({ + clearHistory() { + // CodeMirror manages history per-state; question switching replaces value + // which naturally resets the undo stack + }, + })); + + const handleChange = useCallback((val: string) => { + if (onChange) onChange(val); + }, [onChange]); + + const extensions = useMemo((): Extension[] => { + const exts: Extension[] = [baseStyle, EditorState.tabSize.of(tabSize)]; + + if (mode === "ra") { + exts.push(raStreamLanguage); + } else { + const dialect = engine === "postgresql" ? PostgreSQL : SQLite; + exts.push(sql({ + dialect, + schema: autocomplete ? schema : undefined, + upperCaseKeywords: true, + })); + if (autocomplete) { + exts.push(keymap.of([{ key: "Tab", run: acceptCompletion }])); + } + } + + if (isDarkMode) { + exts.push(oneDark); + } + + return exts; + }, [engine, mode, schema, isDarkMode, autocomplete, tabSize]); + + const isInteractive = !readOnly && !disabled; + + return ( + + ); +}); + +SqlEditor.displayName = "SqlEditor"; +export default SqlEditor; diff --git a/src/ViewsTable.tsx b/src/ViewsTable.tsx index adf055e..fc36e76 100644 --- a/src/ViewsTable.tsx +++ b/src/ViewsTable.tsx @@ -1,5 +1,3 @@ -import "prismjs/components/prism-sql"; -import "prismjs/themes/prism.css"; import React from "react"; import { Upload, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; diff --git a/src/database/pgliteEngine.ts b/src/database/pgliteEngine.ts new file mode 100644 index 0000000..132e23e --- /dev/null +++ b/src/database/pgliteEngine.ts @@ -0,0 +1,101 @@ +import type { PGlite } from "@electric-sql/pglite"; +import type { DatabaseEngine, QueryResult } from "./types"; + +export class PgliteEngine implements DatabaseEngine { + readonly engine = "postgresql" as const; + private db: PGlite; + + constructor(db: PGlite) { + this.db = db; + } + + async exec(sql: string): Promise { + const results = await this.db.exec(sql); + return results.map(r => ({ + columns: r.fields.map(f => f.name), + values: r.rows.map(row => { + // PGLite returns rows as objects, convert to arrays matching column order + const fields = r.fields.map(f => f.name); + return fields.map(name => { + const val = (row as Record)[name]; + // Normalize PG types to match the app's expected format + if (val instanceof Date) return val.toISOString().split("T")[0]; // DATE → "YYYY-MM-DD" + if (typeof val === "boolean") return val ? 1 : 0; // BOOLEAN → 0/1 for result comparison + return val; + }); + }), + })); + } + + async getViews(): Promise<{ name: string; query: string }[]> { + const res = await this.db.query<{ viewname: string; definition: string }>( + "SELECT viewname, definition FROM pg_views WHERE schemaname = 'public'" + ); + return res.rows.map(r => ({ + name: r.viewname, + query: `CREATE VIEW ${r.viewname} AS ${r.definition}`, + })); + } + + async getSchema(): Promise> { + const res = await this.db.query<{ table_name: string; column_name: string }>( + "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position" + ); + const schema: Record = {}; + for (const row of res.rows) { + if (!schema[row.table_name]) schema[row.table_name] = []; + schema[row.table_name].push(row.column_name); + } + return schema; + } + + async validateStatements(sql: string): Promise { + // Count semicolons outside of string literals + const stripped = sql.replace(/'[^']*'/g, ""); // remove string literals + const statements = stripped.split(";").filter(s => s.trim().length > 0); + if (statements.length > 1) { + return "multiple_statements"; + } + // Use EXPLAIN to validate syntax without executing + if (statements.length === 1) { + try { + await this.db.query(`EXPLAIN ${sql}`); + } catch (e) { + return (e as Error).message; + } + } + return null; + } + + async getColumnNames(sql: string): Promise { + // For bare table names, use information_schema + if (/^\w+$/.test(sql.trim())) { + const tableName = sql.trim(); + const res = await this.db.query<{ column_name: string }>( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position", + [tableName] + ); + if (res.rows.length > 0) { + return res.rows.map(r => r.column_name); + } + // PostgreSQL lowercases unquoted identifiers, try lowercase + const resLower = await this.db.query<{ column_name: string }>( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position", + [tableName.toLowerCase()] + ); + if (resLower.rows.length > 0) { + return resLower.rows.map(r => r.column_name); + } + throw new Error(`Table '${tableName}' does not exist`); + } + + // For complex expressions, probe with LIMIT 0 + const probeSQL = `SELECT * FROM (${sql}) AS _probe LIMIT 0`; + const res = await this.db.query(probeSQL); + return res.fields.map(f => f.name); + } + + async close(): Promise { + await this.db.close(); + } +} diff --git a/src/database/sqliteEngine.ts b/src/database/sqliteEngine.ts new file mode 100644 index 0000000..00fc26d --- /dev/null +++ b/src/database/sqliteEngine.ts @@ -0,0 +1,79 @@ +import type { Database } from "sql.js"; +import type { DatabaseEngine, QueryResult } from "./types"; + +export class SqliteEngine implements DatabaseEngine { + readonly engine = "sqlite" as const; + private db: Database; + + constructor(db: Database) { + this.db = db; + } + + async exec(sql: string): Promise { + const results = this.db.exec(sql); + return results.map(r => ({ + columns: r.columns, + values: r.values as unknown[][], + })); + } + + async getViews(): Promise<{ name: string; query: string }[]> { + const res = this.db.exec("SELECT name, sql FROM sqlite_master WHERE type='view'"); + if (res.length === 0) return []; + return (res[0].values as string[][]).map(([name, query]) => ({ name, query })); + } + + async getSchema(): Promise> { + const schema: Record = {}; + const tables = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"); + if (tables.length === 0) return schema; + for (const row of tables[0].values) { + const tableName = row[0] as string; + const cols = this.db.exec(`PRAGMA table_info(${tableName})`); + if (cols.length > 0) { + schema[tableName] = cols[0].values.map(r => r[1] as string); + } + } + return schema; + } + + async validateStatements(sql: string): Promise { + try { + let count = 0; + for (const stmt of this.db.iterateStatements(sql)) { + count++; + stmt.free(); + if (count > 1) { + return "multiple_statements"; + } + } + return null; + } catch (e) { + return (e as Error).message; + } + } + + async getColumnNames(sql: string): Promise { + // For bare table names, use PRAGMA table_info + if (/^\w+$/.test(sql.trim())) { + const tableName = sql.trim(); + const res = this.db.exec(`PRAGMA table_info(${tableName})`); + if (res.length > 0 && res[0].values.length > 0) { + return res[0].values.map(row => String(row[1])); + } + throw new Error(`Table '${tableName}' does not exist`); + } + + // For complex expressions, use prepare to get column names + const probeSQL = `SELECT * FROM (${sql}) LIMIT 0`; + const stmt = this.db.prepare(probeSQL); + stmt.step(); + const cols = stmt.getColumnNames(); + stmt.free(); + return cols; + } + + async close(): Promise { + this.db.close(); + } +} diff --git a/src/database/types.ts b/src/database/types.ts new file mode 100644 index 0000000..1c8a244 --- /dev/null +++ b/src/database/types.ts @@ -0,0 +1,23 @@ +export type EngineType = "sqlite" | "postgresql"; + +export interface QueryResult { + columns: string[]; + values: unknown[][]; +} + +export interface DatabaseEngine { + /** Execute SQL and return results (one per statement) */ + exec(sql: string): Promise; + /** List all user-created views */ + getViews(): Promise<{ name: string; query: string }[]>; + /** Get table→column[] map for autocomplete */ + getSchema(): Promise>; + /** Validate that input contains at most one statement. Returns error message if invalid. */ + validateStatements(sql: string): Promise; + /** Get column names produced by a SQL expression (for RA engine) */ + getColumnNames(sql: string): Promise; + /** The engine type */ + readonly engine: EngineType; + /** Close the database connection */ + close(): Promise; +} diff --git a/src/i18n/context.tsx b/src/i18n/context.tsx index a110792..b40e242 100644 --- a/src/i18n/context.tsx +++ b/src/i18n/context.tsx @@ -1,5 +1,5 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE } from "./languages"; +import { AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE, type EngineType } from "./languages"; import { uiStrings } from "./ui-strings"; export interface QuestionCategory { @@ -20,6 +20,9 @@ export interface QuestionCategory { }>; } +/** Database data: ArrayBuffer for SQLite, string (SQL dump) for PostgreSQL */ +export type DbData = ArrayBuffer | string; + interface LanguageContextValue { /** Current language code */ lang: string; @@ -29,30 +32,46 @@ interface LanguageContextValue { t: (key: string, params?: Record) => string; /** The question pool for the current language */ questions: QuestionCategory[]; - /** The database ArrayBuffer for the current language */ - dbArrayBuffer: ArrayBuffer | null; + /** The database data for the current language + engine */ + dbData: DbData | null; /** Default query for the current language */ defaultQuery: string; + /** Current database engine */ + engine: EngineType; + /** Switch engine */ + setEngine: (engine: EngineType) => void; } const LanguageContext = createContext(null); +const DEFAULT_ENGINE: EngineType = "sqlite"; + function getInitialLanguage(): string { - // 1. URL parameter const urlParams = new URLSearchParams(window.location.search); const urlLang = urlParams.get("lang"); if (urlLang && AVAILABLE_LANGUAGES.some(l => l.code === urlLang)) { return urlLang; } - // 2. localStorage const stored = localStorage.getItem("language"); if (stored && AVAILABLE_LANGUAGES.some(l => l.code === stored)) { return stored; } - // 3. Default return DEFAULT_LANGUAGE; } +function getInitialEngine(): EngineType { + const urlParams = new URLSearchParams(window.location.search); + const urlEngine = urlParams.get("engine"); + if (urlEngine === "postgresql" || urlEngine === "sqlite") { + return urlEngine; + } + const stored = localStorage.getItem("engine"); + if (stored === "postgresql" || stored === "sqlite") { + return stored; + } + return DEFAULT_ENGINE; +} + function updateUrlParam(key: string, value: string | null) { const url = new URL(window.location.href); if (value === null) { @@ -71,21 +90,34 @@ export function setUrlParam(key: string, value: string | null) { updateUrlParam(key, value); } +/** + * Returns the data directory path for a language + engine combination. + * SQLite data lives under /languages/{code}/ + * PostgreSQL data lives under /languages/{code}-pg/ + */ +function dataPath(langCode: string, engine: EngineType): string { + return engine === "postgresql" ? `/languages/${langCode}-pg` : `/languages/${langCode}`; +} + export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [lang, setLangState] = useState(getInitialLanguage); + const [engine, setEngineState] = useState(getInitialEngine); const [questions, setQuestions] = useState([]); - const [dbArrayBuffer, setDbArrayBuffer] = useState(null); + const [dbData, setDbData] = useState(null); const [defaultQuery, setDefaultQuery] = useState("SELECT * FROM Student;"); - const loadLanguageData = useCallback(async (langCode: string) => { + const loadLanguageData = useCallback(async (langCode: string, engineType: EngineType) => { + const base = dataPath(langCode, engineType); + const dataFile = engineType === "postgresql" ? "data.sql" : "data.sqlite3"; + try { const [qpResponse, dbResponse] = await Promise.all([ - fetch(`/languages/${langCode}/questionpool.json`), - fetch(`/languages/${langCode}/data.sqlite3`), + fetch(`${base}/questionpool.json`), + fetch(`${base}/${dataFile}`), ]); if (!qpResponse.ok || !dbResponse.ok) { - console.error(`Failed to load language data for ${langCode}`); + console.error(`Failed to load language data for ${langCode} (${engineType})`); return; } @@ -93,18 +125,23 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil setQuestions(qpData.questions); setDefaultQuery(qpData.defaultQuery || "SELECT * FROM Student;"); - const dbData = await dbResponse.arrayBuffer(); - setDbArrayBuffer(dbData); + if (engineType === "postgresql") { + const sqlText = await dbResponse.text(); + setDbData(sqlText); + } else { + const arrayBuf = await dbResponse.arrayBuffer(); + setDbData(arrayBuf); + } } catch (e) { console.error("Failed to load language data:", e); } }, []); useEffect(() => { - loadLanguageData(lang); - // Always reflect language in URL (even default) + loadLanguageData(lang, engine); updateUrlParam("lang", lang); - }, [lang, loadLanguageData]); + updateUrlParam("engine", engine === DEFAULT_ENGINE ? null : engine); + }, [lang, engine, loadLanguageData]); const setLang = useCallback((newLang: string) => { if (!AVAILABLE_LANGUAGES.some(l => l.code === newLang)) return; @@ -113,8 +150,15 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil setLangState(newLang); }, []); + const setEngine = useCallback((newEngine: EngineType) => { + localStorage.setItem("engine", newEngine); + updateUrlParam("engine", newEngine === DEFAULT_ENGINE ? null : newEngine); + setEngineState(newEngine); + }, []); + const t = useCallback((key: string, params?: Record): string => { - const strings = uiStrings[lang] || uiStrings[DEFAULT_LANGUAGE] || {}; + const baseLang = lang.replace(/-pg$/, ""); + const strings = uiStrings[lang] || uiStrings[baseLang] || uiStrings[DEFAULT_LANGUAGE] || {}; let value = strings[key] || key; if (params) { for (const [k, v] of Object.entries(params)) { @@ -125,7 +169,7 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil }, [lang]); return ( - + {children} ); diff --git a/src/i18n/languages.ts b/src/i18n/languages.ts index 70f3bd7..2ac64ce 100644 --- a/src/i18n/languages.ts +++ b/src/i18n/languages.ts @@ -2,6 +2,8 @@ * Registry of available languages. * Add new entries here when adding a new language pack. */ +export type EngineType = "sqlite" | "postgresql"; + export interface LanguageInfo { code: string; displayName: string; @@ -13,4 +15,9 @@ export const AVAILABLE_LANGUAGES: LanguageInfo[] = [ { code: "de", displayName: "Deutsch" }, ]; +export const AVAILABLE_ENGINES: { type: EngineType; displayName: string }[] = [ + { type: "sqlite", displayName: "SQLite" }, + { type: "postgresql", displayName: "PostgreSQL" }, +]; + export const DEFAULT_LANGUAGE = "sv"; diff --git a/src/ra-engine/raLanguage.ts b/src/ra-engine/raLanguage.ts new file mode 100644 index 0000000..001ca98 --- /dev/null +++ b/src/ra-engine/raLanguage.ts @@ -0,0 +1,146 @@ +/** + * CodeMirror StreamLanguage definition for relational algebra syntax. + * Maps tokens to CodeMirror tag names for theme-based highlighting. + */ +import { StreamLanguage, type StreamParser } from "@codemirror/language"; +import { + UNARY_SYMBOLS, BINARY_SYMBOLS, UNARY_KEYWORDS, BINARY_KEYWORDS, + LOGIC_KEYWORDS, IDENT_START, IDENT_CHAR, +} from "./raShared"; + +interface RAState { + inComment: boolean; +} + +const raParser: StreamParser = { + startState(): RAState { + return { inComment: false }; + }, + + token(stream, _state): string | null { + // Comments: -- until end of line + if (stream.match("--")) { + stream.skipToEnd(); + return "comment"; + } + + // Skip whitespace + if (stream.eatSpace()) return null; + + const ch = stream.peek()!; + + // Unary operator symbols (σ, π, ρ, γ, τ, δ) + if (UNARY_SYMBOLS.has(ch)) { + stream.next(); + return "keyword"; + } + + // Binary operator symbols (×, ⋈, ∪, ∩, etc.) + if (BINARY_SYMBOLS.has(ch)) { + stream.next(); + return "keyword"; + } + + // → rename arrow + if (ch === "→") { + stream.next(); + return "keyword"; + } + + // |X| or |><| natural join + if (ch === "|") { + if (stream.match(/^\|[Xx]\|/) || stream.match(/^\|><\|/)) { + return "keyword"; + } + stream.next(); + return null; + } + + // <- assignment + if (ch === "<" && stream.match("<-")) { + return "keyword"; + } + + // -> rename arrow + if (ch === "-" && stream.match("->")) { + return "keyword"; + } + + // String literals + if (ch === "'") { + stream.next(); + while (!stream.eol()) { + if (stream.next() === "'") break; + } + return "string"; + } + + // Numbers + if (/\d/.test(ch)) { + stream.match(/^\d[\d.]*/); + return "number"; + } + + // Brackets — styled as punctuation + if ("[]{}".includes(ch)) { + stream.next(); + return "bracket"; + } + + // Underscore before bracket (LaTeX-style subscript prefix) + if (ch === "_") { + const pos = stream.pos; + stream.next(); + // Peek ahead past spaces + const rest = stream.string.slice(stream.pos).trimStart(); + if (rest.length > 0 && (rest[0] === "{" || rest[0] === "[")) { + return "bracket"; + } + // Not a subscript prefix — fall through, reset nothing (already consumed) + // Check if it's part of an identifier + if (stream.pos < stream.string.length && IDENT_CHAR.test(stream.string[stream.pos])) { + stream.backUp(1); + stream.pos = pos; + // Let the identifier handler below pick it up + } else { + return null; + } + } + + // Identifiers and keywords + if (IDENT_START.test(ch)) { + const wordStart = stream.pos; + while (!stream.eol() && IDENT_CHAR.test(stream.string[stream.pos])) { + stream.pos++; + } + const word = stream.string.slice(wordStart, stream.pos); + if (word.length === 0) { + stream.next(); + return null; + } + const lower = word.toLowerCase(); + if (UNARY_KEYWORDS.has(lower) || BINARY_KEYWORDS.has(lower)) { + return "keyword"; + } + if (LOGIC_KEYWORDS.has(lower)) { + return "operator"; + } + return "variableName"; + } + + // Comparison operators + if ("<>!=".includes(ch)) { + stream.next(); + if (!stream.eol() && "=>".includes(stream.peek()!)) { + stream.next(); + } + return "operator"; + } + + // Everything else + stream.next(); + return null; + }, +}; + +export const raStreamLanguage = StreamLanguage.define(raParser); diff --git a/src/ra-engine/relationalAlgebra.test.ts b/src/ra-engine/relationalAlgebra.test.ts index 70bfa1f..3853b83 100644 --- a/src/ra-engine/relationalAlgebra.test.ts +++ b/src/ra-engine/relationalAlgebra.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import initSqlJs, { type Database } from "sql.js"; import { raToSQL, RAError } from "./relationalAlgebra"; +import { SqliteEngine } from "../database/sqliteEngine"; // ─── Helper ───────────────────────────────────────────────────────────────── @@ -12,85 +13,85 @@ function norm(s: string): string { // ─── Basic table references ───────────────────────────────────────────────── describe("table references", () => { - it("should return a bare table name", () => { - expect(raToSQL("Person")).toBe("Person"); + it("should return a bare table name", async () => { + expect(await raToSQL("Person")).toBe("Person"); }); - it("should handle parenthesised table name", () => { - expect(raToSQL("(Person)")).toBe("Person"); + it("should handle parenthesised table name", async () => { + expect(await raToSQL("(Person)")).toBe("Person"); }); - it("should handle trailing semicolon", () => { - expect(raToSQL("Person;")).toBe("Person"); + it("should handle trailing semicolon", async () => { + expect(await raToSQL("Person;")).toBe("Person"); }); }); // ─── Selection (σ) ────────────────────────────────────────────────────────── describe("selection (σ)", () => { - it("should transpile σ with Unicode symbol", () => { - expect(norm(raToSQL("σ[age > 20](Person)"))).toBe( + it("should transpile σ with Unicode symbol", async () => { + expect(norm(await raToSQL("σ[age > 20](Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should transpile sigma keyword", () => { - expect(norm(raToSQL("sigma[age > 20](Person)"))).toBe( + it("should transpile sigma keyword", async () => { + expect(norm(await raToSQL("sigma[age > 20](Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should transpile select keyword", () => { - expect(norm(raToSQL("select[age > 20](Person)"))).toBe( + it("should transpile select keyword", async () => { + expect(norm(await raToSQL("select[age > 20](Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should handle string comparison", () => { - expect(norm(raToSQL("σ[name = 'Alice'](Person)"))).toBe( + it("should handle string comparison", async () => { + expect(norm(await raToSQL("σ[name = 'Alice'](Person)"))).toBe( norm("SELECT * FROM (Person) WHERE name = 'Alice'") ); }); - it("should handle compound conditions with AND/OR", () => { - const sql = raToSQL("σ[age > 20 and name = 'Alice'](Person)"); + it("should handle compound conditions with AND/OR", async () => { + const sql = await raToSQL("σ[age > 20 and name = 'Alice'](Person)"); expect(norm(sql)).toBe( norm("SELECT * FROM (Person) WHERE (age > 20 AND name = 'Alice')") ); }); - it("should handle NOT condition", () => { - const sql = raToSQL("σ[not age > 20](Person)"); + it("should handle NOT condition", async () => { + const sql = await raToSQL("σ[not age > 20](Person)"); expect(norm(sql)).toBe( norm("SELECT * FROM (Person) WHERE NOT (age > 20)") ); }); - it("should handle table.column references in conditions", () => { - const sql = raToSQL("σ[Person.age > 20](Person)"); + it("should handle table.column references in conditions", async () => { + const sql = await raToSQL("σ[Person.age > 20](Person)"); expect(norm(sql)).toBe( norm("SELECT * FROM (Person) WHERE Person.age > 20") ); }); - it("should handle nested OR and AND", () => { - const sql = raToSQL("σ[age > 20 or (name = 'Alice' and city = 'Stockholm')](Person)"); + it("should handle nested OR and AND", async () => { + const sql = await raToSQL("σ[age > 20 or (name = 'Alice' and city = 'Stockholm')](Person)"); expect(norm(sql)).toContain("OR"); expect(norm(sql)).toContain("AND"); }); - it("should handle all comparison operators", () => { - expect(norm(raToSQL("σ[a = 1](T)"))).toContain("= 1"); - expect(norm(raToSQL("σ[a <> 1](T)"))).toContain("<> 1"); - expect(norm(raToSQL("σ[a != 1](T)"))).toContain("<> 1"); - expect(norm(raToSQL("σ[a < 1](T)"))).toContain("< 1"); - expect(norm(raToSQL("σ[a > 1](T)"))).toContain("> 1"); - expect(norm(raToSQL("σ[a <= 1](T)"))).toContain("<= 1"); - expect(norm(raToSQL("σ[a >= 1](T)"))).toContain(">= 1"); + it("should handle all comparison operators", async () => { + expect(norm(await raToSQL("σ[a = 1](T)"))).toContain("= 1"); + expect(norm(await raToSQL("σ[a <> 1](T)"))).toContain("<> 1"); + expect(norm(await raToSQL("σ[a != 1](T)"))).toContain("<> 1"); + expect(norm(await raToSQL("σ[a < 1](T)"))).toContain("< 1"); + expect(norm(await raToSQL("σ[a > 1](T)"))).toContain("> 1"); + expect(norm(await raToSQL("σ[a <= 1](T)"))).toContain("<= 1"); + expect(norm(await raToSQL("σ[a >= 1](T)"))).toContain(">= 1"); }); - it("should handle function calls in conditions", () => { - const sql = raToSQL("σ[YEAR(startDate) = 2024](CourseInstance)"); + it("should handle function calls in conditions", async () => { + const sql = await raToSQL("σ[YEAR(startDate) = 2024](CourseInstance)"); expect(norm(sql)).toBe( norm("SELECT * FROM (CourseInstance) WHERE YEAR(startDate) = 2024") ); @@ -100,26 +101,26 @@ describe("selection (σ)", () => { // ─── Projection (π) ───────────────────────────────────────────────────────── describe("projection (π)", () => { - it("should transpile π with Unicode", () => { - expect(norm(raToSQL("π[name, age](Person)"))).toBe( + it("should transpile π with Unicode", async () => { + expect(norm(await raToSQL("π[name, age](Person)"))).toBe( norm("SELECT name, age FROM (Person)") ); }); - it("should transpile pi keyword", () => { - expect(norm(raToSQL("pi[name](Person)"))).toBe( + it("should transpile pi keyword", async () => { + expect(norm(await raToSQL("pi[name](Person)"))).toBe( norm("SELECT name FROM (Person)") ); }); - it("should transpile project keyword", () => { - expect(norm(raToSQL("project[name, city](Person)"))).toBe( + it("should transpile project keyword", async () => { + expect(norm(await raToSQL("project[name, city](Person)"))).toBe( norm("SELECT name, city FROM (Person)") ); }); - it("should handle table.column references", () => { - expect(norm(raToSQL("π[Person.name](Person)"))).toBe( + it("should handle table.column references", async () => { + expect(norm(await raToSQL("π[Person.name](Person)"))).toBe( norm("SELECT Person.name FROM (Person)") ); }); @@ -128,20 +129,20 @@ describe("projection (π)", () => { // ─── Rename (ρ) ───────────────────────────────────────────────────────────── describe("rename (ρ)", () => { - it("should transpile ρ with Unicode arrow", () => { - expect(norm(raToSQL("ρ[name→fullName](Person)"))).toBe( + it("should transpile ρ with Unicode arrow", async () => { + expect(norm(await raToSQL("ρ[name→fullName](Person)"))).toBe( norm("SELECT name AS fullName FROM (Person)") ); }); - it("should transpile rho with ASCII arrow", () => { - expect(norm(raToSQL("rho[name->fullName](Person)"))).toBe( + it("should transpile rho with ASCII arrow", async () => { + expect(norm(await raToSQL("rho[name->fullName](Person)"))).toBe( norm("SELECT name AS fullName FROM (Person)") ); }); - it("should handle multiple rename mappings", () => { - const sql = raToSQL("ρ[name→fullName, age→years](Person)"); + it("should handle multiple rename mappings", async () => { + const sql = await raToSQL("ρ[name→fullName, age→years](Person)"); expect(norm(sql)).toBe( norm("SELECT name AS fullName, age AS years FROM (Person)") ); @@ -151,13 +152,13 @@ describe("rename (ρ)", () => { // ─── Cross product (×) ───────────────────────────────────────────────────── describe("cross product (×)", () => { - it("should transpile × operator", () => { - const sql = raToSQL("Person × Course"); + it("should transpile × operator", async () => { + const sql = await raToSQL("Person × Course"); expect(norm(sql)).toContain("CROSS JOIN"); }); - it("should transpile cross keyword", () => { - const sql = raToSQL("Person cross Course"); + it("should transpile cross keyword", async () => { + const sql = await raToSQL("Person cross Course"); expect(norm(sql)).toContain("CROSS JOIN"); }); }); @@ -165,13 +166,13 @@ describe("cross product (×)", () => { // ─── Natural join (⋈) ────────────────────────────────────────────────────── describe("natural join (⋈)", () => { - it("should transpile ⋈ operator", () => { - const sql = raToSQL("Person ⋈ Student"); + it("should transpile ⋈ operator", async () => { + const sql = await raToSQL("Person ⋈ Student"); expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should transpile natjoin keyword", () => { - const sql = raToSQL("Person natjoin Student"); + it("should transpile natjoin keyword", async () => { + const sql = await raToSQL("Person natjoin Student"); expect(norm(sql)).toContain("NATURAL JOIN"); }); @@ -181,8 +182,8 @@ describe("natural join (⋈)", () => { db.run("CREATE TABLE TableA (id INTEGER, name TEXT)"); db.run("CREATE TABLE TableB (code TEXT, description TEXT)"); - expect(() => raToSQL("TableA ⋈ TableB", db)).toThrow(RAError); - expect(() => raToSQL("TableA ⋈ TableB", db)).toThrow(/no common columns/i); + await expect(raToSQL("TableA ⋈ TableB", new SqliteEngine(db))).rejects.toThrow(RAError); + await expect(raToSQL("TableA ⋈ TableB", new SqliteEngine(db))).rejects.toThrow(/no common columns/i); db.close(); }); @@ -192,7 +193,7 @@ describe("natural join (⋈)", () => { db.run("CREATE TABLE Person (id INTEGER, name TEXT, city TEXT)"); db.run("CREATE TABLE Student (id INTEGER, hasDisability INTEGER)"); - expect(() => raToSQL("Person ⋈ Student", db)).not.toThrow(); + await expect(raToSQL("Person ⋈ Student", new SqliteEngine(db))).resolves.not.toThrow(); db.close(); }); @@ -201,8 +202,8 @@ describe("natural join (⋈)", () => { const db = new SQL.Database(); db.run("CREATE TABLE Person (id INTEGER, name TEXT)"); - expect(() => raToSQL("Person ⋈ Room", db)).toThrow(RAError); - expect(() => raToSQL("Person ⋈ Room", db)).toThrow(/Room/); + await expect(raToSQL("Person ⋈ Room", new SqliteEngine(db))).rejects.toThrow(RAError); + await expect(raToSQL("Person ⋈ Room", new SqliteEngine(db))).rejects.toThrow(/Room/); db.close(); }); @@ -212,22 +213,22 @@ describe("natural join (⋈)", () => { db.run("CREATE TABLE Person (person_id INTEGER, name TEXT, city TEXT)"); db.run("CREATE TABLE Room (room_id INTEGER, building TEXT, capacity INTEGER)"); - expect(() => raToSQL("σ[city = 'York'](Person) ⋈ Room", db)).toThrow(RAError); - expect(() => raToSQL("σ[city = 'York'](Person) ⋈ Room", db)).toThrow(/no common columns/i); + await expect(raToSQL("σ[city = 'York'](Person) ⋈ Room", new SqliteEngine(db))).rejects.toThrow(RAError); + await expect(raToSQL("σ[city = 'York'](Person) ⋈ Room", new SqliteEngine(db))).rejects.toThrow(/no common columns/i); db.close(); }); - it("should not error when no database is provided (parse-only mode)", () => { - expect(() => raToSQL("TableA ⋈ TableB")).not.toThrow(); + it("should not error when no database is provided (parse-only mode)", async () => { + await expect(raToSQL("TableA ⋈ TableB")).resolves.not.toThrow(); }); - it("should support |X| as natural join", () => { - const sql = raToSQL("Person |X| Student"); + it("should support |X| as natural join", async () => { + const sql = await raToSQL("Person |X| Student"); expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should support |><| as natural join", () => { - const sql = raToSQL("Person |><| Student"); + it("should support |><| as natural join", async () => { + const sql = await raToSQL("Person |><| Student"); expect(norm(sql)).toContain("NATURAL JOIN"); }); }); @@ -235,15 +236,15 @@ describe("natural join (⋈)", () => { // ─── Theta join (⋈[cond]) ────────────────────────────────────────────────── describe("theta join (⋈[cond])", () => { - it("should transpile ⋈ with condition", () => { - const sql = raToSQL("Person ⋈[Person.id = Student.id] Student"); + it("should transpile ⋈ with condition", async () => { + const sql = await raToSQL("Person ⋈[Person.id = Student.id] Student"); expect(norm(sql)).toContain("JOIN"); expect(norm(sql)).toContain("ON Person.id = Student.id"); expect(norm(sql)).not.toContain("NATURAL"); }); - it("should transpile join keyword with condition", () => { - const sql = raToSQL("Person join[Person.id = Student.id] Student"); + it("should transpile join keyword with condition", async () => { + const sql = await raToSQL("Person join[Person.id = Student.id] Student"); expect(norm(sql)).toContain("JOIN"); expect(norm(sql)).toContain("ON Person.id = Student.id"); }); @@ -252,50 +253,50 @@ describe("theta join (⋈[cond])", () => { // ─── Set operations ───────────────────────────────────────────────────────── describe("set operations", () => { - it("should transpile union with ∪", () => { - const sql = raToSQL("π[name](Person) ∪ π[name](Teacher)"); + it("should transpile union with ∪", async () => { + const sql = await raToSQL("π[name](Person) ∪ π[name](Teacher)"); expect(norm(sql)).toContain("UNION"); }); - it("should transpile union keyword", () => { - const sql = raToSQL("π[name](Person) union π[name](Teacher)"); + it("should transpile union keyword", async () => { + const sql = await raToSQL("π[name](Person) union π[name](Teacher)"); expect(norm(sql)).toContain("UNION"); }); - it("should transpile intersect with ∩", () => { - const sql = raToSQL("π[name](Person) ∩ π[name](Teacher)"); + it("should transpile intersect with ∩", async () => { + const sql = await raToSQL("π[name](Person) ∩ π[name](Teacher)"); expect(norm(sql)).toContain("INTERSECT"); }); - it("should transpile difference with −", () => { - const sql = raToSQL("π[name](Person) − π[name](Teacher)"); + it("should transpile difference with −", async () => { + const sql = await raToSQL("π[name](Person) − π[name](Teacher)"); expect(norm(sql)).toContain("EXCEPT"); }); - it("should transpile minus keyword", () => { - const sql = raToSQL("π[name](Person) minus π[name](Teacher)"); + it("should transpile minus keyword", async () => { + const sql = await raToSQL("π[name](Person) minus π[name](Teacher)"); expect(norm(sql)).toContain("EXCEPT"); }); - it("should transpile backslash as set difference", () => { - const sql = raToSQL("A \\ B"); + it("should transpile backslash as set difference", async () => { + const sql = await raToSQL("A \\ B"); expect(norm(sql)).toContain("EXCEPT"); }); - it("should generate valid SQL for bare table set operations", () => { + it("should generate valid SQL for bare table set operations", async () => { // Set ops need SELECT statements on both sides, not bare table names - const unionSql = norm(raToSQL("A union B")); + const unionSql = norm(await raToSQL("A union B")); expect(unionSql).toMatch(/SELECT \* FROM A\s+UNION\s+SELECT \* FROM B/i); - const exceptSql = norm(raToSQL("A minus B")); + const exceptSql = norm(await raToSQL("A minus B")); expect(exceptSql).toMatch(/SELECT \* FROM A\s+EXCEPT\s+SELECT \* FROM B/i); - const intersectSql = norm(raToSQL("A intersect B")); + const intersectSql = norm(await raToSQL("A intersect B")); expect(intersectSql).toMatch(/SELECT \* FROM A\s+INTERSECT\s+SELECT \* FROM B/i); }); - it("should handle set difference with hyphenated expression", () => { - const sql = raToSQL("PI[name, city](Person - Teacher)"); + it("should handle set difference with hyphenated expression", async () => { + const sql = await raToSQL("PI[name, city](Person - Teacher)"); expect(norm(sql)).toContain("EXCEPT"); expect(norm(sql)).toContain("SELECT name, city"); }); @@ -306,14 +307,14 @@ describe("set operations", () => { db.run("CREATE TABLE A (x INTEGER, y TEXT)"); db.run("CREATE TABLE B (x INTEGER, y TEXT, z TEXT)"); - expect(() => raToSQL("A union B", db)).toThrow(RAError); - expect(() => raToSQL("A union B", db)).toThrow(/same number of columns/i); - expect(() => raToSQL("A minus B", db)).toThrow(RAError); - expect(() => raToSQL("A intersect B", db)).toThrow(RAError); + await expect(raToSQL("A union B", new SqliteEngine(db))).rejects.toThrow(RAError); + await expect(raToSQL("A union B", new SqliteEngine(db))).rejects.toThrow(/same number of columns/i); + await expect(raToSQL("A minus B", new SqliteEngine(db))).rejects.toThrow(RAError); + await expect(raToSQL("A intersect B", new SqliteEngine(db))).rejects.toThrow(RAError); // Should NOT throw when column counts match db.run("CREATE TABLE C (a INTEGER, b TEXT)"); - expect(() => raToSQL("A union C", db)).not.toThrow(); + await expect(raToSQL("A union C", new SqliteEngine(db))).resolves.not.toThrow(); db.close(); }); @@ -323,8 +324,8 @@ describe("set operations", () => { db.run("CREATE TABLE R1 (name TEXT, city TEXT)"); db.run("CREATE TABLE R2 (name TEXT, city TEXT, age INTEGER)"); - expect(() => raToSQL("R1 union R2", db)).toThrow(/Left has 2/); - expect(() => raToSQL("R1 union R2", db)).toThrow(/right has 3/); + await expect(raToSQL("R1 union R2", new SqliteEngine(db))).rejects.toThrow(/Left has 2/); + await expect(raToSQL("R1 union R2", new SqliteEngine(db))).rejects.toThrow(/right has 3/); db.close(); }); }); @@ -332,24 +333,24 @@ describe("set operations", () => { // ─── Outer joins ──────────────────────────────────────────────────────────── describe("outer joins", () => { - it("should transpile left outer join", () => { - const sql = raToSQL("Person leftjoin[Person.id = Student.id] Student"); + it("should transpile left outer join", async () => { + const sql = await raToSQL("Person leftjoin[Person.id = Student.id] Student"); expect(norm(sql)).toContain("LEFT JOIN"); expect(norm(sql)).toContain("ON Person.id = Student.id"); }); - it("should transpile right outer join", () => { - const sql = raToSQL("Person rightjoin[Person.id = Student.id] Student"); + it("should transpile right outer join", async () => { + const sql = await raToSQL("Person rightjoin[Person.id = Student.id] Student"); expect(norm(sql)).toContain("RIGHT JOIN"); }); - it("should transpile full outer join", () => { - const sql = raToSQL("Person fulljoin[Person.id = Student.id] Student"); + it("should transpile full outer join", async () => { + const sql = await raToSQL("Person fulljoin[Person.id = Student.id] Student"); expect(norm(sql)).toContain("FULL OUTER JOIN"); }); - it("should transpile left outer join with Unicode ⟕", () => { - const sql = raToSQL("Person ⟕[Person.id = Student.id] Student"); + it("should transpile left outer join with Unicode ⟕", async () => { + const sql = await raToSQL("Person ⟕[Person.id = Student.id] Student"); expect(norm(sql)).toContain("LEFT JOIN"); }); }); @@ -357,28 +358,28 @@ describe("outer joins", () => { // ─── Semi-joins and anti-join ─────────────────────────────────────────────── describe("semi-joins and anti-join", () => { - it("should transpile left semi-join", () => { - const sql = raToSQL("Person leftsemijoin Student"); + it("should transpile left semi-join", async () => { + const sql = await raToSQL("Person leftsemijoin Student"); expect(norm(sql)).toContain("WHERE EXISTS"); }); - it("should transpile left semi-join with ⋉", () => { - const sql = raToSQL("Person ⋉ Student"); + it("should transpile left semi-join with ⋉", async () => { + const sql = await raToSQL("Person ⋉ Student"); expect(norm(sql)).toContain("WHERE EXISTS"); }); - it("should transpile right semi-join", () => { - const sql = raToSQL("Person rightsemijoin Student"); + it("should transpile right semi-join", async () => { + const sql = await raToSQL("Person rightsemijoin Student"); expect(norm(sql)).toContain("WHERE EXISTS"); }); - it("should transpile anti-join", () => { - const sql = raToSQL("Person antijoin Student"); + it("should transpile anti-join", async () => { + const sql = await raToSQL("Person antijoin Student"); expect(norm(sql)).toContain("WHERE NOT EXISTS"); }); - it("should transpile anti-join with ▷", () => { - const sql = raToSQL("Person ▷ Student"); + it("should transpile anti-join with ▷", async () => { + const sql = await raToSQL("Person ▷ Student"); expect(norm(sql)).toContain("WHERE NOT EXISTS"); }); }); @@ -386,13 +387,13 @@ describe("semi-joins and anti-join", () => { // ─── Division (÷) ────────────────────────────────────────────────────────── describe("division (÷)", () => { - it("should transpile ÷ operator", () => { - const sql = raToSQL("A ÷ B"); + it("should transpile ÷ operator", async () => { + const sql = await raToSQL("A ÷ B"); expect(norm(sql)).toContain("NOT EXISTS"); }); - it("should transpile divide keyword", () => { - const sql = raToSQL("A divide B"); + it("should transpile divide keyword", async () => { + const sql = await raToSQL("A divide B"); expect(norm(sql)).toContain("NOT EXISTS"); }); }); @@ -400,20 +401,20 @@ describe("division (÷)", () => { // ─── Distinct (δ) ────────────────────────────────────────────────────────── describe("distinct (δ)", () => { - it("should transpile δ", () => { - expect(norm(raToSQL("δ(Person)"))).toBe( + it("should transpile δ", async () => { + expect(norm(await raToSQL("δ(Person)"))).toBe( norm("SELECT DISTINCT * FROM (Person)") ); }); - it("should transpile delta keyword", () => { - expect(norm(raToSQL("delta(Person)"))).toBe( + it("should transpile delta keyword", async () => { + expect(norm(await raToSQL("delta(Person)"))).toBe( norm("SELECT DISTINCT * FROM (Person)") ); }); - it("should transpile distinct keyword", () => { - expect(norm(raToSQL("distinct(Person)"))).toBe( + it("should transpile distinct keyword", async () => { + expect(norm(await raToSQL("distinct(Person)"))).toBe( norm("SELECT DISTINCT * FROM (Person)") ); }); @@ -422,27 +423,27 @@ describe("distinct (δ)", () => { // ─── Sorting (τ) ──────────────────────────────────────────────────────────── describe("sorting (τ)", () => { - it("should transpile τ with single column", () => { - const sql = raToSQL("τ[name](Person)"); + it("should transpile τ with single column", async () => { + const sql = await raToSQL("τ[name](Person)"); expect(norm(sql)).toBe( norm("SELECT * FROM (Person) ORDER BY name") ); }); - it("should transpile τ with DESC", () => { - const sql = raToSQL("τ[age DESC](Person)"); + it("should transpile τ with DESC", async () => { + const sql = await raToSQL("τ[age DESC](Person)"); expect(norm(sql)).toBe( norm("SELECT * FROM (Person) ORDER BY age DESC") ); }); - it("should transpile sort keyword", () => { - const sql = raToSQL("sort[name](Person)"); + it("should transpile sort keyword", async () => { + const sql = await raToSQL("sort[name](Person)"); expect(norm(sql)).toContain("ORDER BY name"); }); - it("should handle multiple sort columns", () => { - const sql = raToSQL("τ[name, age DESC](Person)"); + it("should handle multiple sort columns", async () => { + const sql = await raToSQL("τ[name, age DESC](Person)"); expect(norm(sql)).toContain("ORDER BY name, age DESC"); }); }); @@ -450,20 +451,20 @@ describe("sorting (τ)", () => { // ─── Grouping/Aggregation (γ) ────────────────────────────────────────────── describe("grouping/aggregation (γ)", () => { - it("should transpile γ with group-by and aggregate", () => { - const sql = raToSQL("γ[city; COUNT(id)](Person)"); + it("should transpile γ with group-by and aggregate", async () => { + const sql = await raToSQL("γ[city; COUNT(id)](Person)"); expect(norm(sql)).toContain("COUNT(id)"); expect(norm(sql)).toContain("GROUP BY city"); }); - it("should handle aggregate with alias", () => { - const sql = raToSQL("γ[city; COUNT(id) AS total](Person)"); + it("should handle aggregate with alias", async () => { + const sql = await raToSQL("γ[city; COUNT(id) AS total](Person)"); expect(norm(sql)).toContain("COUNT(id) AS total"); expect(norm(sql)).toContain("GROUP BY city"); }); - it("should transpile gamma keyword", () => { - const sql = raToSQL("gamma[city; SUM(price) AS totalPrice](Course)"); + it("should transpile gamma keyword", async () => { + const sql = await raToSQL("gamma[city; SUM(price) AS totalPrice](Course)"); expect(norm(sql)).toContain("SUM(price) AS totalPrice"); expect(norm(sql)).toContain("GROUP BY city"); }); @@ -472,33 +473,33 @@ describe("grouping/aggregation (γ)", () => { // ─── Composition / nesting ────────────────────────────────────────────────── describe("composition and nesting", () => { - it("should handle projection of selection", () => { - const sql = raToSQL("π[name](σ[age > 20](Person))"); + it("should handle projection of selection", async () => { + const sql = await raToSQL("π[name](σ[age > 20](Person))"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE age > 20"); }); - it("should handle selection of join", () => { - const sql = raToSQL("σ[age > 20](Person ⋈ Student)"); + it("should handle selection of join", async () => { + const sql = await raToSQL("σ[age > 20](Person ⋈ Student)"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should handle complex nested expression", () => { - const sql = raToSQL("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); + it("should handle complex nested expression", async () => { + const sql = await raToSQL("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE city = 'Stockholm'"); expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should handle parenthesised subexpressions", () => { - const sql = raToSQL("(Person ⋈ Student) ∪ (Person ⋈ Teacher)"); + it("should handle parenthesised subexpressions", async () => { + const sql = await raToSQL("(Person ⋈ Student) ∪ (Person ⋈ Teacher)"); expect(norm(sql)).toContain("UNION"); expect(norm(sql).match(/NATURAL JOIN/g)?.length).toBe(2); }); - it("should handle deeply nested expressions", () => { - const sql = raToSQL("π[name](σ[age > 20](ρ[id→personId](Person)))"); + it("should handle deeply nested expressions", async () => { + const sql = await raToSQL("π[name](σ[age > 20](ρ[id→personId](Person)))"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("id AS personId"); @@ -508,15 +509,15 @@ describe("composition and nesting", () => { // ─── Operator precedence ─────────────────────────────────────────────────── describe("operator precedence", () => { - it("should bind join tighter than union", () => { + it("should bind join tighter than union", async () => { // A ⋈ B ∪ C should be (A ⋈ B) ∪ C, not A ⋈ (B ∪ C) - const sql = raToSQL("A ⋈ B ∪ C"); + const sql = await raToSQL("A ⋈ B ∪ C"); // The NATURAL JOIN should come before UNION in the SQL expect(norm(sql)).toMatch(/NATURAL JOIN.*UNION/); }); - it("should bind intersect tighter than union", () => { - const sql = raToSQL("A ∪ B ∩ C"); + it("should bind intersect tighter than union", async () => { + const sql = await raToSQL("A ∪ B ∩ C"); // B ∩ C should be grouped together expect(norm(sql)).toContain("INTERSECT"); expect(norm(sql)).toContain("UNION"); @@ -526,65 +527,65 @@ describe("operator precedence", () => { // ─── Error handling ───────────────────────────────────────────────────────── describe("error handling", () => { - it("should throw RAError on empty input", () => { - expect(() => raToSQL("")).toThrow(RAError); + it("should throw RAError on empty input", async () => { + await expect(raToSQL("")).rejects.toThrow(RAError); }); - it("should throw RAError on invalid syntax", () => { - expect(() => raToSQL("σ[](Person)")).toThrow(RAError); + it("should throw RAError on invalid syntax", async () => { + await expect(raToSQL("σ[](Person)")).rejects.toThrow(RAError); }); - it("should throw RAError on unclosed bracket", () => { - expect(() => raToSQL("σ[age > 20(Person)")).toThrow(RAError); + it("should throw RAError on unclosed bracket", async () => { + await expect(raToSQL("σ[age > 20(Person)")).rejects.toThrow(RAError); }); - it("should throw RAError on unexpected token after expression", () => { - expect(() => raToSQL("Person Student")).toThrow(RAError); + it("should throw RAError on unexpected token after expression", async () => { + await expect(raToSQL("Person Student")).rejects.toThrow(RAError); }); - it("should throw RAError on unterminated string", () => { - expect(() => raToSQL("σ[name = 'Alice](Person)")).toThrow(RAError); + it("should throw RAError on unterminated string", async () => { + await expect(raToSQL("σ[name = 'Alice](Person)")).rejects.toThrow(RAError); }); - it("should throw RAError on missing condition in selection", () => { - expect(() => raToSQL("σ(Person)")).toThrow(RAError); + it("should throw RAError on missing condition in selection", async () => { + await expect(raToSQL("σ(Person)")).rejects.toThrow(RAError); }); - it("should throw RAError on missing relation for selection", () => { - expect(() => raToSQL("σ[age > 20]")).toThrow(RAError); + it("should throw RAError on missing relation for selection", async () => { + await expect(raToSQL("σ[age > 20]")).rejects.toThrow(RAError); }); }); // ─── Multi-line / assignments (←) ────────────────────────────────────────── describe("multi-line and assignments", () => { - it("should handle simple assignment with ←", () => { - const sql = raToSQL("A ← Person\nA"); + it("should handle simple assignment with ←", async () => { + const sql = await raToSQL("A ← Person\nA"); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); }); - it("should handle simple assignment with <-", () => { - const sql = raToSQL("A <- Person\nA"); + it("should handle simple assignment with <-", async () => { + const sql = await raToSQL("A <- Person\nA"); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); }); - it("should handle assignment with complex expression", () => { - const sql = raToSQL("Students ← σ[age > 20](Person)\nπ[name](Students)"); + it("should handle assignment with complex expression", async () => { + const sql = await raToSQL("Students ← σ[age > 20](Person)\nπ[name](Students)"); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("Students AS"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("SELECT name FROM"); }); - it("should handle multiple assignments", () => { + it("should handle multiple assignments", async () => { const input = [ "A ← σ[city = 'Stockholm'](Person)", "B ← π[name](A)", "B", ].join("\n"); - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); expect(norm(sql)).toContain("B AS"); @@ -592,70 +593,70 @@ describe("multi-line and assignments", () => { expect(norm(sql)).toContain("SELECT name FROM"); }); - it("should handle assignment chaining with joins", () => { + it("should handle assignment chaining with joins", async () => { const input = [ "PS ← Person ⋈ Student", "Result ← π[name](PS)", "Result", ].join("\n"); - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("PS AS"); expect(norm(sql)).toContain("NATURAL JOIN"); expect(norm(sql)).toContain("Result AS"); }); - it("should handle semicolons as statement separators", () => { - const sql = raToSQL("A ← Person; π[name](A)"); + it("should handle semicolons as statement separators", async () => { + const sql = await raToSQL("A ← Person; π[name](A)"); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); expect(norm(sql)).toContain("SELECT name FROM"); }); - it("should handle mixed newlines and semicolons", () => { + it("should handle mixed newlines and semicolons", async () => { const input = "A ← Person;\nB ← Student\nA ⋈ B"; - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); expect(norm(sql)).toContain("B AS"); expect(norm(sql)).toContain("NATURAL JOIN"); }); - it("should still work with single-line expressions (no assignments)", () => { + it("should still work with single-line expressions (no assignments)", async () => { // No regression — single-line without assignment should work as before - expect(raToSQL("Person")).toBe("Person"); - expect(norm(raToSQL("π[name](Person)"))).toBe( + expect(await raToSQL("Person")).toBe("Person"); + expect(norm(await raToSQL("π[name](Person)"))).toBe( norm("SELECT name FROM (Person)") ); }); - it("should handle -- comments in multi-line input", () => { + it("should handle -- comments in multi-line input", async () => { const input = [ "-- First get students", "A ← σ[age > 20](Person)", "-- Then project names", "π[name](A)", ].join("\n"); - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("SELECT name FROM"); }); - it("should handle trailing newlines", () => { - const sql = raToSQL("Person\n\n\n"); + it("should handle trailing newlines", async () => { + const sql = await raToSQL("Person\n\n\n"); expect(sql).toBe("Person"); }); - it("should handle leading newlines", () => { - const sql = raToSQL("\n\n\nPerson"); + it("should handle leading newlines", async () => { + const sql = await raToSQL("\n\n\nPerson"); expect(sql).toBe("Person"); }); - it("should handle the student example: A <- pi[student](sigma[name='peter'](P))", () => { + it("should handle the student example: A <- pi[student](sigma[name='peter'](P))", async () => { const input = "A <- π[student](σ[name = 'Peter'](Participation))\nA"; - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("A AS"); expect(norm(sql)).toContain("SELECT student FROM"); @@ -666,91 +667,91 @@ describe("multi-line and assignments", () => { // ─── Alternative notation styles ──────────────────────────────────────────── describe("curly braces (LaTeX-style)", () => { - it("should support σ_{condition}(R)", () => { - expect(norm(raToSQL("σ_{age > 20}(Person)"))).toBe( + it("should support σ_{condition}(R)", async () => { + expect(norm(await raToSQL("σ_{age > 20}(Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should support π_{cols}(R)", () => { - expect(norm(raToSQL("π_{name, city}(Person)"))).toBe( + it("should support π_{cols}(R)", async () => { + expect(norm(await raToSQL("π_{name, city}(Person)"))).toBe( norm("SELECT name, city FROM (Person)") ); }); - it("should support ρ_{old→new}(R)", () => { - expect(norm(raToSQL("ρ_{name→fullName}(Person)"))).toBe( + it("should support ρ_{old→new}(R)", async () => { + expect(norm(await raToSQL("ρ_{name→fullName}(Person)"))).toBe( norm("SELECT name AS fullName FROM (Person)") ); }); - it("should support curly braces without underscore", () => { - expect(norm(raToSQL("σ{age > 20}(Person)"))).toBe( + it("should support curly braces without underscore", async () => { + expect(norm(await raToSQL("σ{age > 20}(Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should support theta join with curly braces", () => { - const sql = raToSQL("Person ⋈{Person.id = Student.id} Student"); + it("should support theta join with curly braces", async () => { + const sql = await raToSQL("Person ⋈{Person.id = Student.id} Student"); expect(norm(sql)).toContain("JOIN"); expect(norm(sql)).toContain("ON Person.id = Student.id"); }); }); describe("implicit subscripts (no brackets)", () => { - it("should support σ condition (R)", () => { - expect(norm(raToSQL("σ age > 20 (Person)"))).toBe( + it("should support σ condition (R)", async () => { + expect(norm(await raToSQL("σ age > 20 (Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should support π cols (R)", () => { - expect(norm(raToSQL("π name, city (Person)"))).toBe( + it("should support π cols (R)", async () => { + expect(norm(await raToSQL("π name, city (Person)"))).toBe( norm("SELECT name, city FROM (Person)") ); }); - it("should support sigma condition (R) with keyword", () => { - expect(norm(raToSQL("sigma age > 20 (Person)"))).toBe( + it("should support sigma condition (R) with keyword", async () => { + expect(norm(await raToSQL("sigma age > 20 (Person)"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should support pi cols (R) with keyword", () => { - expect(norm(raToSQL("pi name (Person)"))).toBe( + it("should support pi cols (R) with keyword", async () => { + expect(norm(await raToSQL("pi name (Person)"))).toBe( norm("SELECT name FROM (Person)") ); }); - it("should support ρ old→new (R) implicit", () => { - expect(norm(raToSQL("ρ name→fullName (Person)"))).toBe( + it("should support ρ old→new (R) implicit", async () => { + expect(norm(await raToSQL("ρ name→fullName (Person)"))).toBe( norm("SELECT name AS fullName FROM (Person)") ); }); - it("should support τ col (R) implicit", () => { - expect(norm(raToSQL("τ name (Person)"))).toContain("ORDER BY name"); + it("should support τ col (R) implicit", async () => { + expect(norm(await raToSQL("τ name (Person)"))).toContain("ORDER BY name"); }); - it("should support nested implicit subscripts", () => { - const sql = raToSQL("π name (σ age > 20 (Person))"); + it("should support nested implicit subscripts", async () => { + const sql = await raToSQL("π name (σ age > 20 (Person))"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE age > 20"); }); - it("should support compound implicit condition with AND", () => { - const sql = raToSQL("σ age > 20 and city = 'Stockholm' (Person)"); + it("should support compound implicit condition with AND", async () => { + const sql = await raToSQL("σ age > 20 and city = 'Stockholm' (Person)"); expect(norm(sql)).toContain("WHERE"); expect(norm(sql)).toContain("AND"); expect(norm(sql)).toContain("age > 20"); }); - it("should work with multi-line and implicit subscripts", () => { + it("should work with multi-line and implicit subscripts", async () => { const input = [ "A <- σ age > 20 (Person)", "π name (A)", ].join("\n"); - const sql = raToSQL(input); + const sql = await raToSQL(input); expect(norm(sql)).toContain("WITH"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("SELECT name FROM"); @@ -758,44 +759,44 @@ describe("implicit subscripts (no brackets)", () => { }); describe("parenthesis-free syntax", () => { - it("should allow σ[cond] Table without parens", () => { - expect(norm(raToSQL("σ[age > 20] Person"))).toBe( + it("should allow σ[cond] Table without parens", async () => { + expect(norm(await raToSQL("σ[age > 20] Person"))).toBe( norm("SELECT * FROM (Person) WHERE age > 20") ); }); - it("should allow π[cols] Table without parens", () => { - expect(norm(raToSQL("π[name] Person"))).toBe( + it("should allow π[cols] Table without parens", async () => { + expect(norm(await raToSQL("π[name] Person"))).toBe( norm("SELECT name FROM (Person)") ); }); - it("should allow chained unary ops without parens", () => { - const sql = raToSQL("π[name] σ[age > 20] Person"); + it("should allow chained unary ops without parens", async () => { + const sql = await raToSQL("π[name] σ[age > 20] Person"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE age > 20"); }); - it("should handle the user's example without excessive parens", () => { - const sql = raToSQL("π[person_id, name, city] σ[city = 'York' OR city = 'Bristol'] Person"); + it("should handle the user's example without excessive parens", async () => { + const sql = await raToSQL("π[person_id, name, city] σ[city = 'York' OR city = 'Bristol'] Person"); expect(norm(sql)).toContain("SELECT person_id, name, city FROM"); expect(norm(sql)).toContain("WHERE"); expect(norm(sql)).toContain("city = 'York'"); expect(norm(sql)).toContain("city = 'Bristol'"); }); - it("should still work with parens for grouping binary ops", () => { - const sql = raToSQL("π[name] (Person ∪ Student)"); + it("should still work with parens for grouping binary ops", async () => { + const sql = await raToSQL("π[name] (Person ∪ Student)"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("UNION"); }); - it("should allow δ Table without parens", () => { - expect(norm(raToSQL("δ Person"))).toContain("SELECT DISTINCT"); + it("should allow δ Table without parens", async () => { + expect(norm(await raToSQL("δ Person"))).toContain("SELECT DISTINCT"); }); - it("should allow triple chain without parens", () => { - const sql = raToSQL("π[name] σ[age > 20] ρ[n→name] Person"); + it("should allow triple chain without parens", async () => { + const sql = await raToSQL("π[name] σ[age > 20] ρ[n→name] Person"); expect(norm(sql)).toContain("SELECT name FROM"); expect(norm(sql)).toContain("WHERE age > 20"); expect(norm(sql)).toContain("n AS name"); @@ -803,36 +804,36 @@ describe("parenthesis-free syntax", () => { }); describe("implicit return from last assignment", () => { - it("should return last assigned variable when no explicit result", () => { - const sql = raToSQL("A <- σ[age > 20](Person)"); + it("should return last assigned variable when no explicit result", async () => { + const sql = await raToSQL("A <- σ[age > 20](Person)"); expect(norm(sql)).toContain("WITH A AS"); expect(norm(sql)).toContain("SELECT * FROM A"); }); - it("should return last variable with multiple assignments", () => { - const sql = raToSQL("A <- σ[age > 20](Person)\nB <- π[name](A)"); + it("should return last variable with multiple assignments", async () => { + const sql = await raToSQL("A <- σ[age > 20](Person)\nB <- π[name](A)"); expect(norm(sql)).toContain("SELECT * FROM B"); }); - it("should still work when explicit result is given", () => { - const sql = raToSQL("A <- σ[age > 20](Person)\nA"); + it("should still work when explicit result is given", async () => { + const sql = await raToSQL("A <- σ[age > 20](Person)\nA"); expect(norm(sql)).toContain("SELECT * FROM A"); }); - it("should handle self-referential A <- A as a no-op", () => { - const sql = raToSQL("A <- A"); + it("should handle self-referential A <- A as a no-op", async () => { + const sql = await raToSQL("A <- A"); expect(norm(sql)).toBe(norm("SELECT * FROM A")); }); - it("should handle variable reassignment with versioned CTE names", () => { - const sql = raToSQL("A <- σ[age > 20](Person)\nA <- π[name](A)\nA"); + it("should handle variable reassignment with versioned CTE names", async () => { + const sql = await raToSQL("A <- σ[age > 20](Person)\nA <- π[name](A)\nA"); expect(norm(sql)).toContain("WITH A AS"); expect(norm(sql)).toContain("A_v2 AS"); expect(norm(sql)).toContain("SELECT * FROM A_v2"); }); - it("should handle triple reassignment", () => { - const sql = raToSQL("X <- Person\nX <- σ[age > 20](X)\nX <- π[name](X)"); + it("should handle triple reassignment", async () => { + const sql = await raToSQL("X <- Person\nX <- σ[age > 20](X)\nX <- π[name](X)"); expect(norm(sql)).toContain("X AS"); expect(norm(sql)).toContain("X_v2 AS"); expect(norm(sql)).toContain("X_v3 AS"); @@ -877,141 +878,141 @@ describe("SQLite execution", () => { afterAll(() => db.close()); /** Convert RA to SQL and execute, returning result rows */ - function execRA(expr: string): initSqlJs.QueryExecResult[] { - const sql = raToSQL(expr, db); + async function execRA(expr: string): Promise { + const sql = await raToSQL(expr, new SqliteEngine(db)); return db.exec(sql); } // ── Selection ── - it("σ with condition", () => { - const res = execRA("σ[age > 20](Person)"); + it("σ with condition", async () => { + const res = await execRA("σ[age > 20](Person)"); expect(res[0].values.length).toBe(2); // Alice (25) and Carol (30) }); - it("σ with string comparison", () => { - const res = execRA("σ[name = 'Alice'](Person)"); + it("σ with string comparison", async () => { + const res = await execRA("σ[name = 'Alice'](Person)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); - it("σ with compound condition", () => { - const res = execRA("σ[age > 20 and city = 'Stockholm'](Person)"); + it("σ with compound condition", async () => { + const res = await execRA("σ[age > 20 and city = 'Stockholm'](Person)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); // ── Projection ── - it("π selects columns", () => { - const res = execRA("π[name, city](Person)"); + it("π selects columns", async () => { + const res = await execRA("π[name, city](Person)"); expect(res[0].columns).toEqual(["name", "city"]); expect(res[0].values.length).toBe(3); }); // ── Rename ── - it("ρ renames columns", () => { - const res = execRA("ρ[name→fullName](Person)"); + it("ρ renames columns", async () => { + const res = await execRA("ρ[name→fullName](Person)"); expect(res[0].columns).toContain("fullName"); expect(res[0].columns).not.toContain("name"); }); // ── Natural join ── - it("⋈ natural join", () => { - const res = execRA("Person ⋈ Student"); + it("⋈ natural join", async () => { + const res = await execRA("Person ⋈ Student"); expect(res[0].values.length).toBe(2); // ids 1 and 2 match }); - it("natjoin keyword", () => { - const res = execRA("Person natjoin Student"); + it("natjoin keyword", async () => { + const res = await execRA("Person natjoin Student"); expect(res[0].values.length).toBe(2); }); // ── Cross product ── - it("× cross product", () => { - const res = execRA("Person × Course"); + it("× cross product", async () => { + const res = await execRA("Person × Course"); expect(res[0].values.length).toBe(6); // 3 × 2 }); // ── Theta join ── - it("⋈[cond] theta join", () => { + it("⋈[cond] theta join", async () => { // Use unqualified column names since the generator aliases tables as _raN - const res = execRA("Person ⋈[age > credits] Course"); + const res = await execRA("Person ⋈[age > credits] Course"); expect(res[0].values.length).toBeGreaterThan(0); }); // ── Set operations ── - it("∪ union", () => { - const res = execRA("π[name](Person) ∪ π[name](Teacher)"); + it("∪ union", async () => { + const res = await execRA("π[name](Person) ∪ π[name](Teacher)"); expect(res[0].values.length).toBe(5); // 3 + 2, all distinct names }); - it("∩ intersect", () => { + it("∩ intersect", async () => { // No overlapping names between Person and Teacher - const res = execRA("π[name](Person) ∩ π[name](Teacher)"); + const res = await execRA("π[name](Person) ∩ π[name](Teacher)"); expect(res.length === 0 || res[0].values.length === 0).toBe(true); }); - it("− set difference", () => { - const res = execRA("π[name](Person) − π[name](Teacher)"); + it("− set difference", async () => { + const res = await execRA("π[name](Person) − π[name](Teacher)"); expect(res[0].values.length).toBe(3); // All Person names, none overlap }); - it("minus keyword", () => { - const res = execRA("π[name](Person) minus π[name](Teacher)"); + it("minus keyword", async () => { + const res = await execRA("π[name](Person) minus π[name](Teacher)"); expect(res[0].values.length).toBe(3); }); - it("bare table set difference (Person - Teacher)", () => { + it("bare table set difference (Person - Teacher)", async () => { // Person and Teacher are union-compatible (both have id, name, age, city) // but Teacher has an extra column (department) — use projections - const res = execRA("π[id, name](Person) − π[id, name](Teacher)"); + const res = await execRA("π[id, name](Person) − π[id, name](Teacher)"); expect(res[0].values.length).toBe(3); }); - it("set difference with hyphen syntax", () => { - const res = execRA("π[name, city](Person) - π[name, city](Teacher)"); + it("set difference with hyphen syntax", async () => { + const res = await execRA("π[name, city](Person) - π[name, city](Teacher)"); expect(res[0].values.length).toBeGreaterThan(0); }); - it("errors on union-incompatible set operations", () => { + it("errors on union-incompatible set operations", async () => { // Person has 4 cols (id, name, age, city), Course has 3 cols (course_id, title, credits) - expect(() => execRA("Person union Course")).toThrow(RAError); - expect(() => execRA("Person union Course")).toThrow(/same number of columns/i); - expect(() => execRA("Person minus Course")).toThrow(RAError); - expect(() => execRA("Person intersect Course")).toThrow(RAError); + await expect(execRA("Person union Course")).rejects.toThrow(RAError); + await expect(execRA("Person union Course")).rejects.toThrow(/same number of columns/i); + await expect(execRA("Person minus Course")).rejects.toThrow(RAError); + await expect(execRA("Person intersect Course")).rejects.toThrow(RAError); }); - it("backslash set difference", () => { - const res = execRA("π[name](Person) \\ π[name](Teacher)"); + it("backslash set difference", async () => { + const res = await execRA("π[name](Person) \\ π[name](Teacher)"); expect(res[0].values.length).toBe(3); }); // ── Outer joins ── - it("leftjoin", () => { + it("leftjoin", async () => { // Use unqualified column names since the generator aliases tables as _raN - const res = execRA("Person leftjoin[age > credits] Course"); + const res = await execRA("Person leftjoin[age > credits] Course"); expect(res[0].values.length).toBeGreaterThan(0); }); // ── Semi-join ── - it("⋉ left semi-join returns only matching rows", () => { - const res = execRA("Person ⋉ Student"); + it("⋉ left semi-join returns only matching rows", async () => { + const res = await execRA("Person ⋉ Student"); // Person ids 1,2,3 — Student ids 1,2 — semi-join on common col "id" expect(res[0].values.length).toBe(2); }); // ── Anti-join ── - it("▷ anti-join returns only non-matching rows", () => { - const res = execRA("Person ▷ Student"); + it("▷ anti-join returns only non-matching rows", async () => { + const res = await execRA("Person ▷ Student"); // Only Carol (id=3) has no matching Student row expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Carol"); @@ -1019,169 +1020,169 @@ describe("SQLite execution", () => { // ── Distinct ── - it("δ distinct", () => { - const res = execRA("δ(Person)"); + it("δ distinct", async () => { + const res = await execRA("δ(Person)"); expect(res[0].values.length).toBe(3); }); // ── Sort ── - it("τ sort", () => { - const res = execRA("τ[name](Person)"); + it("τ sort", async () => { + const res = await execRA("τ[name](Person)"); expect(res[0].values[0]).toContain("Alice"); expect(res[0].values[2]).toContain("Carol"); }); - it("τ sort DESC", () => { - const res = execRA("τ[age DESC](Person)"); + it("τ sort DESC", async () => { + const res = await execRA("τ[age DESC](Person)"); expect(res[0].values[0]).toContain("Carol"); // age 30, highest }); // ── Grouping / aggregation ── - it("γ group by with COUNT", () => { - const res = execRA("γ[city; COUNT(id) AS cnt](Person)"); + it("γ group by with COUNT", async () => { + const res = await execRA("γ[city; COUNT(id) AS cnt](Person)"); expect(res[0].columns).toContain("cnt"); expect(res[0].values.length).toBe(3); // 3 distinct cities }); // ── Division ── - it("÷ division returns correct result", () => { + it("÷ division returns correct result", async () => { // Enrollment: (1,100),(1,101),(2,100) — Course: (100),(101) // Only id=1 is enrolled in ALL courses - const res = execRA("π[id, course_id](Enrollment) ÷ π[course_id](Course)"); + const res = await execRA("π[id, course_id](Enrollment) ÷ π[course_id](Course)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain(1); // id=1 (Alice) }); // ── Composition / nesting ── - it("π of σ", () => { - const res = execRA("π[name](σ[age > 20](Person))"); + it("π of σ", async () => { + const res = await execRA("π[name](σ[age > 20](Person))"); expect(res[0].columns).toEqual(["name"]); expect(res[0].values.length).toBe(2); }); - it("σ of ⋈", () => { - const res = execRA("σ[age > 20](Person ⋈ Student)"); + it("σ of ⋈", async () => { + const res = await execRA("σ[age > 20](Person ⋈ Student)"); expect(res[0].values.length).toBe(1); // Only Alice (25) matches }); - it("deeply nested", () => { - const res = execRA("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); + it("deeply nested", async () => { + const res = await execRA("π[name](σ[city = 'Stockholm'](Person ⋈ Student))"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); // ── Parenthesis-free syntax ── - it("σ[cond] Table without parens", () => { - const res = execRA("σ[age > 20] Person"); + it("σ[cond] Table without parens", async () => { + const res = await execRA("σ[age > 20] Person"); expect(res[0].values.length).toBe(2); }); - it("π[cols] σ[cond] Table chained", () => { - const res = execRA("π[name] σ[age > 20] Person"); + it("π[cols] σ[cond] Table chained", async () => { + const res = await execRA("π[name] σ[age > 20] Person"); expect(res[0].columns).toEqual(["name"]); expect(res[0].values.length).toBe(2); }); // ── Implicit subscripts ── - it("σ cond (R) implicit", () => { - const res = execRA("σ age > 20 (Person)"); + it("σ cond (R) implicit", async () => { + const res = await execRA("σ age > 20 (Person)"); expect(res[0].values.length).toBe(2); }); - it("π cols (R) implicit", () => { - const res = execRA("π name, city (Person)"); + it("π cols (R) implicit", async () => { + const res = await execRA("π name, city (Person)"); expect(res[0].columns).toEqual(["name", "city"]); }); // ── Assignments ── - it("single assignment", () => { - const res = execRA("A ← σ[age > 20](Person)\nπ[name](A)"); + it("single assignment", async () => { + const res = await execRA("A ← σ[age > 20](Person)\nπ[name](A)"); expect(res[0].values.length).toBe(2); }); - it("multiple assignments", () => { - const res = execRA("A ← σ[city = 'Stockholm'](Person)\nB ← π[name](A)\nB"); + it("multiple assignments", async () => { + const res = await execRA("A ← σ[city = 'Stockholm'](Person)\nB ← π[name](A)\nB"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); - it("variable reassignment", () => { - const res = execRA("X ← Person\nX ← σ[age > 20](X)\nX ← π[name](X)"); + it("variable reassignment", async () => { + const res = await execRA("X ← Person\nX ← σ[age > 20](X)\nX ← π[name](X)"); expect(res[0].values.length).toBe(2); }); // ── LaTeX-style curly braces ── - it("σ_{cond}(R)", () => { - const res = execRA("σ_{age > 20}(Person)"); + it("σ_{cond}(R)", async () => { + const res = await execRA("σ_{age > 20}(Person)"); expect(res[0].values.length).toBe(2); }); - it("π_{cols}(R)", () => { - const res = execRA("π_{name, city}(Person)"); + it("π_{cols}(R)", async () => { + const res = await execRA("π_{name, city}(Person)"); expect(res[0].columns).toEqual(["name", "city"]); }); - it("ρ_{old→new}(R)", () => { - const res = execRA("ρ_{name→fullName}(Person)"); + it("ρ_{old→new}(R)", async () => { + const res = await execRA("ρ_{name→fullName}(Person)"); expect(res[0].columns).toContain("fullName"); expect(res[0].columns).not.toContain("name"); }); - it("σ{cond}(R) without underscore", () => { - const res = execRA("σ{age > 20}(Person)"); + it("σ{cond}(R) without underscore", async () => { + const res = await execRA("σ{age > 20}(Person)"); expect(res[0].values.length).toBe(2); }); - it("⋈{cond} theta join with curly braces", () => { - const res = execRA("Person ⋈{age > credits} Course"); + it("⋈{cond} theta join with curly braces", async () => { + const res = await execRA("Person ⋈{age > credits} Course"); expect(res[0].values.length).toBeGreaterThan(0); }); // ── Selection edge cases ── - it("σ with OR condition", () => { - const res = execRA("σ[city = 'Stockholm' or city = 'York'](Person)"); + it("σ with OR condition", async () => { + const res = await execRA("σ[city = 'Stockholm' or city = 'York'](Person)"); expect(res[0].values.length).toBe(2); // Alice and Bob }); - it("σ with NOT condition", () => { - const res = execRA("σ[not age > 20](Person)"); + it("σ with NOT condition", async () => { + const res = await execRA("σ[not age > 20](Person)"); expect(res[0].values.length).toBe(1); // Only Bob (19) expect(res[0].values[0]).toContain("Bob"); }); - it("σ with nested OR and AND", () => { - const res = execRA("σ[age > 20 or (name = 'Bob' and city = 'York')](Person)"); + it("σ with nested OR and AND", async () => { + const res = await execRA("σ[age > 20 or (name = 'Bob' and city = 'York')](Person)"); expect(res[0].values.length).toBe(3); // Alice, Bob, Carol }); - it("σ with all comparison operators", () => { - expect(execRA("σ[age = 25](Person)")[0].values.length).toBe(1); - expect(execRA("σ[age <> 25](Person)")[0].values.length).toBe(2); - expect(execRA("σ[age != 25](Person)")[0].values.length).toBe(2); - expect(execRA("σ[age < 25](Person)")[0].values.length).toBe(1); - expect(execRA("σ[age > 25](Person)")[0].values.length).toBe(1); - expect(execRA("σ[age <= 25](Person)")[0].values.length).toBe(2); - expect(execRA("σ[age >= 25](Person)")[0].values.length).toBe(2); + it("σ with all comparison operators", async () => { + expect((await execRA("σ[age = 25](Person)"))[0].values.length).toBe(1); + expect((await execRA("σ[age <> 25](Person)"))[0].values.length).toBe(2); + expect((await execRA("σ[age != 25](Person)"))[0].values.length).toBe(2); + expect((await execRA("σ[age < 25](Person)"))[0].values.length).toBe(1); + expect((await execRA("σ[age > 25](Person)"))[0].values.length).toBe(1); + expect((await execRA("σ[age <= 25](Person)"))[0].values.length).toBe(2); + expect((await execRA("σ[age >= 25](Person)"))[0].values.length).toBe(2); }); - it("σ with table.column references", () => { - const res = execRA("σ[Person.age > 20](Person)"); + it("σ with table.column references", async () => { + const res = await execRA("σ[Person.age > 20](Person)"); expect(res[0].values.length).toBe(2); }); // ── Rename edge cases ── - it("ρ with multiple rename mappings", () => { - const res = execRA("ρ[name→fullName, age→years](Person)"); + it("ρ with multiple rename mappings", async () => { + const res = await execRA("ρ[name→fullName, age→years](Person)"); expect(res[0].columns).toContain("fullName"); expect(res[0].columns).toContain("years"); expect(res[0].columns).not.toContain("name"); @@ -1189,225 +1190,225 @@ describe("SQLite execution", () => { expect(res[0].values.length).toBe(3); }); - it("ρ with ASCII arrow", () => { - const res = execRA("rho[name->fullName](Person)"); + it("ρ with ASCII arrow", async () => { + const res = await execRA("rho[name->fullName](Person)"); expect(res[0].columns).toContain("fullName"); }); // ── Outer joins ── - it("rightjoin", () => { - const res = execRA("Student rightjoin[age > hasDisability] Person"); + it("rightjoin", async () => { + const res = await execRA("Student rightjoin[age > hasDisability] Person"); expect(res[0].values.length).toBeGreaterThan(0); }); - it("fulljoin", () => { - const res = execRA("Person fulljoin[age > credits] Course"); + it("fulljoin", async () => { + const res = await execRA("Person fulljoin[age > credits] Course"); expect(res[0].values.length).toBeGreaterThan(0); }); - it("⟕ left outer join with Unicode", () => { - const res = execRA("Person ⟕[age > credits] Course"); + it("⟕ left outer join with Unicode", async () => { + const res = await execRA("Person ⟕[age > credits] Course"); expect(res[0].values.length).toBeGreaterThan(0); }); - it("left join preserves non-matching rows", () => { + it("left join preserves non-matching rows", async () => { // Carol (age 30) has no Student row — left join should keep her with NULLs - const res = execRA("Person leftjoin[Person.id = Student.id] Student"); + const res = await execRA("Person leftjoin[Person.id = Student.id] Student"); expect(res[0].values.length).toBe(3); // All 3 Person rows }); // ── Semi-join edge cases ── - it("⋊ right semi-join", () => { - const res = execRA("Student ⋊ Person"); + it("⋊ right semi-join", async () => { + const res = await execRA("Student ⋊ Person"); // Student ids 1,2 both exist in Person — all Student rows match expect(res[0].values.length).toBe(2); }); - it("rightsemijoin keyword", () => { - const res = execRA("Student rightsemijoin Person"); + it("rightsemijoin keyword", async () => { + const res = await execRA("Student rightsemijoin Person"); expect(res[0].values.length).toBe(2); }); - it("leftsemijoin keyword", () => { - const res = execRA("Person leftsemijoin Student"); + it("leftsemijoin keyword", async () => { + const res = await execRA("Person leftsemijoin Student"); expect(res[0].values.length).toBe(2); }); - it("antijoin keyword", () => { - const res = execRA("Person antijoin Student"); + it("antijoin keyword", async () => { + const res = await execRA("Person antijoin Student"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Carol"); }); // ── Sort edge cases ── - it("τ with multiple sort columns", () => { - const res = execRA("τ[city, age DESC](Person)"); + it("τ with multiple sort columns", async () => { + const res = await execRA("τ[city, age DESC](Person)"); // Bristol(30), Stockholm(25), York(19) expect(res[0].values[0]).toContain("Carol"); // Bristol expect(res[0].values[1]).toContain("Alice"); // Stockholm expect(res[0].values[2]).toContain("Bob"); // York }); - it("sort keyword", () => { - const res = execRA("sort[name](Person)"); + it("sort keyword", async () => { + const res = await execRA("sort[name](Person)"); expect(res[0].values[0]).toContain("Alice"); expect(res[0].values[2]).toContain("Carol"); }); // ── Aggregation edge cases ── - it("γ with SUM", () => { - const res = execRA("γ[city; SUM(age) AS totalAge](Person)"); + it("γ with SUM", async () => { + const res = await execRA("γ[city; SUM(age) AS totalAge](Person)"); expect(res[0].columns).toContain("totalAge"); expect(res[0].values.length).toBe(3); }); - it("γ with AVG", () => { - const res = execRA("γ[city; AVG(age) AS avgAge](Person)"); + it("γ with AVG", async () => { + const res = await execRA("γ[city; AVG(age) AS avgAge](Person)"); expect(res[0].columns).toContain("avgAge"); expect(res[0].values.length).toBe(3); }); - it("γ with MIN and MAX", () => { - const res = execRA("γ[city; MIN(age) AS youngest, MAX(age) AS oldest](Person)"); + it("γ with MIN and MAX", async () => { + const res = await execRA("γ[city; MIN(age) AS youngest, MAX(age) AS oldest](Person)"); expect(res[0].columns).toContain("youngest"); expect(res[0].columns).toContain("oldest"); }); - it("γ COUNT without alias", () => { - const res = execRA("γ[city; COUNT(id)](Person)"); + it("γ COUNT without alias", async () => { + const res = await execRA("γ[city; COUNT(id)](Person)"); expect(res[0].values.length).toBe(3); }); - it("gamma keyword", () => { - const res = execRA("gamma[city; COUNT(id) AS cnt](Person)"); + it("gamma keyword", async () => { + const res = await execRA("gamma[city; COUNT(id) AS cnt](Person)"); expect(res[0].columns).toContain("cnt"); }); // ── Distinct edge cases ── - it("delta keyword", () => { - const res = execRA("delta(Person)"); + it("delta keyword", async () => { + const res = await execRA("delta(Person)"); expect(res[0].values.length).toBe(3); }); - it("distinct keyword", () => { - const res = execRA("distinct(Person)"); + it("distinct keyword", async () => { + const res = await execRA("distinct(Person)"); expect(res[0].values.length).toBe(3); }); - it("δ without parens", () => { - const res = execRA("δ Person"); + it("δ without parens", async () => { + const res = await execRA("δ Person"); expect(res[0].values.length).toBe(3); }); // ── Keyword variants for operators ── - it("select keyword", () => { - const res = execRA("select[age > 20](Person)"); + it("select keyword", async () => { + const res = await execRA("select[age > 20](Person)"); expect(res[0].values.length).toBe(2); }); - it("project keyword", () => { - const res = execRA("project[name, city](Person)"); + it("project keyword", async () => { + const res = await execRA("project[name, city](Person)"); expect(res[0].columns).toEqual(["name", "city"]); }); - it("cross keyword", () => { - const res = execRA("Person cross Course"); + it("cross keyword", async () => { + const res = await execRA("Person cross Course"); expect(res[0].values.length).toBe(6); }); - it("join keyword with condition", () => { - const res = execRA("Person join[age > credits] Course"); + it("join keyword with condition", async () => { + const res = await execRA("Person join[age > credits] Course"); expect(res[0].values.length).toBeGreaterThan(0); }); - it("|X| as natural join", () => { - const res = execRA("Person |X| Student"); + it("|X| as natural join", async () => { + const res = await execRA("Person |X| Student"); expect(res[0].values.length).toBe(2); }); - it("|><| as natural join", () => { - const res = execRA("Person |><| Student"); + it("|><| as natural join", async () => { + const res = await execRA("Person |><| Student"); expect(res[0].values.length).toBe(2); }); - it("intersect keyword", () => { - const res = execRA("π[name](Person) intersect π[name](Teacher)"); + it("intersect keyword", async () => { + const res = await execRA("π[name](Person) intersect π[name](Teacher)"); expect(res.length === 0 || res[0].values.length === 0).toBe(true); }); - it("divide keyword", () => { - const res = execRA("π[id, course_id](Enrollment) divide π[course_id](Course)"); + it("divide keyword", async () => { + const res = await execRA("π[id, course_id](Enrollment) divide π[course_id](Course)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain(1); }); // ── Implicit subscripts edge cases ── - it("ρ old→new (R) implicit", () => { - const res = execRA("ρ name→fullName (Person)"); + it("ρ old→new (R) implicit", async () => { + const res = await execRA("ρ name→fullName (Person)"); expect(res[0].columns).toContain("fullName"); }); - it("τ col (R) implicit", () => { - const res = execRA("τ name (Person)"); + it("τ col (R) implicit", async () => { + const res = await execRA("τ name (Person)"); expect(res[0].values[0]).toContain("Alice"); }); - it("σ compound implicit with AND", () => { - const res = execRA("σ age > 20 and city = 'Stockholm' (Person)"); + it("σ compound implicit with AND", async () => { + const res = await execRA("σ age > 20 and city = 'Stockholm' (Person)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); - it("nested implicit subscripts", () => { - const res = execRA("π name (σ age > 20 (Person))"); + it("nested implicit subscripts", async () => { + const res = await execRA("π name (σ age > 20 (Person))"); expect(res[0].columns).toEqual(["name"]); expect(res[0].values.length).toBe(2); }); // ── Parenthesis-free edge cases ── - it("triple chain without parens", () => { - const res = execRA("π[name] σ[age > 20] δ Person"); + it("triple chain without parens", async () => { + const res = await execRA("π[name] σ[age > 20] δ Person"); expect(res[0].columns).toEqual(["name"]); expect(res[0].values.length).toBe(2); }); - it("π[cols] over union with parens", () => { - const res = execRA("π[name] (π[name](Person) ∪ π[name](Teacher))"); + it("π[cols] over union with parens", async () => { + const res = await execRA("π[name] (π[name](Person) ∪ π[name](Teacher))"); expect(res[0].values.length).toBe(5); }); // ── Assignment edge cases ── - it("assignment with <- ASCII arrow", () => { - const res = execRA("A <- σ[age > 20](Person)\nπ[name](A)"); + it("assignment with <- ASCII arrow", async () => { + const res = await execRA("A <- σ[age > 20](Person)\nπ[name](A)"); expect(res[0].values.length).toBe(2); }); - it("semicolons as statement separators", () => { - const res = execRA("A ← Person; π[name](A)"); + it("semicolons as statement separators", async () => { + const res = await execRA("A ← Person; π[name](A)"); expect(res[0].values.length).toBe(3); }); - it("comments in multi-line input", () => { - const res = execRA("-- Get adults\nA ← σ[age > 20](Person)\n-- Project names\nπ[name](A)"); + it("comments in multi-line input", async () => { + const res = await execRA("-- Get adults\nA ← σ[age > 20](Person)\n-- Project names\nπ[name](A)"); expect(res[0].values.length).toBe(2); }); - it("implicit return from last assignment", () => { - const res = execRA("A ← σ[age > 20](Person)"); + it("implicit return from last assignment", async () => { + const res = await execRA("A ← σ[age > 20](Person)"); expect(res[0].values.length).toBe(2); }); - it("complex pipeline with reassignment", () => { - const res = execRA("X ← Person ⋈ Student\nX ← σ[age > 20](X)\nX ← π[name](X)"); + it("complex pipeline with reassignment", async () => { + const res = await execRA("X ← Person ⋈ Student\nX ← σ[age > 20](X)\nX ← π[name](X)"); expect(res[0].values.length).toBe(1); expect(res[0].values[0]).toContain("Alice"); }); diff --git a/src/ra-engine/relationalAlgebra.ts b/src/ra-engine/relationalAlgebra.ts index b1e8514..1363bd3 100644 --- a/src/ra-engine/relationalAlgebra.ts +++ b/src/ra-engine/relationalAlgebra.ts @@ -7,6 +7,8 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License in the LICENSE.md file in this repository. */ +import type { DatabaseEngine } from "../database/types"; + // ─── Token Types ──────────────────────────────────────────────────────────── enum TokenType { @@ -1006,56 +1008,28 @@ function aliasFor(node: RANode): string { return node.type === "table" ? node.name : `_ra${subqueryCounter++}`; } -/** - * Interface for a minimal database handle used to resolve column names. - * Compatible with sql.js Database. - */ -interface DatabaseHandle { - exec(sql: string): { columns: string[]; values: unknown[][] }[]; - prepare(sql: string): { step(): boolean; getColumnNames(): string[]; free(): void }; -} - /** * Resolve the column names produced by a SQL expression using the database. - * Uses prepare() to get column metadata without executing the query. + * Uses db.getColumnNames() which handles both SQLite and PostgreSQL internally. */ -function resolveColumns(sql: string, db: DatabaseHandle): string[] { - // For bare table names, use PRAGMA table_info which always works - if (/^\w+$/.test(sql.trim())) { - const tableName = sql.trim(); - try { - const res = db.exec(`PRAGMA table_info(${tableName})`); - if (res.length > 0 && res[0].values.length > 0) { - // PRAGMA table_info returns rows with [cid, name, type, notnull, dflt_value, pk] - return res[0].values.map((row: unknown[]) => String(row[1])); - } - } catch { - // Table doesn't exist - } - throw new RAError(`Table '${tableName}' does not exist`); - } - - // For complex expressions, use prepare to get column names +async function resolveColumns(sql: string, db: DatabaseEngine): Promise { try { - const probeSQL = `SELECT * FROM (${sql}) LIMIT 0`; - const stmt = db.prepare(probeSQL); - // Step once to initialize column metadata (required by some sql.js versions) - stmt.step(); - const cols = stmt.getColumnNames(); - stmt.free(); - return cols; + return await db.getColumnNames(sql); } catch (e) { const msg = (e as Error).message || String(e); if (/no such table/i.test(msg)) { const match = msg.match(/no such table:\s*(\S+)/i); throw new RAError(match ? `Table '${match[1]}' does not exist` : msg); } + if (/does not exist/i.test(msg) || /not found/i.test(msg)) { + throw new RAError(msg); + } throw new RAError(msg); } } /** Ensure a node produces a full SELECT statement (needed for UNION/INTERSECT/EXCEPT) */ -function asSelect(node: RANode, db?: DatabaseHandle): string { - const sql = nodeToSQL(node, db); +async function asSelect(node: RANode, db?: DatabaseEngine): Promise { + const sql = await nodeToSQL(node, db); // Bare table names need wrapping; anything starting with SELECT is already a query return sql.trimStart().toUpperCase().startsWith("SELECT") ? sql : `SELECT * FROM ${sql}`; } @@ -1064,11 +1038,11 @@ function asSelect(node: RANode, db?: DatabaseHandle): string { * Build a WHERE clause correlating two aliases on their common columns. * Returns empty string if no db or no common columns found. */ -function buildCorrelation(leftSQL: string, rightSQL: string, lAlias: string, rAlias: string, db?: DatabaseHandle): string { +async function buildCorrelation(leftSQL: string, rightSQL: string, lAlias: string, rAlias: string, db?: DatabaseEngine): Promise { if (!db) return ""; try { - const leftCols = resolveColumns(leftSQL, db); - const rightCols = resolveColumns(rightSQL, db); + const leftCols = await resolveColumns(leftSQL, db); + const rightCols = await resolveColumns(rightSQL, db); const common = leftCols.filter(c => rightCols.includes(c)); if (common.length > 0) { return " WHERE " + common.map(c => `${lAlias}.${c} = ${rAlias}.${c}`).join(" AND "); @@ -1079,26 +1053,26 @@ function buildCorrelation(leftSQL: string, rightSQL: string, lAlias: string, rAl return ""; } -function nodeToSQL(node: RANode, db?: DatabaseHandle): string { +async function nodeToSQL(node: RANode, db?: DatabaseEngine): Promise { switch (node.type) { case "table": return node.name; case "selection": - return `SELECT * FROM (${nodeToSQL(node.relation, db)}) WHERE ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${await nodeToSQL(node.relation, db)}) WHERE ${conditionToSQL(node.condition)}`; case "projection": - return `SELECT ${node.columns.map(columnRefToSQL).join(", ")} FROM (${nodeToSQL(node.relation, db)})`; + return `SELECT ${node.columns.map(columnRefToSQL).join(", ")} FROM (${await nodeToSQL(node.relation, db)})`; case "rename": { - const inner = nodeToSQL(node.relation, db); + const inner = await nodeToSQL(node.relation, db); // Simple case: rename columns const colMappings = node.mappings.map(m => `${m.from} AS ${m.to}`).join(", "); return `SELECT ${colMappings} FROM (${inner})`; } case "group": { - const inner = nodeToSQL(node.relation, db); + const inner = await nodeToSQL(node.relation, db); const groupCols = node.groupBy.map(columnRefToSQL); const aggExprs = node.aggregates.map(a => { const expr = `${a.func}(${columnRefToSQL(a.column)})`; @@ -1110,7 +1084,7 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { } case "sort": { - const inner = nodeToSQL(node.relation, db); + const inner = await nodeToSQL(node.relation, db); const orderParts = node.columns.map(c => `${columnRefToSQL(c.column)}${c.desc ? " DESC" : ""}` ).join(", "); @@ -1118,17 +1092,17 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { } case "distinct": - return `SELECT DISTINCT * FROM (${nodeToSQL(node.relation, db)})`; + return `SELECT DISTINCT * FROM (${await nodeToSQL(node.relation, db)})`; case "crossProduct": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} CROSS JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)}`; + return `SELECT * FROM (${await nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} CROSS JOIN (${await nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)}`; case "naturalJoin": { - const leftSQL = nodeToSQL(node.left, db); - const rightSQL = nodeToSQL(node.right, db); + const leftSQL = await nodeToSQL(node.left, db); + const rightSQL = await nodeToSQL(node.right, db); if (db) { - const leftCols = resolveColumns(leftSQL, db); - const rightCols = resolveColumns(rightSQL, db); + const leftCols = await resolveColumns(leftSQL, db); + const rightCols = await resolveColumns(rightSQL, db); if (leftCols.length > 0 && rightCols.length > 0) { const common = leftCols.filter(c => rightCols.includes(c)); if (common.length === 0) { @@ -1144,53 +1118,53 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { } case "thetaJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${await nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} JOIN (${await nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "leftJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} LEFT JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${await nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} LEFT JOIN (${await nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "rightJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} RIGHT JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${await nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} RIGHT JOIN (${await nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "fullJoin": - return `SELECT * FROM (${nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} FULL OUTER JOIN (${nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; + return `SELECT * FROM (${await nodeToSQL(node.left, db)}) AS ${aliasFor(node.left)} FULL OUTER JOIN (${await nodeToSQL(node.right, db)}) AS ${aliasFor(node.right)} ON ${conditionToSQL(node.condition)}`; case "leftSemiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - const leftSQL = nodeToSQL(node.left, db); - const rightSQL = nodeToSQL(node.right, db); - const corr = buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); + const leftSQL = await nodeToSQL(node.left, db); + const rightSQL = await nodeToSQL(node.right, db); + const corr = await buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); return `SELECT ${lAlias}.* FROM (${leftSQL}) AS ${lAlias} WHERE EXISTS (SELECT 1 FROM (${rightSQL}) AS ${rAlias}${corr})`; } case "rightSemiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - const leftSQL = nodeToSQL(node.left, db); - const rightSQL = nodeToSQL(node.right, db); + const leftSQL = await nodeToSQL(node.left, db); + const rightSQL = await nodeToSQL(node.right, db); // Outer relation is right; EXISTS checks left — correlate right (outer) with left (inner) - const corr = buildCorrelation(rightSQL, leftSQL, rAlias, lAlias, db); + const corr = await buildCorrelation(rightSQL, leftSQL, rAlias, lAlias, db); return `SELECT ${rAlias}.* FROM (${rightSQL}) AS ${rAlias} WHERE EXISTS (SELECT 1 FROM (${leftSQL}) AS ${lAlias}${corr})`; } case "antiJoin": { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; - const leftSQL = nodeToSQL(node.left, db); - const rightSQL = nodeToSQL(node.right, db); - const corr = buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); + const leftSQL = await nodeToSQL(node.left, db); + const rightSQL = await nodeToSQL(node.right, db); + const corr = await buildCorrelation(leftSQL, rightSQL, lAlias, rAlias, db); return `SELECT ${lAlias}.* FROM (${leftSQL}) AS ${lAlias} WHERE NOT EXISTS (SELECT 1 FROM (${rightSQL}) AS ${rAlias}${corr})`; } case "union": case "intersect": case "difference": { - const leftSQL = asSelect(node.left, db); - const rightSQL = asSelect(node.right, db); + const leftSQL = await asSelect(node.left, db); + const rightSQL = await asSelect(node.right, db); if (db) { - const leftCols = resolveColumns(leftSQL, db); - const rightCols = resolveColumns(rightSQL, db); + const leftCols = await resolveColumns(leftSQL, db); + const rightCols = await resolveColumns(rightSQL, db); if (leftCols.length > 0 && rightCols.length > 0 && leftCols.length !== rightCols.length) { const opName = node.type === "union" ? "Union (∪)" : node.type === "intersect" ? "Intersect (∩)" : "Difference (−)"; throw new RAError( @@ -1208,13 +1182,13 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string { const lAlias = `_ra${subqueryCounter++}`; const rAlias = `_ra${subqueryCounter++}`; const innerAlias = `_ra${subqueryCounter++}`; - const leftSQL = nodeToSQL(node.left, db); - const rightSQL = nodeToSQL(node.right, db); + const leftSQL = await nodeToSQL(node.left, db); + const rightSQL = await nodeToSQL(node.right, db); if (db) { try { - const leftCols = resolveColumns(leftSQL, db); - const rightCols = resolveColumns(rightSQL, db); + const leftCols = await resolveColumns(leftSQL, db); + const rightCols = await resolveColumns(rightSQL, db); const aOnlyCols = leftCols.filter(c => !rightCols.includes(c)); const bCols = rightCols; @@ -1283,9 +1257,9 @@ function rewriteTableRefs(node: RANode, nameMap: Record): RANode } } -function programToSQL(program: RAProgram, db?: DatabaseHandle): string { +async function programToSQL(program: RAProgram, db?: DatabaseEngine): Promise { if (program.assignments.length === 0) { - return nodeToSQL(program.result, db); + return await nodeToSQL(program.result, db); } // Use CTEs (WITH clauses) for assignments. @@ -1305,7 +1279,7 @@ function programToSQL(program: RAProgram, db?: DatabaseHandle): string { // Rewrite the expression: replace table references with their current CTE aliases const rewrittenExpr = rewriteTableRefs(a.expr, nameMap); - const sql = nodeToSQL(rewrittenExpr, db); + const sql = await nodeToSQL(rewrittenExpr, db); const wrappedSQL = /^\w+$/.test(sql) ? `SELECT * FROM ${sql}` : sql; // Determine the CTE name for this assignment @@ -1318,7 +1292,7 @@ function programToSQL(program: RAProgram, db?: DatabaseHandle): string { // Rewrite the result expression with final name mappings const rewrittenResult = rewriteTableRefs(program.result, nameMap); - const resultSQL = nodeToSQL(rewrittenResult, db); + const resultSQL = await nodeToSQL(rewrittenResult, db); const wrappedResult = /^\w+$/.test(resultSQL) ? `SELECT * FROM ${resultSQL}` : resultSQL; if (ctes.length === 0) { @@ -1364,12 +1338,12 @@ function programToSQL(program: RAProgram, db?: DatabaseHandle): string { * @returns The equivalent SQL query string * @throws RAError if the expression cannot be parsed or a natural join has no common columns */ -export function raToSQL(input: string, db?: DatabaseHandle): string { +export async function raToSQL(input: string, db?: DatabaseEngine): Promise { subqueryCounter = 0; const tokens = tokenize(input); const parser = new Parser(tokens); const program = parser.parse(); - return programToSQL(program, db); + return await programToSQL(program, db); } // Re-export for potential future use (e.g., AST visualization) diff --git a/src/shims/empty.ts b/src/shims/empty.ts new file mode 100644 index 0000000..cf8189b --- /dev/null +++ b/src/shims/empty.ts @@ -0,0 +1,2 @@ +// Empty shim for Node modules that PGLite imports but doesn't use in browser +export default {}; diff --git a/src/useEditorSettings.ts b/src/useEditorSettings.ts new file mode 100644 index 0000000..4f130d6 --- /dev/null +++ b/src/useEditorSettings.ts @@ -0,0 +1,39 @@ +import { useState, useCallback } from "react"; + +export interface EditorSettings { + autocomplete: boolean; + lineNumbers: boolean; + highlightActiveLine: boolean; + tabSize: number; +} + +const STORAGE_KEY = "editorSettings"; + +const DEFAULTS: EditorSettings = { + autocomplete: true, + lineNumbers: false, + highlightActiveLine: true, + tabSize: 2, +}; + +function loadSettings(): EditorSettings { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) return { ...DEFAULTS, ...JSON.parse(raw) }; + } catch { /* ignore */ } + return { ...DEFAULTS }; +} + +export function useEditorSettings() { + const [settings, setSettingsState] = useState(loadSettings); + + const setSettings = useCallback((partial: Partial) => { + setSettingsState(prev => { + const next = { ...prev, ...partial }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + return next; + }); + }, []); + + return { settings, setSettings }; +} diff --git a/vite.config.ts b/vite.config.ts index 7a3a673..e06bbaf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,8 @@ import react from "@vitejs/plugin-react"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import path from "path"; +const emptyShim = path.resolve(__dirname, "src/shims/empty.ts"); + // https://vite.dev/config/ export default defineConfig({ plugins: [ @@ -28,6 +30,13 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), + // PGLite imports these Node subpaths which polyfill mocks don't support. + // PGLite detects browser environment and doesn't actually use them. + "stream/promises": emptyShim, + "fs/promises": emptyShim, }, }, + optimizeDeps: { + exclude: ["@electric-sql/pglite"], + }, }); From 57289047f7cd7cf3bb1e97c4afe45e7e279b951e Mon Sep 17 00:00:00 2001 From: Edwin <60476129+Edwinexd@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:45:09 +0100 Subject: [PATCH 09/13] Add engine to save format (v4) and engine-aware import/export --- src/ImportDialog.tsx | 7 ++++++- src/mergeUtils.ts | 16 +++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ImportDialog.tsx b/src/ImportDialog.tsx index af181c6..1a7792d 100644 --- a/src/ImportDialog.tsx +++ b/src/ImportDialog.tsx @@ -80,7 +80,7 @@ function SqlBlock({ code, changedLines, variant }: { code: string; changedLines? const ImportDialog = forwardRef( ({ importedData, onOverwrite, onMergeApply }, ref) => { - const { t, lang, questions } = useLanguage(); + const { t, lang, engine, questions } = useLanguage(); const [isOpen, setIsOpen] = useState(false); const [step, setStep] = useState<"choose" | "resolve">("choose"); const [analysis, setAnalysis] = useState(null); @@ -169,6 +169,11 @@ const ImportDialog = forwardRef( {t("languageMismatchWarning", { fileLang: importedData.language })}

)} + {importedData && importedData.engine !== engine && ( +

+ {t("engineMismatchWarning", { fileEngine: importedData.engine })} +

+ )}