From 44ed877bf6956696aab736df753a246b5222a1b9 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Mon, 15 Jan 2024 10:12:49 +0530 Subject: [PATCH 01/16] Prisma: Initial Wiring --- example/prisma/.gitignore | 133 ++ example/prisma/client.js | 128 ++ example/prisma/index.js | 168 ++ example/prisma/package-lock.json | 295 +++ example/prisma/package.json | 23 + example/prisma/schema.prisma | 23 + example/prisma/tsconfig.json | 109 + example/socket/.gitignore | 133 ++ example/socket/client.js | 96 + example/socket/index.js | 50 + example/socket/index.v2.js | 19 + example/socket/package.json | 11 + package-lock.json | 4 +- package.json | 3 +- .../prisma-adapter/.github/workflows/ci.yml | 27 + .../.github/workflows/publish.yml | 26 + prisma/prisma-adapter/.gitignore | 45 + prisma/prisma-adapter/README.md | 156 ++ prisma/prisma-adapter/dev_guide.md | 8 + prisma/prisma-adapter/package-lock.json | 1894 +++++++++++++++++ prisma/prisma-adapter/package.json | 37 + prisma/prisma-adapter/src/index.ts | 143 ++ prisma/prisma-adapter/src/queryUtils.ts | 103 + prisma/prisma-adapter/tsconfig.build.json | 6 + prisma/prisma-adapter/tsconfig.json | 23 + src/queryExecuter.js | 50 +- src/queryParser.js | 80 +- src/server.js | 162 ++ student.csv | 12 +- tests/insertExecuter.test.js | 75 +- {demo => web}/server/enrollment.csv | 0 {demo => web}/server/index.js | 0 {demo => web}/server/package-lock.json | 0 {demo => web}/server/package.json | 0 {demo => web}/server/student.csv | 0 {demo => web}/www/frontend.js | 0 {demo => web}/www/index.html | 0 {demo => web}/www/xterm.css | 0 {demo => web}/www/xterm.js | 0 39 files changed, 4014 insertions(+), 28 deletions(-) create mode 100644 example/prisma/.gitignore create mode 100644 example/prisma/client.js create mode 100644 example/prisma/index.js create mode 100644 example/prisma/package-lock.json create mode 100644 example/prisma/package.json create mode 100644 example/prisma/schema.prisma create mode 100644 example/prisma/tsconfig.json create mode 100644 example/socket/.gitignore create mode 100644 example/socket/client.js create mode 100644 example/socket/index.js create mode 100644 example/socket/index.v2.js create mode 100644 example/socket/package.json create mode 100644 prisma/prisma-adapter/.github/workflows/ci.yml create mode 100644 prisma/prisma-adapter/.github/workflows/publish.yml create mode 100644 prisma/prisma-adapter/.gitignore create mode 100644 prisma/prisma-adapter/README.md create mode 100644 prisma/prisma-adapter/dev_guide.md create mode 100644 prisma/prisma-adapter/package-lock.json create mode 100644 prisma/prisma-adapter/package.json create mode 100644 prisma/prisma-adapter/src/index.ts create mode 100644 prisma/prisma-adapter/src/queryUtils.ts create mode 100644 prisma/prisma-adapter/tsconfig.build.json create mode 100644 prisma/prisma-adapter/tsconfig.json create mode 100644 src/server.js rename {demo => web}/server/enrollment.csv (100%) rename {demo => web}/server/index.js (100%) rename {demo => web}/server/package-lock.json (100%) rename {demo => web}/server/package.json (100%) rename {demo => web}/server/student.csv (100%) rename {demo => web}/www/frontend.js (100%) rename {demo => web}/www/index.html (100%) rename {demo => web}/www/xterm.css (100%) rename {demo => web}/www/xterm.js (100%) diff --git a/example/prisma/.gitignore b/example/prisma/.gitignore new file mode 100644 index 0000000..656790b --- /dev/null +++ b/example/prisma/.gitignore @@ -0,0 +1,133 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +**/.DS_Store \ No newline at end of file diff --git a/example/prisma/client.js b/example/prisma/client.js new file mode 100644 index 0000000..cecf1ea --- /dev/null +++ b/example/prisma/client.js @@ -0,0 +1,128 @@ + +export class Client { + socket = new net.Socket(); + isConnected = false; + isWaitingForResponse = false; + responseHandlers = {}; + + constructor(url) { + this.url = url; + } + + connect() { + return new Promise((resolve, reject) => { + this.socket.connect(this.url.split(":")[1], this.url.split(":")[0], () => { + console.log('Attempting to connect to the server...'); + }); + + let buffer = ''; + + this.socket.on('data', (data) => { + buffer += data.toString(); + let boundary = buffer.indexOf('\n'); + while (boundary !== -1) { + const message = buffer.substring(0, boundary).trim(); + buffer = buffer.substring(boundary + 1); + boundary = buffer.indexOf('\n'); + + this.handleResponse(message); + } + }); + + this.socket.on('connect', () => { + this.isConnected = true; + resolve(); + }); + + this.socket.on('error', (error) => { + console.error('Error:', error.message); + this.isConnected = false; + reject(error); + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.isConnected = false; + }); + }); + } + + + disconnect() { + this.socket.end(); + } + + setup() { + this.socket.connect(this.url.split(":")[1], this.url.split(":")[0], () => { + console.log('Attempting to connect to the server...'); + }); + + let buffer = ''; + + this.socket.on('data', (data) => { + buffer += data.toString(); + let boundary = buffer.indexOf('\n'); + while (boundary !== -1) { + const message = buffer.substring(0, boundary).trim(); + buffer = buffer.substring(boundary + 1); + boundary = buffer.indexOf('\n'); + + this.handleResponse(message); + } + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.isConnected = false; + }); + + this.socket.on('error', (error) => { + console.error('Error:', error.message); + this.isConnected = false; + }); + } + + execute = (query) => { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error('Not connected to the server.')); + return; + } + if (this.isWaitingForResponse) { + reject(new Error('Waiting for the previous query to be processed.')); + return; + } + + const queryId = this.generateQueryId(); + this.responseHandlers[queryId] = { resolve, reject }; + this.socket.write(`${queryId}:${query}`); + this.isWaitingForResponse = true; + }); + } + + handleResponse(response) { + if (response === 'Connected') { + this.isConnected = true; + console.log('Successfully connected to the server.'); + return; + } + + // Handle query response + const [queryId, result] = response.split('<|>', 2); + console.log('Received from server:', queryId, result); + if (this.responseHandlers[queryId]) { + try { + const parsedResult = JSON.parse(result); + this.responseHandlers[queryId].resolve(parsedResult); + } catch (error) { + this.responseHandlers[queryId].reject(error); + } + delete this.responseHandlers[queryId]; + this.isWaitingForResponse = false; + } + } + + generateQueryId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} \ No newline at end of file diff --git a/example/prisma/index.js b/example/prisma/index.js new file mode 100644 index 0000000..38222f2 --- /dev/null +++ b/example/prisma/index.js @@ -0,0 +1,168 @@ +import { PrismaClient } from '@prisma/client'; +import { PrismaStylusDBSQL } from 'stylusdb-sql-prisma-adapter'; +// const net = require('net'); +import net from 'net'; + + +const connectionString = `127.0.0.1:5432` + +class Client { + socket = new net.Socket(); + isConnected = false; + isWaitingForResponse = false; + responseHandlers = {}; + + constructor(url) { + this.url = url; + } + + connect() { + return new Promise((resolve, reject) => { + this.socket.connect(this.url.split(":")[1], this.url.split(":")[0], () => { + console.log('Attempting to connect to the server...'); + }); + + let buffer = ''; + + this.socket.on('data', (data) => { + buffer += data.toString(); + let boundary = buffer.indexOf('\n'); + while (boundary !== -1) { + const message = buffer.substring(0, boundary).trim(); + buffer = buffer.substring(boundary + 1); + boundary = buffer.indexOf('\n'); + + this.handleResponse(message); + } + }); + + this.socket.on('connect', () => { + this.isConnected = true; + resolve(); + }); + + this.socket.on('error', (error) => { + console.error('Error:', error.message); + this.isConnected = false; + reject(error); + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.isConnected = false; + }); + }); + } + + + disconnect() { + this.socket.end(); + } + + setup() { + this.socket.connect(this.url.split(":")[1], this.url.split(":")[0], () => { + console.log('Attempting to connect to the server...'); + }); + + let buffer = ''; + + this.socket.on('data', (data) => { + buffer += data.toString(); + let boundary = buffer.indexOf('\n'); + while (boundary !== -1) { + const message = buffer.substring(0, boundary).trim(); + buffer = buffer.substring(boundary + 1); + boundary = buffer.indexOf('\n'); + + this.handleResponse(message); + } + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.isConnected = false; + }); + + this.socket.on('error', (error) => { + console.error('Error:', error.message); + this.isConnected = false; + }); + } + + execute = (query) => { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error('Not connected to the server.')); + return; + } + if (this.isWaitingForResponse) { + reject(new Error('Waiting for the previous query to be processed.')); + return; + } + + const queryId = this.generateQueryId(); + this.responseHandlers[queryId] = { resolve, reject }; + this.socket.write(`${queryId}:${query}`); + this.isWaitingForResponse = true; + }); + } + + handleResponse(response) { + if (response === 'Connected') { + this.isConnected = true; + console.log('Successfully connected to the server.'); + return; + } + + // Handle query response + const [queryId, result] = response.split('<|>', 2); + console.log('Received from server:', queryId, result); + if (this.responseHandlers[queryId]) { + try { + const parsedResult = JSON.parse(result); + this.responseHandlers[queryId].resolve(parsedResult); + } catch (error) { + this.responseHandlers[queryId].reject(error); + } + delete this.responseHandlers[queryId]; + this.isWaitingForResponse = false; + } + } + + generateQueryId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} + +const client = new Client(connectionString) +const adapter = new PrismaStylusDBSQL(client, {}) +const prisma = new PrismaClient({ adapter }) + +async function main() { + await client.connect(); + const rawQueryData = await prisma.$queryRaw`SELECT id from student`; + console.log({ rawQueryData }); + const student = await prisma.student.create({ + data: { + name: 'test', + age: '28', + }, + }).catch((e) => { + console.log(e) + }); + console.log(student); + + const students = await prisma.student.findMany(); + console.log(students); +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) + diff --git a/example/prisma/package-lock.json b/example/prisma/package-lock.json new file mode 100644 index 0000000..a7b346d --- /dev/null +++ b/example/prisma/package-lock.json @@ -0,0 +1,295 @@ +{ + "name": "stylusdb-example", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stylusdb-example", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.8.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "prisma": "^5.8.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.8.0.tgz", + "integrity": "sha512-QxO6C4MaA/ysTIbC+EcAH1aX/YkpymhXtO6zPdk+FvA7+59tNibIYpd+7koPdViLg2iKES4ojsxWNUGNJaEcbA==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.8.0.tgz", + "integrity": "sha512-ZqPpkvbovu/kQJ1bvy57NO4dw97fpQGcbQSCtsqlwSE1UNKJP75R3BKxdznk8ZPMY+GJdMRetWNv4oAvSbWn8Q==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.8.0.tgz", + "integrity": "sha512-Qhqm9WWLujNEC13AuZlUO14SQ15tNLe5puaz+tOk7UqINqJ3PtqMmuSuzomiw2diGVqZ+HYiSQzlR3+pPucVHA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.8.0", + "@prisma/engines-version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "@prisma/fetch-engine": "5.8.0", + "@prisma/get-platform": "5.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848.tgz", + "integrity": "sha512-cXcoVweYbnv8xRfkWq9oj8BECOdzHUazrSpYCa0ehp5TNz4l5Spa8jbq/VROCTzj3ZncH5D9Q2TmySYTOUeKlw==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.8.0.tgz", + "integrity": "sha512-1CAuE+JoYsPNggMEn6qk0zos06Uc9bYZBJ0VBPHD6R7REL05614koAbOCmn52IaYz3nobb7f25hqW6AY7rLkIw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.8.0", + "@prisma/engines-version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "@prisma/get-platform": "5.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.8.0.tgz", + "integrity": "sha512-Nk3rhTFZ1LYkFZJnpSvQcLPCaBWgJQfteHII6UEENOOkYlmP0k3FuswND54tzzEr4qs39wOdV9pbXKX9U2lv7A==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.8.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/prisma": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.8.0.tgz", + "integrity": "sha512-hDKoEqPt2qEUTH5yGO3l27CBnPtwvte0CGMKrpCr9+/A919JghfqJ3qgCGgMbOwdkXUOzdho0RH9tyUF3UhpMw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.8.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/example/prisma/package.json b/example/prisma/package.json new file mode 100644 index 0000000..d38b75b --- /dev/null +++ b/example/prisma/package.json @@ -0,0 +1,23 @@ +{ + "name": "stylusdb-example", + "version": "0.0.0", + "description": "A Node.js example of using StylusDB with all bells and whistles.", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prisma": "node index.js" + }, + "author": "Chakshu Gautam", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.11.0", + "prisma": "^5.8.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "dependencies": { + "@prisma/client": "^5.8.0", + "stylusdb-sql-prisma-adapter": "^0.0.1" + } +} \ No newline at end of file diff --git a/example/prisma/schema.prisma b/example/prisma/schema.prisma new file mode 100644 index 0000000..fe61546 --- /dev/null +++ b/example/prisma/schema.prisma @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = "postgresql://127.0.0.1:5432/database?sslaccept=strict" +} + +model Student { + id Int @id @default(autoincrement()) + name String + age String + enrollment Enrollment[] +} + +model Enrollment { + id Int @id @default(autoincrement()) + studentId Int + course String + student Student @relation(fields: [studentId], references: [id]) +} diff --git a/example/prisma/tsconfig.json b/example/prisma/tsconfig.json new file mode 100644 index 0000000..e075f97 --- /dev/null +++ b/example/prisma/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/example/socket/.gitignore b/example/socket/.gitignore new file mode 100644 index 0000000..656790b --- /dev/null +++ b/example/socket/.gitignore @@ -0,0 +1,133 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +**/.DS_Store \ No newline at end of file diff --git a/example/socket/client.js b/example/socket/client.js new file mode 100644 index 0000000..27127d9 --- /dev/null +++ b/example/socket/client.js @@ -0,0 +1,96 @@ +const net = require('net'); + +class Client { + socket = new net.Socket(); + isConnected = false; + isWaitingForResponse = false; + responseHandlers = {}; + + constructor(url) { + this.url = url; + } + + connect() { + this.setup(); + } + + disconnect() { + this.socket.end(); + } + + setup() { + this.socket.connect(this.url.split(":")[1], this.url.split(":")[0], () => { + console.log('Attempting to connect to the server...'); + }); + + let buffer = ''; + + this.socket.on('data', (data) => { + buffer += data.toString(); + let boundary = buffer.indexOf('\n'); + while (boundary !== -1) { + const message = buffer.substring(0, boundary).trim(); + buffer = buffer.substring(boundary + 1); + boundary = buffer.indexOf('\n'); + + this.handleResponse(message); + } + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.isConnected = false; + }); + + this.socket.on('error', (error) => { + console.error('Error:', error.message); + this.isConnected = false; + }); + } + + execute = (query) => { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error('Not connected to the server.')); + return; + } + if (this.isWaitingForResponse) { + reject(new Error('Waiting for the previous query to be processed.')); + return; + } + + const queryId = this.generateQueryId(); + this.responseHandlers[queryId] = { resolve, reject }; + this.socket.write(`${queryId}:${query}`); + this.isWaitingForResponse = true; + }); + } + + handleResponse(response) { + if (response === 'Connected') { + this.isConnected = true; + console.log('Successfully connected to the server.'); + return; + } + + // Handle query response + const [queryId, result] = response.split('<|>', 2); + console.log('Received from server:', queryId, result); + if (this.responseHandlers[queryId]) { + try { + const parsedResult = JSON.parse(result); + this.responseHandlers[queryId].resolve(parsedResult); + } catch (error) { + this.responseHandlers[queryId].reject(error); + } + delete this.responseHandlers[queryId]; + this.isWaitingForResponse = false; + } + } + + generateQueryId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} + +module.exports = { Client }; \ No newline at end of file diff --git a/example/socket/index.js b/example/socket/index.js new file mode 100644 index 0000000..c3b3817 --- /dev/null +++ b/example/socket/index.js @@ -0,0 +1,50 @@ +const net = require('net'); + +const client = new net.Socket(); +const port = 5432; +const host = '127.0.0.1'; + +client.connect(port, host, () => { + console.log('Attempting to connect to the server...'); +}); + +const queries = [ + 'SELECT id, name FROM student WHERE age = 25', + 'SELECT student.name, enrollment.course, student.age FROM student INNER JOIN enrollment ON student.id = enrollment.student_id WHERE student.age > 25', + 'SELECT FROM table', + 'SELECT DISTINCT student_id, course FROM enrollment' +]; + +client.on('data', (data) => { + const message = data.toString().trim(); + console.log('Received from server:', message); + + if (message === 'Connected') { + isConnected = true; + sendQueries(); + } +}); + +client.on('close', () => { + console.log('Connection closed'); + isConnected = false; +}); + +client.on('error', (error) => { + console.error('Error:', error.message); + isConnected = false; +}); + +function sendQueries() { + if (!isConnected) { + console.log('Not connected to the server.'); + return; + } + + queries.forEach((query, index) => { + setTimeout(() => { + console.log(`Sending: ${query}`); + client.write(query); + }, index * 1000); + }); +} \ No newline at end of file diff --git a/example/socket/index.v2.js b/example/socket/index.v2.js new file mode 100644 index 0000000..4b73750 --- /dev/null +++ b/example/socket/index.v2.js @@ -0,0 +1,19 @@ +// Import the Client class +const { Client } = require('./client'); + +const client = new Client('127.0.0.1:5432'); +client.connect(); + + +setTimeout(() => { + client.execute('SELECT id FROM student') + .then(response => { + console.log('Response:', response); + }) + .catch(error => { + console.error('Error:', error.message); + }) + .finally(() => { + client.disconnect(); + }); +}, 3000); \ No newline at end of file diff --git a/example/socket/package.json b/example/socket/package.json new file mode 100644 index 0000000..6fbefd0 --- /dev/null +++ b/example/socket/package.json @@ -0,0 +1,11 @@ +{ + "name": "socket-example", + "version": "1.0.0", + "description": "Socket Connection Example for StylusDB", + "main": "server.js", + "scripts": { + "test": "node index.v2.js" + }, + "author": "Chakshu Gautam", + "license": "ISC" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5a952d0..3afaec3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stylusdb-sql", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stylusdb-sql", - "version": "0.1.5", + "version": "0.1.6", "license": "ISC", "dependencies": { "csv-parser": "^3.0.0", diff --git a/package.json b/package.json index 334b1c7..4eb4c53 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "scripts": { "test": "jest", - "test-verbose": "jest --verbose" + "test-verbose": "jest --verbose", + "server": "node ./src/server.js" }, "bin": { "stylusdb-cli": "node ./src/cli.js" diff --git a/prisma/prisma-adapter/.github/workflows/ci.yml b/prisma/prisma-adapter/.github/workflows/ci.yml new file mode 100644 index 0000000..23da817 --- /dev/null +++ b/prisma/prisma-adapter/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + pull_request: + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 8.x.x + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: pnpm + + - name: install dependencies + run: pnpm install --frozen-lockfile + - name: lint + run: pnpm lint + - name: build + run: pnpm build \ No newline at end of file diff --git a/prisma/prisma-adapter/.github/workflows/publish.yml b/prisma/prisma-adapter/.github/workflows/publish.yml new file mode 100644 index 0000000..7b6839d --- /dev/null +++ b/prisma/prisma-adapter/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8.x.x + - uses: actions/setup-node@v3 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/prisma/prisma-adapter/.gitignore b/prisma/prisma-adapter/.gitignore new file mode 100644 index 0000000..ce5095a --- /dev/null +++ b/prisma/prisma-adapter/.gitignore @@ -0,0 +1,45 @@ +node_modules + +yarn-error.log +dist +build +tmp +pnpm-debug.log +sandbox +.DS_Store +.idea + +query-engine* +migration-engine* +schema-engine* +libquery_engine* +libquery-engine* +query_engine-windows.dll.node + +*tmp.db +dist/ +declaration/ + +*.tsbuildinfo +.prisma +.pnpm-store + +.vscode +!.vscode/launch.json.default +coverage + +.eslintcache + +.pnpm-debug.log + +.envrc + +esm +reproductions/* +!reproductions/basic-sqlite +!reproductions/tracing +!reproductions/pnpm-workspace.yaml + +dev.db +junit.xml +/output.txt \ No newline at end of file diff --git a/prisma/prisma-adapter/README.md b/prisma/prisma-adapter/README.md new file mode 100644 index 0000000..28f7f5d --- /dev/null +++ b/prisma/prisma-adapter/README.md @@ -0,0 +1,156 @@ +# @tidbcloud/prisma-adapter + +Prisma driver adapter for [TiDB Cloud Serverless Driver](https://github.com/tidbcloud/serverless-js). For more details, see [TiDB Cloud Serverless Driver Prisma Tutorial +](https://docs.pingcap.com/tidbcloud/serverless-driver-prisma-example). + +## Before you start + +Before you start, make sure you have: + +- A TiDB Cloud account +- Node >= 18 +- [Prisma CLI](https://www.prisma.io/docs/concepts/components/prisma-cli) installed + +## Install + +You will need to install the `@tidbcloud/prisma-adapter` driver adapter and the `@tidbcloud/serverless` serverless driver. + +``` +npm install @tidbcloud/prisma-adapter @tidbcloud/serverless +``` + +## DATABASE URL + +Set the environment to your .env file in the local environment. You can get connection information on the TiDB Cloud console. + +```env +// .env +DATABASE_URL="mysql://username:password@host:4000/database?sslaccept=strict" +``` + +> NOTE +> +> The adapter only supports Prisma Client. Prisma migration and introspection still go through the traditional TCP way. If you only need Prisma Client, you can set the DATABASE_URL as the `mysql://username:password@host/database` format which port and ssl parameters are not needed). + +## Define Prisma schema + +First, you need to create a Prisma schema file called schema.prisma and define the model. Here we use the user as an example. + +```prisma +// schema.prisma +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +// define model according to your database table +model user { + id Int @id @default(autoincrement()) + email String? @unique(map: "uniq_email") @db.VarChar(255) + name String? @db.VarChar(255) +} +``` + +## Query + +Here is an example of query: + +```js +// query.js +import { connect } from '@tidbcloud/serverless'; +import { PrismaTiDBCloud } from '@tidbcloud/prisma-adapter'; +import { PrismaClient } from '@prisma/client'; +import dotenv from 'dotenv'; + +// setup +dotenv.config(); +const connectionString = `${process.env.DATABASE_URL}`; + +// init prisma client +const connection = connect({ url: connectionString }); +const adapter = new PrismaTiDBCloud(connection); +const prisma = new PrismaClient({ adapter }); + +// insert +const user = await prisma.user.create({ + data: { + email: 'test@prisma.io', + name: 'test', + }, +}) +console.log(user) + +// query after insert +console.log(await prisma.user.findMany()) +``` + +## Transaction + +Here is an example of transaction: + +```js +// query.js +import { connect } from '@tidbcloud/serverless'; +import { PrismaTiDBCloud } from '@tidbcloud/prisma-adapter'; +import { PrismaClient } from '@prisma/client'; +import dotenv from 'dotenv'; + +// setup +dotenv.config(); +const connectionString = `${process.env.DATABASE_URL}`; + +// init prisma client +const connection = connect({ url: connectionString }); +const adapter = new PrismaTiDBCloud(connection); +const prisma = new PrismaClient({ adapter }); + +const createUser1 = prisma.user.create({ + data: { + email: 'yuhang.shi@pingcap.com', + name: 'Shi Yuhang', + }, +}) + +const createUser2 = prisma.user.create({ + data: { + email: 'yuhang.shi@pingcap.com', + name: 'Shi Yuhang2', + }, +}) + +const createUser3 = prisma.user.create({ + data: { + email: 'yuhang2.shi@pingcap.com', + name: 'Shi Yuhang2', + }, +}) +try { + await prisma.$transaction([createUser1, createUser2]) // Operations fail together +} catch (e) { + console.log(e) + await prisma.$transaction([createUser1, createUser3]) // Operations succeed together +} +``` + +## Choose a version + +| Adapter | Prisma/Prisma Client | serverless driver | +|---------|----------------------|-------------------| +| v5.4.x | v5.4.x | ^v0.0.6 | +| v5.5.x | v5.5.x | ^v0.0.7 | +| v5.6.x | v5.6.x | ^v0.0.7 | +| v5.7.x | v5.7.x | ^v0.0.7 | + +Here is the step to step guide for how to choose the version: +1. Choose the Prisma version: Choose the one as you need. +2. Choose the adapter version: If you are using Prisma vx.y.z, you should choose the same version of adapter. If there is no adapter version for your Prisma version, you can choose the latest adapter version in vx.y. Open an issue once you find the adapter version is not compatible with Prisma version. +3. Choose the serverless driver version: You can always use the latest version according to the table above. + +## Limitations + +- [Set isolation level](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#transaction-isolation-level) is not supported yet. diff --git a/prisma/prisma-adapter/dev_guide.md b/prisma/prisma-adapter/dev_guide.md new file mode 100644 index 0000000..470ef8a --- /dev/null +++ b/prisma/prisma-adapter/dev_guide.md @@ -0,0 +1,8 @@ +# Test + +Now, we test the adapter by this [pull request](https://github.com/prisma/prisma-engines/pull/4342) as a temporary solution. + +# Release + +Just publish a new GitHub release, then the GitHub Action will publish the package to npm. + diff --git a/prisma/prisma-adapter/package-lock.json b/prisma/prisma-adapter/package-lock.json new file mode 100644 index 0000000..43ff94b --- /dev/null +++ b/prisma/prisma-adapter/package-lock.json @@ -0,0 +1,1894 @@ +{ + "name": "@tidbcloud/prisma-adapter", + "version": "5.8.0-dev.15", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@tidbcloud/prisma-adapter", + "version": "5.8.0-dev.15", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "5.8.0-dev.15" + }, + "devDependencies": { + "@tidbcloud/serverless": "^0.0.9", + "@types/node": "^20.5.1", + "tsup": "^7.2.0", + "tsx": "^3.12.7", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "@tidbcloud/serverless": "^0.0.9" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "5.8.0-dev.15", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-5.8.0-dev.15.tgz", + "integrity": "sha512-H4+QctoR4fbcPrkgwNxCTztzqLm0teWSQ9zMgxUSOizdckfmIakpSNJK7mXZTXPYJWtbyzZS8fr1VVrMBqC7bA==", + "dependencies": { + "debug": "4.3.4" + } + }, + "node_modules/@tidbcloud/serverless": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@tidbcloud/serverless/-/serverless-0.0.9.tgz", + "integrity": "sha512-kymM6nkBLQxUNUN75ESISjsFERd6Bd3L/Uxa1WeMNWpRnlVKcGGCGS6c//wedZvAYgciJaunvBzKL3/drHn8/Q==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-require": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", + "integrity": "sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsup": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-7.2.0.tgz", + "integrity": "sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==", + "dev": true, + "dependencies": { + "bundle-require": "^4.0.0", + "cac": "^6.7.12", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.18.2", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^4.0.1", + "resolve-from": "^5.0.0", + "rollup": "^3.2.5", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.14.0.tgz", + "integrity": "sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==", + "dev": true, + "dependencies": { + "esbuild": "~0.18.20", + "get-tsconfig": "^4.7.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/prisma/prisma-adapter/package.json b/prisma/prisma-adapter/package.json new file mode 100644 index 0000000..bc5b8cb --- /dev/null +++ b/prisma/prisma-adapter/package.json @@ -0,0 +1,37 @@ +{ + "name": "stylusdb-sql-prisma-adapter", + "version": "0.0.1", + "description": "Prisma's driver adapter for StylusDB SQL", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsup ./src/index.ts --format cjs,esm --dts", + "lint": "tsc -p ./tsconfig.build.json" + }, + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "StylusDB", + "SQL", + "Prisma" + ], + "author": "Chakshu Gautam", + "license": "Apache-2.0", + "sideEffects": false, + "dependencies": { + "@prisma/driver-adapter-utils": "5.8.0-dev.15" + }, + "devDependencies": { + "@tidbcloud/serverless": "^0.0.9", + "@types/node": "^20.5.1", + "tsup": "^7.2.0", + "tsx": "^3.12.7", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "@tidbcloud/serverless": "^0.0.9" + } +} \ No newline at end of file diff --git a/prisma/prisma-adapter/src/index.ts b/prisma/prisma-adapter/src/index.ts new file mode 100644 index 0000000..0f9781b --- /dev/null +++ b/prisma/prisma-adapter/src/index.ts @@ -0,0 +1,143 @@ +import { ConnectionInfo, Debug, ok, Queryable, Query, Result, ResultSet, Transaction, TransactionOptions, ColumnType, ColumnTypeEnum, DriverAdapter } from '@prisma/driver-adapter-utils' +import { adaptInsertQuery, convertToPrismaResultSetINSERTQuery, convertToPrismaResultSetSELECTQuery, evaluateQuery } from './queryUtils' +const debug = Debug('prisma:driver-adapter:stylusdb') + +const defaultDatabase = 'test' + +class RollbackError extends Error { + constructor() { + super('ROLLBACK') + this.name = 'RollbackError' + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, RollbackError) + } + } +} + +class StylusDBSQLQueryable implements Queryable { + readonly provider = 'postgres' + constructor(protected client: any) {} + + /** + * Execute a query given as SQL, interpolating the given parameters. + */ + async queryRaw(query: Query): Promise> { + debug('PrismaStylusDBSQL: queryRaw Called'); + const tag = '[js::query_raw]' + + const queries = adaptInsertQuery(query) + let result; + + debug('PrismaStylusDBSQL: queryRaw queries: %O', queries); + + for (const query of queries) { + debug(`${tag} %O`, query) + const fixedQuery = adaptInsertQuery({sql: query, args: []})[0] //single query so this works + result = await this.performIO({sql: fixedQuery, args: []}) + } + + debug('PrismaStylusDBSQL: queryRaw result: %O', result); + + let resultSet: ResultSet; + if(query.sql.includes('INSERT INTO')) { + resultSet = convertToPrismaResultSetINSERTQuery(result); + }else { + resultSet = convertToPrismaResultSetSELECTQuery(result); + } + + return ok(resultSet) + } + + /** + * Execute a query given as SQL, interpolating the given parameters and + * returning the number of affected rows. + * Note: Queryable expects a u64, but napi.rs only supports u32. + */ + async executeRaw(query: Query): Promise> { + const tag = '[js::execute_raw]' + debug(`${tag} %O`, query) + + const result = await this.performIO(query) + const rowsAffected = result.rowsAffected as number + return ok(rowsAffected) + } + + /** + * Run a query against the database, returning the result set. + * Should the query fail due to a connection error, the connection is + * marked as unhealthy. + */ + private async performIO(query: Query) { + const { sql, args: values } = query + try { + const result = await this.client.execute(sql) + return result; + } catch (e) { + const error = e as Error + debug('Error in performIO: %O', error) + throw error + } + } +} + +class StylusDBSQLTransaction extends StylusDBSQLQueryable implements Transaction { + finished = false + + constructor( + tx: any, + readonly options: TransactionOptions, + ) { + super(tx) + } + + async commit(): Promise> { + debug(`[js::commit]`) + + this.finished = true + await this.client.commit() + return Promise.resolve(ok(undefined)) + } + + async rollback(): Promise> { + debug(`[js::rollback]`) + + this.finished = true + await this.client.rollback() + return Promise.resolve(ok(undefined)) + } + + dispose(): Result { + if (!this.finished) { + this.rollback().catch(console.error) + } + return ok(undefined) + } +} + +export class PrismaStylusDBSQL extends StylusDBSQLTransaction implements DriverAdapter { + constructor(client: any, options: any) { + super(client, options) + + debug('PrismaStylusDBSQL: Client Constructor Called'); + } + + getConnectionInfo(): Result { + debug('PrismaStylusDBSQL: getConnectionInfo Called'); + return ok({ + schemaName: undefined, + }) + } + + async startTransaction() { + const options: TransactionOptions = { + usePhantomQuery: true, + } + + const tag = '[js::startTransaction]' + debug(`${tag} options: %O`, options) + + const tx = await this.client.begin() + return ok(new StylusDBSQLTransaction(tx, options)) + } +} diff --git a/prisma/prisma-adapter/src/queryUtils.ts b/prisma/prisma-adapter/src/queryUtils.ts new file mode 100644 index 0000000..1767e31 --- /dev/null +++ b/prisma/prisma-adapter/src/queryUtils.ts @@ -0,0 +1,103 @@ +import { ColumnTypeEnum, Query } from "@prisma/driver-adapter-utils"; + +function adaptInsertQuery(query: Query): string [] { + // Check if it's an INSERT query + if (!query.sql.startsWith('INSERT INTO')) { + // Remove schema ("public".) from the query + let simplifiedQuery = query.sql + .replace(/"public"\./g, '') + .replace(/\sWHERE\s1=1/g, '') + .replace(/\sOFFSET\s'0'/g, '') + .replace(/\sOFFSET\s0/g, '') + .replace(/"([^"]+)"/g, '$1') + .replace(/"?\b\w+\b"?\."?(\w+)"?/g, '$1') + .replace(/\bFROM\s+(\w+)/, (match, p1) => `FROM ${p1.toLowerCase()}`); + return [evaluateQuery({sql: simplifiedQuery, args: query.args})]; + } + + // Remove schema ("public".) from the query + let simplifiedQuery = query.sql.replace(/"public"\./g, ''); + + // Extract the table name + const tableName = simplifiedQuery.split(' ')[2].replace(/"/g, ''); + + // Split the query into INSERT and SELECT parts + const insertPart = simplifiedQuery; + + return [evaluateQuery({sql: insertPart, args: query.args})] +} + +function convertToPrismaResultSetSELECTQuery(dbResponse) { + // Ensure the response is in array format even if it's a single record + const records = Array.isArray(dbResponse) ? dbResponse : [dbResponse]; + + // Extract column names from the first record (assuming all records have the same structure) + const columnNames = records.length > 0 ? Object.keys(records[0]) : []; + + // Assign 'Number' type to 'id' column, and 'String' to all other columns + const columnTypes = columnNames.map(columnName => columnName === 'id' ? ColumnTypeEnum.Int32 : ColumnTypeEnum.Text); + + // Convert each record into an array of values + const rows = records.map(record => + columnNames.map(columnName => + columnName === 'id' ? Number(record[columnName]) : record[columnName] + ) + ); + + // Construct the ResultSet + const resultSet = { + columnNames, + columnTypes, + rows, + lastInsertId: undefined // Set this appropriately if your DB returns last insert ID + }; + + return resultSet; +} + +function convertToPrismaResultSetINSERTQuery(dbResponse) { + const { message, insertedId, returning } = dbResponse; + + // If no data is returned (i.e., no RETURNING clause), create an empty result set + if (!returning || Object.keys(returning).length === 0) { + return { + columnNames: [], + columnTypes: [], + rows: [], + lastInsertId: insertedId ? insertedId.toString() : undefined + }; + } + + // Extract column names and values from the returning object + const columnNames = Object.keys(returning); + const values = Object.values(returning); + + // Assign 'Number' type to 'id' column, and 'String' to all other columns + const columnTypes = columnNames.map(columnName => columnName === 'id' ? ColumnTypeEnum.Int32 : ColumnTypeEnum.Text); + + // The rows array will contain only one row (the inserted row) + const rows = [values]; + + // Construct the ResultSet + const resultSet = { + columnNames, + columnTypes, + rows, + lastInsertId: insertedId ? insertedId.toString() : undefined + }; + + return resultSet; +} + +function evaluateQuery(queryObject: Query) { + let { sql, args } = queryObject; + + // Replace placeholders with the corresponding values from args + args.forEach((arg, index) => { + sql = sql.replace(`$${index + 1}`, `'${arg}'`); + }); + + return sql; +} + +export { adaptInsertQuery, convertToPrismaResultSetSELECTQuery, convertToPrismaResultSetINSERTQuery, evaluateQuery } \ No newline at end of file diff --git a/prisma/prisma-adapter/tsconfig.build.json b/prisma/prisma-adapter/tsconfig.build.json new file mode 100644 index 0000000..07a3a34 --- /dev/null +++ b/prisma/prisma-adapter/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "declaration" + } +} diff --git a/prisma/prisma-adapter/tsconfig.json b/prisma/prisma-adapter/tsconfig.json new file mode 100644 index 0000000..516c114 --- /dev/null +++ b/prisma/prisma-adapter/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "Bundler", + "esModuleInterop": false, + "isolatedModules": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": false, + "noUncheckedIndexedAccess": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "useUnknownInCatchVariables": false, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "emitDeclarationOnly": true, + "resolveJsonModule": true + }, + "exclude": ["**/dist", "**/declaration", "**/node_modules", "**/src/__tests__"] +} \ No newline at end of file diff --git a/src/queryExecuter.js b/src/queryExecuter.js index 43ba919..ce210a3 100644 --- a/src/queryExecuter.js +++ b/src/queryExecuter.js @@ -324,30 +324,58 @@ async function executeSELECTQuery(query) { } async function executeINSERTQuery(query) { - console.log(parseInsertQuery(query)); - const { table, columns, values } = parseInsertQuery(query); + const { table, columns, values, returningColumns } = parseInsertQuery(query); const data = await readCSV(`${table}.csv`); - // Create a new row object + // Check if 'id' column is included in the query and in CSV headers + let newId = null; + if (!columns.includes('id') && data.length > 0 && 'id' in data[0]) { + // 'id' column not included in the query, so we auto-generate an ID + const existingIds = data.map(row => parseInt(row.id)).filter(id => !isNaN(id)); + const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0; + newId = maxId + 1; + columns.push('id'); + values.push(newId.toString()); // Add as a string + } + + // Create a new row object matching the CSV structure + const headers = data.length > 0 ? Object.keys(data[0]) : columns; const newRow = {}; - columns.forEach((column, index) => { - // Remove single quotes from the values - let value = values[index]; - if (value.startsWith("'") && value.endsWith("'")) { - value = value.substring(1, value.length - 1); + headers.forEach(header => { + const columnIndex = columns.indexOf(header); + if (columnIndex !== -1) { + let value = values[columnIndex]; + if (value.startsWith("'") && value.endsWith("'")) { + value = value.substring(1, value.length - 1); + } + newRow[header] = value; + } else { + newRow[header] = header === 'id' ? newId.toString() : ''; } - newRow[column] = value; }); // Add the new row to the data data.push(newRow); // Save the updated data back to the CSV file - await writeCSV(`${table}.csv`, data); // Implement writeCSV function + await writeCSV(`${table}.csv`, data); - return { message: "Row inserted successfully." }; + // Prepare the returning result if returningColumns are specified + let returningResult = {}; + if (returningColumns.length > 0) { + returningColumns.forEach(column => { + returningResult[column] = newRow[column]; + }); + } + + return { + message: "Row inserted successfully.", + insertedId: newId, + returning: returningResult + }; } + async function executeDELETEQuery(query) { const { table, whereClauses } = parseDeleteQuery(query); let data = await readCSV(`${table}.csv`); diff --git a/src/queryParser.js b/src/queryParser.js index cde1b89..ccc539a 100644 --- a/src/queryParser.js +++ b/src/queryParser.js @@ -138,20 +138,90 @@ function parseJoinClause(query) { }; } +// function parseInsertQuery(query) { +// // Remove schema names +// let simplifiedQuery = query.replace(/"?\w+"?\."(\w+)"?/g, '$1'); + +// console.log({ simplifiedQuery }); + +// // Parse the INSERT INTO part +// const insertRegex = /INSERT INTO "?(\w+)"?\s\(([^)]+)\)\sVALUES\s\(([^)]+)\)/i; +// const match = simplifiedQuery.match(insertRegex); + +// if (!match) { +// throw new Error("Invalid INSERT INTO syntax."); +// } + +// const [, table, columns, values] = match; + +// // Function to clean and remove quotes from a string +// const cleanValue = (value) => { +// return value.trim().replace(/^"?(.*?)"?$/g, '$1'); +// }; + +// // Parse and clean columns and values +// const parsedColumns = columns.split(',').map(cleanValue); +// const parsedValues = values.split(',').map(cleanValue); + +// // Parse the RETURNING part if present +// const returningMatch = simplifiedQuery.match(/RETURNING\s(.+)$/i); +// const returningColumns = returningMatch +// ? returningMatch[1].split(',').map(cleanValue) +// : []; + +// return { +// type: 'INSERT', +// table: cleanValue(table), +// columns: parsedColumns, +// values: parsedValues, +// returningColumns +// }; +// } + function parseInsertQuery(query) { - const insertRegex = /INSERT INTO (\w+)\s\((.+)\)\sVALUES\s\((.+)\)/i; - const match = query.match(insertRegex); + // Simplify the query by removing schema names and table references from column names + let simplifiedQuery = query.replace(/"?\w+"?\."(\w+)"?/g, '$1'); + + // Parse the INSERT INTO part + const insertRegex = /INSERT INTO "?(\w+)"?\s\(([^)]+)\)\sVALUES\s\(([^)]+)\)/i; + const match = simplifiedQuery.match(insertRegex); if (!match) { throw new Error("Invalid INSERT INTO syntax."); } const [, table, columns, values] = match; + + // Function to clean and remove surrounding quotes from column names + const cleanColumnName = (name) => { + return name.trim().replace(/^"?(.+?)"?$/g, '$1'); + }; + + // Function to clean and remove surrounding single quotes from values + const cleanValue = (value) => { + return value.trim().replace(/^'(.*)'$/g, '$1').replace(/^"(.*)"$/g, '$1'); + }; + + // Function to clean returning column names by removing table prefixes and quotes + const cleanReturningColumn = (name) => { + return name.trim().replace(/\w+\./g, '').replace(/^"?(.+?)"?$/g, '$1'); + }; + + // Parse and clean columns and values + const parsedColumns = columns.split(',').map(cleanColumnName); + const parsedValues = values.split(',').map(cleanValue); + + // Parse the RETURNING part if present + const returningMatch = simplifiedQuery.match(/RETURNING\s(.+)$/i); + const returningColumns = returningMatch + ? returningMatch[1].split(',').map(cleanReturningColumn) + : []; return { type: 'INSERT', - table: table.trim(), - columns: columns.split(',').map(column => column.trim()), - values: values.split(',').map(value => value.trim()) + table: cleanColumnName(table), + columns: parsedColumns, + values: parsedValues, + returningColumns }; } diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..f7ce998 --- /dev/null +++ b/src/server.js @@ -0,0 +1,162 @@ +// const net = require('net'); +// const { EventEmitter } = require('events'); +// const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('./queryExecuter'); + +// class QueryQueue extends EventEmitter { +// constructor() { +// super(); +// this.queue = []; +// this.isProcessing = false; +// } + +// addQuery(query, callback) { +// const queryId = Date.now() + Math.random().toString(16); +// this.queue.push({ query, callback, queryId }); +// this.emit('newQuery'); +// return queryId; +// } + +// processQueue() { +// if (this.isProcessing || this.queue.length === 0) { +// return; +// } +// this.isProcessing = true; +// const { query, callback, queryId } = this.queue.shift(); +// this.execute(query) +// .then(result => callback(null, { queryId, result })) +// .catch(error => callback(error, { queryId })) +// .finally(() => { +// this.isProcessing = false; +// this.processQueue(); +// }); +// } + +// async execute(query) { +// if (query.toLowerCase().startsWith('select')) { +// return await executeSELECTQuery(query); +// } else if (query.toLowerCase().startsWith('insert into')) { +// return await executeINSERTQuery(query); +// } else if (query.toLowerCase().startsWith('delete from')) { +// return await executeDELETEQuery(query); +// } else { +// throw new Error('Unsupported command'); +// } +// } +// } + +// const queryQueue = new QueryQueue(); +// queryQueue.on('newQuery', () => queryQueue.processQueue()); + +// const server = net.createServer(); +// let activeConnection = false; + +// server.on('connection', (socket) => { +// if (activeConnection) { +// socket.end('Another connection is already active.'); +// return; +// } +// activeConnection = true; + +// socket.write('Connected\n'); + +// socket.on('data', (data) => { +// const query = data.toString().trim(); +// const queryId = queryQueue.addQuery(query, (error, response) => { +// if (error) { +// socket.write(`Error: ${error.message}\n`); +// } else { +// socket.write(`Result for Query ID ${response.queryId}: ${JSON.stringify(response.result)}\n`); +// } +// }); +// socket.write(`Query received with ID: ${queryId}\n`); +// }); + +// socket.on('close', () => { +// activeConnection = false; +// }); +// }); + +// server.listen(5432, () => { +// console.log('Server listening on port 5432'); +// }); + +const net = require('net'); +const { EventEmitter } = require('events'); +const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('./queryExecuter'); + +class QueryQueue extends EventEmitter { + constructor() { + super(); + this.queue = []; + this.isProcessing = false; + } + + addQuery(queryId, query, callback) { + this.queue.push({ query, callback, queryId }); + this.emit('newQuery'); + } + + processQueue() { + if (this.isProcessing || this.queue.length === 0) { + return; + } + this.isProcessing = true; + const { query, callback, queryId } = this.queue.shift(); + this.execute(query) + .then(result => callback(null, queryId, result)) + .catch(error => callback(error, queryId)) + .finally(() => { + this.isProcessing = false; + this.processQueue(); + }); + } + + async execute(query) { + if (query.toLowerCase().startsWith('select')) { + return await executeSELECTQuery(query); + } else if (query.toLowerCase().startsWith('insert into')) { + return await executeINSERTQuery(query); + } else if (query.toLowerCase().startsWith('delete from')) { + return await executeDELETEQuery(query); + } else { + throw new Error('Unsupported command'); + } + } +} + +const queryQueue = new QueryQueue(); +queryQueue.on('newQuery', () => queryQueue.processQueue()); + +const server = net.createServer(); +let activeConnection = false; + +server.on('connection', (socket) => { + if (activeConnection) { + socket.end('Another connection is already active.'); + return; + } + activeConnection = true; + + socket.write('Connected\n'); + + socket.on('data', (data) => { + const [queryId, query] = data.toString().trim().split(':', 2); + queryQueue.addQuery(queryId, query, (error, queryId, result) => { + let response; + if (error) { + response = `${queryId}<|>Error: ${error.message}`; + } else { + response = `${queryId}<|>${JSON.stringify(result)}`; + } + socket.write(response + '\n'); + }); + }); + + socket.on('close', () => { + activeConnection = false; + }); +}); + +server.listen(5432, () => { + console.log('Server listening on port 5432'); +}); diff --git a/student.csv b/student.csv index 0e50ccf..ea771c9 100644 --- a/student.csv +++ b/student.csv @@ -1,6 +1,6 @@ -id,name,age -1,John,30 -2,Jane,25 -3,Bob,22 -4,Alice,24 -5,Jane,22 \ No newline at end of file +"id","name","age" +"1","John","30" +"2","Jane","25" +"3","Bob","22" +"4","Alice","24" +"5","Jane","22" \ No newline at end of file diff --git a/tests/insertExecuter.test.js b/tests/insertExecuter.test.js index 762412d..4d7ebba 100644 --- a/tests/insertExecuter.test.js +++ b/tests/insertExecuter.test.js @@ -1,6 +1,7 @@ const { executeINSERTQuery } = require('../src/queryExecuter'); const { readCSV, writeCSV } = require('../src/csvStorage'); const fs = require('fs'); +const exp = require('constants'); // Helper function to create grades.csv with initial data async function createGradesCSV() { @@ -18,16 +19,84 @@ test('Execute INSERT INTO Query for grades.csv', async () => { await createGradesCSV(); // Execute INSERT statement - const insertQuery = "INSERT INTO grades (student_id, course, grade) VALUES ('4', 'Physics', 'A')"; + const insertQuery = `INSERT INTO grades (student_id, course, grade) VALUES ("4", "Physics", "A")`; await executeINSERTQuery(insertQuery); // Verify the new entry const updatedData = await readCSV('grades.csv'); const newEntry = updatedData.find(row => row.student_id === '4' && row.course === 'Physics'); - console.log(updatedData) expect(newEntry).toBeDefined(); expect(newEntry.grade).toEqual('A'); // Cleanup: Delete grades.csv fs.unlinkSync('grades.csv'); -}); \ No newline at end of file +}); + +// Helper function to create teachers.csv with initial data +async function createTeachersCSV() { + const initialData = [ + { id: '1', name: 'Mr. Smith', subject: 'Mathematics' }, + { id: '2', name: 'Ms. Johnson', subject: 'Chemistry' } + ]; + await writeCSV('teachers.csv', initialData); +} + +// Test to INSERT a new teacher and verify +test('Execute INSERT INTO Query for teachers with autogenerated id', async () => { + // Create teachers.csv with initial data + await createTeachersCSV(); + + // Execute INSERT statement + const insertQuery = `INSERT INTO teachers (name, subject) VALUES ("Dr. Brown", "Physics")`; + await executeINSERTQuery(insertQuery); + + // Verify the new entry + const updatedData = await readCSV('teachers.csv'); + const newEntry = updatedData.find(row => row.id === '3' && row.name === 'Dr. Brown'); + expect(newEntry).toBeDefined(); + expect(newEntry.subject).toEqual('Physics'); + + const query = `INSERT INTO "public"."teachers" ("name","subject") VALUES ("test", "maths") RETURNING "public"."teachers"."id", "public"."teachers"."name", "public"."teachers"."subject"`; + const returningData = await executeINSERTQuery(query); + const updatedData2 = await readCSV('teachers.csv'); + const newEntry2 = updatedData2.find(row => row.id === '4' && row.name === 'test'); + expect(newEntry2).toBeDefined(); + expect(newEntry2.subject).toEqual('maths'); + + expect(returningData).toEqual({ + message: 'Row inserted successfully.', + insertedId: 4, + returning: { id: '4', name: 'test', subject: 'maths' } + }); + + // Cleanup: Delete teachers.csv + fs.unlinkSync('teachers.csv'); +}); + + +// Helper function to create Student.csv with initial data +async function createUserCSV() { + const initialData = [ + { id: '1', name: 'John', age: '30' }, + { id: '2', name: 'Jane', age: '25' } + ]; + await writeCSV('user.csv', initialData); +} + +test('Execute INSERT INTO Query for users with partial columns', async () => { + // Create Student.csv with initial data + await createUserCSV(); + + // Execute INSERT statement + await executeINSERTQuery(`INSERT INTO "User" ("name") VALUES ("test")`); + + // Read the updated CSV content + const updatedData = await readCSV('user.csv'); + + // Check if the new entry is added correctly + const isEntryAdded = updatedData.some(row => row.name === 'test'); + expect(isEntryAdded).toBe(true); + + // Clean up: delete Student.csv + fs.unlinkSync('user.csv'); +}); diff --git a/demo/server/enrollment.csv b/web/server/enrollment.csv similarity index 100% rename from demo/server/enrollment.csv rename to web/server/enrollment.csv diff --git a/demo/server/index.js b/web/server/index.js similarity index 100% rename from demo/server/index.js rename to web/server/index.js diff --git a/demo/server/package-lock.json b/web/server/package-lock.json similarity index 100% rename from demo/server/package-lock.json rename to web/server/package-lock.json diff --git a/demo/server/package.json b/web/server/package.json similarity index 100% rename from demo/server/package.json rename to web/server/package.json diff --git a/demo/server/student.csv b/web/server/student.csv similarity index 100% rename from demo/server/student.csv rename to web/server/student.csv diff --git a/demo/www/frontend.js b/web/www/frontend.js similarity index 100% rename from demo/www/frontend.js rename to web/www/frontend.js diff --git a/demo/www/index.html b/web/www/index.html similarity index 100% rename from demo/www/index.html rename to web/www/index.html diff --git a/demo/www/xterm.css b/web/www/xterm.css similarity index 100% rename from demo/www/xterm.css rename to web/www/xterm.css diff --git a/demo/www/xterm.js b/web/www/xterm.js similarity index 100% rename from demo/www/xterm.js rename to web/www/xterm.js From 16e4ab3c1d883ae7eeaa126260c28094a29a3504 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Mon, 15 Jan 2024 10:55:27 +0530 Subject: [PATCH 02/16] Chores: Remove redundant code --- example/socket/index.js | 69 +++++++++----------------------- example/socket/index.v2.js | 19 --------- src/queryParser.js | 40 ------------------- src/server.js | 82 -------------------------------------- 4 files changed, 19 insertions(+), 191 deletions(-) delete mode 100644 example/socket/index.v2.js diff --git a/example/socket/index.js b/example/socket/index.js index c3b3817..4b73750 100644 --- a/example/socket/index.js +++ b/example/socket/index.js @@ -1,50 +1,19 @@ -const net = require('net'); - -const client = new net.Socket(); -const port = 5432; -const host = '127.0.0.1'; - -client.connect(port, host, () => { - console.log('Attempting to connect to the server...'); -}); - -const queries = [ - 'SELECT id, name FROM student WHERE age = 25', - 'SELECT student.name, enrollment.course, student.age FROM student INNER JOIN enrollment ON student.id = enrollment.student_id WHERE student.age > 25', - 'SELECT FROM table', - 'SELECT DISTINCT student_id, course FROM enrollment' -]; - -client.on('data', (data) => { - const message = data.toString().trim(); - console.log('Received from server:', message); - - if (message === 'Connected') { - isConnected = true; - sendQueries(); - } -}); - -client.on('close', () => { - console.log('Connection closed'); - isConnected = false; -}); - -client.on('error', (error) => { - console.error('Error:', error.message); - isConnected = false; -}); - -function sendQueries() { - if (!isConnected) { - console.log('Not connected to the server.'); - return; - } - - queries.forEach((query, index) => { - setTimeout(() => { - console.log(`Sending: ${query}`); - client.write(query); - }, index * 1000); - }); -} \ No newline at end of file +// Import the Client class +const { Client } = require('./client'); + +const client = new Client('127.0.0.1:5432'); +client.connect(); + + +setTimeout(() => { + client.execute('SELECT id FROM student') + .then(response => { + console.log('Response:', response); + }) + .catch(error => { + console.error('Error:', error.message); + }) + .finally(() => { + client.disconnect(); + }); +}, 3000); \ No newline at end of file diff --git a/example/socket/index.v2.js b/example/socket/index.v2.js deleted file mode 100644 index 4b73750..0000000 --- a/example/socket/index.v2.js +++ /dev/null @@ -1,19 +0,0 @@ -// Import the Client class -const { Client } = require('./client'); - -const client = new Client('127.0.0.1:5432'); -client.connect(); - - -setTimeout(() => { - client.execute('SELECT id FROM student') - .then(response => { - console.log('Response:', response); - }) - .catch(error => { - console.error('Error:', error.message); - }) - .finally(() => { - client.disconnect(); - }); -}, 3000); \ No newline at end of file diff --git a/src/queryParser.js b/src/queryParser.js index ccc539a..4230a15 100644 --- a/src/queryParser.js +++ b/src/queryParser.js @@ -138,46 +138,6 @@ function parseJoinClause(query) { }; } -// function parseInsertQuery(query) { -// // Remove schema names -// let simplifiedQuery = query.replace(/"?\w+"?\."(\w+)"?/g, '$1'); - -// console.log({ simplifiedQuery }); - -// // Parse the INSERT INTO part -// const insertRegex = /INSERT INTO "?(\w+)"?\s\(([^)]+)\)\sVALUES\s\(([^)]+)\)/i; -// const match = simplifiedQuery.match(insertRegex); - -// if (!match) { -// throw new Error("Invalid INSERT INTO syntax."); -// } - -// const [, table, columns, values] = match; - -// // Function to clean and remove quotes from a string -// const cleanValue = (value) => { -// return value.trim().replace(/^"?(.*?)"?$/g, '$1'); -// }; - -// // Parse and clean columns and values -// const parsedColumns = columns.split(',').map(cleanValue); -// const parsedValues = values.split(',').map(cleanValue); - -// // Parse the RETURNING part if present -// const returningMatch = simplifiedQuery.match(/RETURNING\s(.+)$/i); -// const returningColumns = returningMatch -// ? returningMatch[1].split(',').map(cleanValue) -// : []; - -// return { -// type: 'INSERT', -// table: cleanValue(table), -// columns: parsedColumns, -// values: parsedValues, -// returningColumns -// }; -// } - function parseInsertQuery(query) { // Simplify the query by removing schema names and table references from column names let simplifiedQuery = query.replace(/"?\w+"?\."(\w+)"?/g, '$1'); diff --git a/src/server.js b/src/server.js index f7ce998..3e82d7f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,85 +1,3 @@ -// const net = require('net'); -// const { EventEmitter } = require('events'); -// const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('./queryExecuter'); - -// class QueryQueue extends EventEmitter { -// constructor() { -// super(); -// this.queue = []; -// this.isProcessing = false; -// } - -// addQuery(query, callback) { -// const queryId = Date.now() + Math.random().toString(16); -// this.queue.push({ query, callback, queryId }); -// this.emit('newQuery'); -// return queryId; -// } - -// processQueue() { -// if (this.isProcessing || this.queue.length === 0) { -// return; -// } -// this.isProcessing = true; -// const { query, callback, queryId } = this.queue.shift(); -// this.execute(query) -// .then(result => callback(null, { queryId, result })) -// .catch(error => callback(error, { queryId })) -// .finally(() => { -// this.isProcessing = false; -// this.processQueue(); -// }); -// } - -// async execute(query) { -// if (query.toLowerCase().startsWith('select')) { -// return await executeSELECTQuery(query); -// } else if (query.toLowerCase().startsWith('insert into')) { -// return await executeINSERTQuery(query); -// } else if (query.toLowerCase().startsWith('delete from')) { -// return await executeDELETEQuery(query); -// } else { -// throw new Error('Unsupported command'); -// } -// } -// } - -// const queryQueue = new QueryQueue(); -// queryQueue.on('newQuery', () => queryQueue.processQueue()); - -// const server = net.createServer(); -// let activeConnection = false; - -// server.on('connection', (socket) => { -// if (activeConnection) { -// socket.end('Another connection is already active.'); -// return; -// } -// activeConnection = true; - -// socket.write('Connected\n'); - -// socket.on('data', (data) => { -// const query = data.toString().trim(); -// const queryId = queryQueue.addQuery(query, (error, response) => { -// if (error) { -// socket.write(`Error: ${error.message}\n`); -// } else { -// socket.write(`Result for Query ID ${response.queryId}: ${JSON.stringify(response.result)}\n`); -// } -// }); -// socket.write(`Query received with ID: ${queryId}\n`); -// }); - -// socket.on('close', () => { -// activeConnection = false; -// }); -// }); - -// server.listen(5432, () => { -// console.log('Server listening on port 5432'); -// }); - const net = require('net'); const { EventEmitter } = require('events'); const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('./queryExecuter'); From 60c035b09a31bcbe3c1e8c47ea40a58b46877f22 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Mon, 15 Jan 2024 16:19:36 +0530 Subject: [PATCH 03/16] Chores: Readme for Prisma Adapter; GitHub CI Refactoring --- .github/workflows/tests.yml | 25 --- example/prisma/index.js | 1 - example/socket/package.json | 2 +- .../prisma-adapter/.github/workflows/ci.yml | 27 ---- .../.github/workflows/publish.yml | 26 ---- prisma/prisma-adapter/README.md | 143 +++++++----------- 6 files changed, 56 insertions(+), 168 deletions(-) delete mode 100644 .github/workflows/tests.yml delete mode 100644 prisma/prisma-adapter/.github/workflows/ci.yml delete mode 100644 prisma/prisma-adapter/.github/workflows/publish.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 3e5e185..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Run Tests - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x, 16.x, 18.x] - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - run: npm i - - run: npm test diff --git a/example/prisma/index.js b/example/prisma/index.js index 38222f2..86214e6 100644 --- a/example/prisma/index.js +++ b/example/prisma/index.js @@ -1,6 +1,5 @@ import { PrismaClient } from '@prisma/client'; import { PrismaStylusDBSQL } from 'stylusdb-sql-prisma-adapter'; -// const net = require('net'); import net from 'net'; diff --git a/example/socket/package.json b/example/socket/package.json index 6fbefd0..de7b413 100644 --- a/example/socket/package.json +++ b/example/socket/package.json @@ -4,7 +4,7 @@ "description": "Socket Connection Example for StylusDB", "main": "server.js", "scripts": { - "test": "node index.v2.js" + "test": "node index.js" }, "author": "Chakshu Gautam", "license": "ISC" diff --git a/prisma/prisma-adapter/.github/workflows/ci.yml b/prisma/prisma-adapter/.github/workflows/ci.yml deleted file mode 100644 index 23da817..0000000 --- a/prisma/prisma-adapter/.github/workflows/ci.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: ci - -on: - pull_request: - workflow_dispatch: - -jobs: - ci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v2 - with: - version: 8.x.x - - - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: pnpm - - - name: install dependencies - run: pnpm install --frozen-lockfile - - name: lint - run: pnpm lint - - name: build - run: pnpm build \ No newline at end of file diff --git a/prisma/prisma-adapter/.github/workflows/publish.yml b/prisma/prisma-adapter/.github/workflows/publish.yml deleted file mode 100644 index 7b6839d..0000000 --- a/prisma/prisma-adapter/.github/workflows/publish.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Publish - -on: - release: - types: [published] - workflow_dispatch: - -jobs: - publish-npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8.x.x - - uses: actions/setup-node@v3 - with: - node-version: '18' - registry-url: 'https://registry.npmjs.org' - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm build - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/prisma/prisma-adapter/README.md b/prisma/prisma-adapter/README.md index 28f7f5d..fbceb9c 100644 --- a/prisma/prisma-adapter/README.md +++ b/prisma/prisma-adapter/README.md @@ -1,22 +1,20 @@ -# @tidbcloud/prisma-adapter +# @stylusdb-sql/prisma-adapter -Prisma driver adapter for [TiDB Cloud Serverless Driver](https://github.com/tidbcloud/serverless-js). For more details, see [TiDB Cloud Serverless Driver Prisma Tutorial -](https://docs.pingcap.com/tidbcloud/serverless-driver-prisma-example). +Prisma driver adapter for [StylusDB-SQL](https://github.com/ChakshuGautam/stylusdb-sql). ## Before you start Before you start, make sure you have: -- A TiDB Cloud account - Node >= 18 - [Prisma CLI](https://www.prisma.io/docs/concepts/components/prisma-cli) installed ## Install -You will need to install the `@tidbcloud/prisma-adapter` driver adapter and the `@tidbcloud/serverless` serverless driver. +You will need to install the `stylusdb-sql-prisma-adapter` driver adapter and the `stylus-db-prisma-client` which is currently unpackaged and can be found [here](./../../examples/prisma/client.js). ``` -npm install @tidbcloud/prisma-adapter @tidbcloud/serverless +npm install stylusdb-sql-prisma-adapter ``` ## DATABASE URL @@ -25,12 +23,12 @@ Set the environment to your .env file in the local environment. You can get conn ```env // .env -DATABASE_URL="mysql://username:password@host:4000/database?sslaccept=strict" +DATABASE_URL="localhost:5432" ``` > NOTE > -> The adapter only supports Prisma Client. Prisma migration and introspection still go through the traditional TCP way. If you only need Prisma Client, you can set the DATABASE_URL as the `mysql://username:password@host/database` format which port and ssl parameters are not needed). +> The adapter only supports Prisma Client. How to make it work with the Prisma Migrate CLI is still under investigation. ## Define Prisma schema @@ -44,15 +42,22 @@ generator client { } datasource db { - provider = "mysql" - url = env("DATABASE_URL") + provider = "postgresql" + url = "postgresql://127.0.0.1:5432/database?sslaccept=strict" } -// define model according to your database table -model user { - id Int @id @default(autoincrement()) - email String? @unique(map: "uniq_email") @db.VarChar(255) - name String? @db.VarChar(255) +model Student { + id Int @id @default(autoincrement()) + name String + age String + enrollment Enrollment[] +} + +model Enrollment { + id Int @id @default(autoincrement()) + studentId Int + course String + student Student @relation(fields: [studentId], references: [id]) } ``` @@ -62,42 +67,9 @@ Here is an example of query: ```js // query.js -import { connect } from '@tidbcloud/serverless'; -import { PrismaTiDBCloud } from '@tidbcloud/prisma-adapter'; -import { PrismaClient } from '@prisma/client'; -import dotenv from 'dotenv'; - -// setup -dotenv.config(); -const connectionString = `${process.env.DATABASE_URL}`; - -// init prisma client -const connection = connect({ url: connectionString }); -const adapter = new PrismaTiDBCloud(connection); -const prisma = new PrismaClient({ adapter }); - -// insert -const user = await prisma.user.create({ - data: { - email: 'test@prisma.io', - name: 'test', - }, -}) -console.log(user) - -// query after insert -console.log(await prisma.user.findMany()) -``` - -## Transaction - -Here is an example of transaction: - -```js -// query.js -import { connect } from '@tidbcloud/serverless'; -import { PrismaTiDBCloud } from '@tidbcloud/prisma-adapter'; import { PrismaClient } from '@prisma/client'; +import { PrismaStylusDBSQL } from 'stylusdb-sql-prisma-adapter'; +import net from 'net'; import dotenv from 'dotenv'; // setup @@ -109,48 +81,43 @@ const connection = connect({ url: connectionString }); const adapter = new PrismaTiDBCloud(connection); const prisma = new PrismaClient({ adapter }); -const createUser1 = prisma.user.create({ - data: { - email: 'yuhang.shi@pingcap.com', - name: 'Shi Yuhang', - }, -}) - -const createUser2 = prisma.user.create({ - data: { - email: 'yuhang.shi@pingcap.com', - name: 'Shi Yuhang2', - }, -}) - -const createUser3 = prisma.user.create({ - data: { - email: 'yuhang2.shi@pingcap.com', - name: 'Shi Yuhang2', - }, -}) -try { - await prisma.$transaction([createUser1, createUser2]) // Operations fail together -} catch (e) { - console.log(e) - await prisma.$transaction([createUser1, createUser3]) // Operations succeed together +const client = new Client(connectionString) +const adapter = new PrismaStylusDBSQL(client, {}) +const prisma = new PrismaClient({ adapter }) + +async function main() { + await client.connect(); + const rawQueryData = await prisma.$queryRaw`SELECT id from student`; + console.log({ rawQueryData }); + const student = await prisma.student.create({ + data: { + name: 'test', + age: '28', + }, + }).catch((e) => { + console.log(e) + }); + console.log(student); + + const students = await prisma.student.findMany(); + console.log(students); } -``` -## Choose a version +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) -| Adapter | Prisma/Prisma Client | serverless driver | -|---------|----------------------|-------------------| -| v5.4.x | v5.4.x | ^v0.0.6 | -| v5.5.x | v5.5.x | ^v0.0.7 | -| v5.6.x | v5.6.x | ^v0.0.7 | -| v5.7.x | v5.7.x | ^v0.0.7 | +``` -Here is the step to step guide for how to choose the version: -1. Choose the Prisma version: Choose the one as you need. -2. Choose the adapter version: If you are using Prisma vx.y.z, you should choose the same version of adapter. If there is no adapter version for your Prisma version, you can choose the latest adapter version in vx.y. Open an issue once you find the adapter version is not compatible with Prisma version. -3. Choose the serverless driver version: You can always use the latest version according to the table above. +## Transaction +> Coming Soon ## Limitations -- [Set isolation level](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#transaction-isolation-level) is not supported yet. +- Heavily under development. From 7549af00fdcd69da7ec04e9aacdc5a0af4f86bd2 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Mon, 15 Jan 2024 20:10:23 +0530 Subject: [PATCH 04/16] Chores: Remove random file that got added --- .github/workflows/ci-adapter.yml | 27 +++++++++++++++++++++++++++ .github/workflows/db-tests.yml | 25 +++++++++++++++++++++++++ .github/workflows/publish-adapter.yml | 26 ++++++++++++++++++++++++++ stylusdb-sql.js | 0 4 files changed, 78 insertions(+) create mode 100644 .github/workflows/ci-adapter.yml create mode 100644 .github/workflows/db-tests.yml create mode 100644 .github/workflows/publish-adapter.yml delete mode 100644 stylusdb-sql.js diff --git a/.github/workflows/ci-adapter.yml b/.github/workflows/ci-adapter.yml new file mode 100644 index 0000000..23da817 --- /dev/null +++ b/.github/workflows/ci-adapter.yml @@ -0,0 +1,27 @@ +name: ci + +on: + pull_request: + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 8.x.x + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: pnpm + + - name: install dependencies + run: pnpm install --frozen-lockfile + - name: lint + run: pnpm lint + - name: build + run: pnpm build \ No newline at end of file diff --git a/.github/workflows/db-tests.yml b/.github/workflows/db-tests.yml new file mode 100644 index 0000000..3e5e185 --- /dev/null +++ b/.github/workflows/db-tests.yml @@ -0,0 +1,25 @@ +name: Run Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm i + - run: npm test diff --git a/.github/workflows/publish-adapter.yml b/.github/workflows/publish-adapter.yml new file mode 100644 index 0000000..7b6839d --- /dev/null +++ b/.github/workflows/publish-adapter.yml @@ -0,0 +1,26 @@ +name: Publish + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8.x.x + - uses: actions/setup-node@v3 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/stylusdb-sql.js b/stylusdb-sql.js deleted file mode 100644 index e69de29..0000000 From 9609b2b7af1af1c77af70e2c406883c71469afb7 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Mon, 15 Jan 2024 20:17:11 +0530 Subject: [PATCH 05/16] Demo: Fix server URL --- web/www/frontend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/www/frontend.js b/web/www/frontend.js index 3f0d1a9..671a3a8 100644 --- a/web/www/frontend.js +++ b/web/www/frontend.js @@ -1,4 +1,4 @@ -const socket = new WebSocket("wss://stylusdb-sql-server.onrender.com"); +const socket = new WebSocket("ws://127.0.0.1:6060"); var term = new window.Terminal({ cursorBlink: true From 83afe44444a0502a325b3bd46af5bee8461675ee Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 08:35:57 +0530 Subject: [PATCH 06/16] Fix Readme.md --- prisma/prisma-adapter/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/prisma/prisma-adapter/README.md b/prisma/prisma-adapter/README.md index fbceb9c..9f21ad4 100644 --- a/prisma/prisma-adapter/README.md +++ b/prisma/prisma-adapter/README.md @@ -76,11 +76,6 @@ import dotenv from 'dotenv'; dotenv.config(); const connectionString = `${process.env.DATABASE_URL}`; -// init prisma client -const connection = connect({ url: connectionString }); -const adapter = new PrismaTiDBCloud(connection); -const prisma = new PrismaClient({ adapter }); - const client = new Client(connectionString) const adapter = new PrismaStylusDBSQL(client, {}) const prisma = new PrismaClient({ adapter }) From fdcfedf455fcbb161b56be7aa44c6ffae7f58ac4 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 08:42:19 +0530 Subject: [PATCH 07/16] Fix main readme --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2d67f53..f35657b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,14 @@ This database is for educational purposes only. It is not intended for production use. It is written ground up in JavaScript and is a great way to learn how databases work. You can find all the code in the [src](./src) directory and the tutorial in the [docs](./docs) directory. ### Features +- [x] Support for [Prisma](https://www.prisma.io/) - [x] `INSERT`, `DELETE`, `SELECT` -- [ ] `UPDATE`, `CREATE TABLE`, `DROP TABLE` - [x] CLI -- [ ] NPM Package +- [x] Server/Client Basic Protocol +- [x] NPM Package for StylusDB-SQL +- [ ] `UPDATE`, `CREATE TABLE`, `DROP TABLE` +- [ ] SQL Spec Tracker +- [ ] Minimal PostgreSQL Protocol for Server/Client Communication ### Installation @@ -17,6 +21,12 @@ npm i stylusdb-sql ### Usage +#### Client +For usage with the client, see documentation [here](./examples/client/README.md). + +#### With Prisma Client +For usage with Prisma Client, see documentation [here](./prisma/prisma-adapter/README.md). + #### SELECT Assuming you have a CSV file called `student.csv` with the following data: ``` From 6aa5d2c17d7a8904667cc8f1ae5d44ca4f96cd09 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 09:02:32 +0530 Subject: [PATCH 08/16] Fix CLI Tests --- tests/cli.test.js | 31 ++++++++++++++++++++++++++----- tests/insertExecuter.test.js | 2 +- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/cli.test.js b/tests/cli.test.js index 5d222e6..050d31d 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -1,6 +1,29 @@ const child_process = require('child_process'); const path = require('path'); +function convertToJson(str) { + // Regular expression to find unquoted keys + const regexKeys = /(\b\w+\b)(?=:)/g; + // Regular expression to strip ANSI color codes + const regexAnsi = /\x1B\[\d+m/g; + + str = str.replace(/'/g, '"').replace(/(\w+):/g, '"$1":') + // Replace unquoted keys with quoted keys and remove ANSI color codes + const jsonString = str.replace(regexKeys, '"$1"').replace(regexAnsi, ''); + + try { + // Parse the corrected string into JSON + const json = JSON.parse(jsonString); + return json; + } catch (e) { + // If an error occurs, log it and return null + console.error("Error parsing JSON:", e); + return null; + } +} + + + test('DISTINCT with Multiple Columns via CLI', (done) => { const cliPath = path.join(__dirname, '..', 'src', 'cli.js'); const cliProcess = child_process.spawn('node', [cliPath]); @@ -16,12 +39,10 @@ test('DISTINCT with Multiple Columns via CLI', (done) => { const resultRegex = /Result: (\[.+\])/s; const match = cleanedOutput.match(resultRegex); - // Fix JSON outputput - match[1] = match[1].replace(/'/g, '"').replace(/(\w+):/g, '"$1":'); if (match && match[1]) { // Parse the captured JSON string - const results = JSON.parse(match[1]); + const results = convertToJson(match[1]); // Validation logic expect(results).toEqual([ @@ -32,8 +53,8 @@ test('DISTINCT with Multiple Columns via CLI', (done) => { { student_id: '5', course: 'Biology' }, { student_id: '5', course: 'Physics' } ]); - console.log("Test passed successfully"); } else { + done(); throw new Error('Failed to parse CLI output'); } @@ -47,4 +68,4 @@ test('DISTINCT with Multiple Columns via CLI', (done) => { cliProcess.stdin.write("exit\n"); }, 1000); // 1 second delay }, 1000); // 1 second delay -}); \ No newline at end of file +}, 10000); \ No newline at end of file diff --git a/tests/insertExecuter.test.js b/tests/insertExecuter.test.js index 4d7ebba..a80b45c 100644 --- a/tests/insertExecuter.test.js +++ b/tests/insertExecuter.test.js @@ -88,7 +88,7 @@ test('Execute INSERT INTO Query for users with partial columns', async () => { await createUserCSV(); // Execute INSERT statement - await executeINSERTQuery(`INSERT INTO "User" ("name") VALUES ("test")`); + await executeINSERTQuery(`INSERT INTO "user" ("name") VALUES ("test")`); // Read the updated CSV content const updatedData = await readCSV('user.csv'); From 81a19292cf51567ccf7fa675741afa7baffa8169 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 09:06:20 +0530 Subject: [PATCH 09/16] Fix CI --- .github/workflows/ci-adapter.yml | 16 ++++++++++++---- .github/workflows/db-tests.yml | 2 +- .github/workflows/publish-adapter.yml | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-adapter.yml b/.github/workflows/ci-adapter.yml index 23da817..0c19f98 100644 --- a/.github/workflows/ci-adapter.yml +++ b/.github/workflows/ci-adapter.yml @@ -1,4 +1,4 @@ -name: ci +name: CI Adapter on: pull_request: @@ -20,8 +20,16 @@ jobs: cache: pnpm - name: install dependencies - run: pnpm install --frozen-lockfile + run: | + cd prisma/prisma-adapter + pnpm install --frozen-lockfile + - name: lint - run: pnpm lint + run: | + cd prisma/prisma-adapter + pnpm lint + - name: build - run: pnpm build \ No newline at end of file + run: | + cd prisma/prisma-adapter + pnpm build \ No newline at end of file diff --git a/.github/workflows/db-tests.yml b/.github/workflows/db-tests.yml index 3e5e185..c0cd8a3 100644 --- a/.github/workflows/db-tests.yml +++ b/.github/workflows/db-tests.yml @@ -1,4 +1,4 @@ -name: Run Tests +name: Run Tests on DB on: push: diff --git a/.github/workflows/publish-adapter.yml b/.github/workflows/publish-adapter.yml index 7b6839d..ab91145 100644 --- a/.github/workflows/publish-adapter.yml +++ b/.github/workflows/publish-adapter.yml @@ -1,4 +1,4 @@ -name: Publish +name: Publish Adapter to NPM on: release: From 5ccb690de29106ded60e57a2f8265b31ecf3a87e Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 09:18:03 +0530 Subject: [PATCH 10/16] Fix CI --- .github/workflows/ci-adapter.yml | 10 +++++----- .github/workflows/publish-adapter.yml | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-adapter.yml b/.github/workflows/ci-adapter.yml index 0c19f98..acc54c8 100644 --- a/.github/workflows/ci-adapter.yml +++ b/.github/workflows/ci-adapter.yml @@ -10,26 +10,26 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: npm/action-setup@v2 with: version: 8.x.x - uses: actions/setup-node@v3 with: node-version: '18' - cache: pnpm + cache: npm - name: install dependencies run: | cd prisma/prisma-adapter - pnpm install --frozen-lockfile + npm install --frozen-lockfile - name: lint run: | cd prisma/prisma-adapter - pnpm lint + npm lint - name: build run: | cd prisma/prisma-adapter - pnpm build \ No newline at end of file + npm build \ No newline at end of file diff --git a/.github/workflows/publish-adapter.yml b/.github/workflows/publish-adapter.yml index ab91145..eeb93b0 100644 --- a/.github/workflows/publish-adapter.yml +++ b/.github/workflows/publish-adapter.yml @@ -10,17 +10,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: npm/action-setup@v2 with: version: 8.x.x - uses: actions/setup-node@v3 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - cache: pnpm + cache: npm - - run: pnpm install --frozen-lockfile - - run: pnpm build + # Publish Adapter + - run: | + cd prisma/prisma-adapter + npm install --frozen-lockfile + + - run: | + cd prisma/prisma-adapter + npm build + - run: | + cd prisma/prisma-adapter + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # Publish Database - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 0379d4710caf09489d7fd388fe7969efaae9eb73 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 09:19:46 +0530 Subject: [PATCH 11/16] Fix CI --- .github/workflows/ci-adapter.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci-adapter.yml b/.github/workflows/ci-adapter.yml index acc54c8..5ca8566 100644 --- a/.github/workflows/ci-adapter.yml +++ b/.github/workflows/ci-adapter.yml @@ -9,11 +9,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: npm/action-setup@v2 - with: - version: 8.x.x - - uses: actions/setup-node@v3 with: node-version: '18' From ff5060ed2aa2ded67d77c9b5a4eddedf7270998e Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Wed, 17 Jan 2024 09:20:41 +0530 Subject: [PATCH 12/16] Fix CI --- .github/workflows/ci-adapter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-adapter.yml b/.github/workflows/ci-adapter.yml index 5ca8566..2a20ce7 100644 --- a/.github/workflows/ci-adapter.yml +++ b/.github/workflows/ci-adapter.yml @@ -22,9 +22,9 @@ jobs: - name: lint run: | cd prisma/prisma-adapter - npm lint + npm run lint - name: build run: | cd prisma/prisma-adapter - npm build \ No newline at end of file + npm run build \ No newline at end of file From 0e962f37ef8279f6cea715b054d9ed164e3d18a5 Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Sat, 9 Mar 2024 15:29:14 +0530 Subject: [PATCH 13/16] Tutorial-21 --- .github/workflows/db-tests.yml | 1 + .gitignore | 4 +- package-lock.json | 150 +++++++++++++++-- package.json | 3 + src/csvStorage.js | 21 ++- src/queryExecuter.js | 29 +++- src/queryParser.js | 47 +++++- tests/appoximateLargeFile.test.js | 63 ++++++++ tests/generateLargeFile.js | 35 ++++ tests/queryExecuter.test.js | 56 +++++++ tests/queryParser.test.js | 257 +++++++++++++++++++++++++++--- 11 files changed, 624 insertions(+), 42 deletions(-) create mode 100644 tests/appoximateLargeFile.test.js create mode 100644 tests/generateLargeFile.js diff --git a/.github/workflows/db-tests.yml b/.github/workflows/db-tests.yml index c0cd8a3..eee2c08 100644 --- a/.github/workflows/db-tests.yml +++ b/.github/workflows/db-tests.yml @@ -22,4 +22,5 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm i + - run: npm generate - run: npm test diff --git a/.gitignore b/.gitignore index 656790b..93ee4f1 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,6 @@ dist .pnp.* -**/.DS_Store \ No newline at end of file +**/.DS_Store + +student_large.csv \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3afaec3..9b5dd73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "csv-parser": "^3.0.0", + "hll": "^2.0.0", "json2csv": "^6.0.0-alpha.2", "xterm": "^5.3.0" }, @@ -17,6 +18,7 @@ "stylusdb-cli": "node ./src/cli.js" }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "jest": "^29.7.0" } }, @@ -655,6 +657,22 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1200,6 +1218,11 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1310,14 +1333,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1525,8 +1546,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -1796,8 +1816,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -1865,7 +1884,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1917,6 +1935,17 @@ "node": ">= 0.4" } }, + "node_modules/hll": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hll/-/hll-2.0.0.tgz", + "integrity": "sha512-PV92xkmczdzgSDIf8ii7zInuvw40X+XFovrtSOaVx7TfuKnb4EWvgjLHTcCVm5FGXMDUNe2v/4g0vyMtQESJdg==", + "dependencies": { + "murmurhash3": "^0.5.0" + }, + "engines": { + "node": ">=10 <15" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -1964,7 +1993,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1973,8 +2001,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-arrayish": { "version": "0.2.1", @@ -2726,6 +2753,17 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-beautify-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/js-beautify-node/-/js-beautify-node-1.0.0.tgz", + "integrity": "sha512-P5t/LUeK2PUIEzhJ3ehjHiMGQjstXgCBpX4HJkG0Igq6XwnSI0c4HUGsJep34pZK5BEpkCyIzOCcthV/ucSitg==", + "engines": [ + "node >= 0.3.0" + ], + "bin": { + "jsbeautify": "beautify-node.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2931,7 +2969,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2947,12 +2984,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/murmurhash3": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/murmurhash3/-/murmurhash3-0.5.0.tgz", + "integrity": "sha512-bgvgIBctpwslE9kIurdGH9knDDyYCZca5upcy9MLzsc16afyBdwkfhOJmxN607exbyesZgTQEtAcqs3euEsFlA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.14.2", + "shipitjs": "^0.3.2" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2996,7 +3062,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -3098,7 +3163,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3274,6 +3338,17 @@ "node": ">=10" } }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3304,6 +3379,33 @@ "node": ">=8" } }, + "node_modules/shipitjs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/shipitjs/-/shipitjs-0.3.2.tgz", + "integrity": "sha512-uLtgZP2PBzxwoCDfEhARWpuQewMwRLGhkrD8O/rqXHdyYjaV8LY0F4aMi5Lr3l4XWm5YiHINsCMhzD0scu7GzQ==", + "dependencies": { + "async": "", + "commander": ">=0.6.1", + "js-beautify-node": "", + "semver": "^5.5.1", + "temp": "", + "underscore": "" + }, + "bin": { + "shipitjs": "bin/shipitjs" + }, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/shipitjs/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3455,6 +3557,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -3517,6 +3631,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -3611,8 +3730,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index 4eb4c53..83f7671 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "doc": "docs" }, "scripts": { + "generate": "node ./tests/generateLargeFile.js", "test": "jest", "test-verbose": "jest --verbose", "server": "node ./src/server.js" @@ -18,10 +19,12 @@ "author": "Chakshu Gautam", "license": "ISC", "devDependencies": { + "@faker-js/faker": "^8.4.1", "jest": "^29.7.0" }, "dependencies": { "csv-parser": "^3.0.0", + "hll": "^2.0.0", "json2csv": "^6.0.0-alpha.2", "xterm": "^5.3.0" } diff --git a/src/csvStorage.js b/src/csvStorage.js index 4729599..c4c33b9 100644 --- a/src/csvStorage.js +++ b/src/csvStorage.js @@ -1,9 +1,11 @@ const fs = require('fs'); const csv = require('csv-parser'); const { parse } = require('json2csv'); +const hll = require('hll'); function readCSV(filePath) { const results = []; + var h = hll(); return new Promise((resolve, reject) => { fs.createReadStream(filePath) @@ -18,9 +20,26 @@ function readCSV(filePath) { }); } +function readCSVForHLL(filePath, bitSampleSize = 12, digestSize = 128) { + const results = []; + var h = hll({ bitSampleSize: bitSampleSize, digestSize: digestSize }); + + return new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(csv()) + .on('data', (data) => h.insert(JSON.stringify(data))) + .on('end', () => { + resolve(h); + }) + .on('error', (error) => { + reject(error); + }); + }); +} + async function writeCSV(filename, data) { const csv = parse(data); fs.writeFileSync(filename, csv); } -module.exports = { readCSV, writeCSV }; \ No newline at end of file +module.exports = { readCSV, readCSVForHLL, writeCSV }; \ No newline at end of file diff --git a/src/queryExecuter.js b/src/queryExecuter.js index ce210a3..863cd1a 100644 --- a/src/queryExecuter.js +++ b/src/queryExecuter.js @@ -1,5 +1,7 @@ const { parseSelectQuery, parseInsertQuery, parseDeleteQuery } = require('./queryParser.js'); -const { readCSV, writeCSV } = require('./csvStorage.js'); +const { readCSV, readCSVForHLL, writeCSV } = require('./csvStorage.js'); +const hll = require('hll'); + function performInnerJoin(data, joinData, joinCondition, fields, table) { return data.flatMap(mainRow => { @@ -203,8 +205,14 @@ function applyGroupBy(data, groupByFields, aggregateFunctions) { async function executeSELECTQuery(query) { try { + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, isApproximateCount, orderByFields, limit, isDistinct, distinctFields, isCountDistinct } = parseSelectQuery(query); + + + if (isApproximateCount && fields.length === 1 && fields[0] === 'COUNT(*)' && whereClauses.length === 0) { + let hll = await readCSVForHLL(`${table}.csv`); + return [{ 'APPROXIMATE_COUNT(*)': hll.estimate() }]; + } - const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, orderByFields, limit, isDistinct } = parseSelectQuery(query); let data = await readCSV(`${table}.csv`); // Perform INNER JOIN if specified @@ -229,6 +237,7 @@ async function executeSELECTQuery(query) { ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) : data; + let groupResults = filteredData; if (hasAggregateWithoutGroupBy) { // Special handling for queries like 'SELECT COUNT(*) FROM table' @@ -293,6 +302,20 @@ async function executeSELECTQuery(query) { }); } + // Distinct inside count - example "SELECT COUNT (DISTINCT student.name) FROM student" + if (isCountDistinct) { + + if (isApproximateCount) { + var h = hll({ bitSampleSize: 12, digestSize: 128 }); + orderedResults.forEach(row => h.insert(distinctFields.map(field => row[field]).join('|'))); + return [{ [`APPROXIMATE_${fields[0]}`]: h.estimate() }]; + } + else { + let distinctResults = [...new Map(orderedResults.map(item => [distinctFields.map(field => item[field]).join('|'), item])).values()]; + return [{ [fields[0]]: distinctResults.length }]; + } + } + // Select the specified fields let finalResults = orderedResults.map(row => { const selectedRow = {}; @@ -303,6 +326,8 @@ async function executeSELECTQuery(query) { return selectedRow; }); + // console.log("CP-2", orderedResults) + // Remove duplicates if specified let distinctResults = finalResults; if (isDistinct) { diff --git a/src/queryParser.js b/src/queryParser.js index 4230a15..c81aada 100644 --- a/src/queryParser.js +++ b/src/queryParser.js @@ -8,7 +8,25 @@ function parseSelectQuery(query) { query = query.trim(); // Initialize distinct flag - let isDistinct = false; + let isDistinct = false; // Global DISTINCT, not within COUNT + let isCountDistinct = false; // New flag for DISTINCT within COUNT + let distinctFields = []; // Array to hold fields after DISTINCT within COUNT or APPROXIMATE_COUNT + + + // Detect APPROXIMATE_COUNT + let isApproximateCount = false; + const approximateCountRegex = /APPROXIMATE_COUNT\((DISTINCT\s)?(.+?)\)/i; + const approximateCountMatch = query.match(approximateCountRegex); + if (approximateCountMatch) { + isApproximateCount = true; + // If DISTINCT is used within APPROXIMATE_COUNT, capture the fields + if (approximateCountMatch[1]) { + isCountDistinct = true; + // distinctFields.push(approximateCountMatch[2].trim()); + } + // Simplify further processing by normalizing to COUNT (adjust as necessary for your logic) + query = query.replace(approximateCountRegex, `COUNT(${approximateCountMatch[1] || ''}${approximateCountMatch[2]})`); + } // Check for DISTINCT keyword and update the query if (query.toUpperCase().includes('SELECT DISTINCT')) { @@ -59,13 +77,24 @@ function parseSelectQuery(query) { // Extract JOIN information const { joinType, joinTable, joinCondition } = parseJoinClause(queryWithoutWhere); + const countDistinctRegex = /COUNT\((DISTINCT\s\((.*?)\))\)/gi; + let countDistinctMatch; + while ((countDistinctMatch = countDistinctRegex.exec(query)) !== null) { + isCountDistinct = true; + if (isApproximateCount) { + distinctFields.push(...countDistinctMatch[2].trim().split(',').map(field => field.trim())); + } else { + distinctFields.push(...countDistinctMatch[2].trim().split(',').map(field => field.trim())); + } + } + // Parse SELECT part const selectRegex = /^SELECT\s(.+?)\sFROM\s(.+)/i; const selectMatch = selectPart.match(selectRegex); if (!selectMatch) { throw new Error('Invalid SELECT format'); } - const [, fields, table] = selectMatch; + let [, fields, table] = selectMatch; // Parse WHERE part if it exists let whereClauses = []; @@ -76,8 +105,17 @@ function parseSelectQuery(query) { // Check for aggregate functions without GROUP BY const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy(query, groupByFields); + // Temporarily replace commas within parentheses to avoid incorrect splitting + const tempPlaceholder = '__TEMP_COMMA__'; // Ensure this placeholder doesn't appear in your actual queries + fields = fields.replace(/\(([^)]+)\)/g, (match) => match.replace(/,/g, tempPlaceholder)); + + // Now split fields and restore any temporary placeholders + const parsedFields = fields.split(',').map(field => + field.trim().replace(new RegExp(tempPlaceholder, 'g'), ',')); + + return { - fields: fields.split(',').map(field => field.trim()), + fields: parsedFields, table: table.trim(), whereClauses, joinType, @@ -86,7 +124,10 @@ function parseSelectQuery(query) { groupByFields, orderByFields, hasAggregateWithoutGroupBy, + isApproximateCount, + isCountDistinct, limit, + distinctFields, isDistinct }; } catch (error) { diff --git a/tests/appoximateLargeFile.test.js b/tests/appoximateLargeFile.test.js new file mode 100644 index 0000000..f8d241a --- /dev/null +++ b/tests/appoximateLargeFile.test.js @@ -0,0 +1,63 @@ +const fs = require('fs'); +const { executeSELECTQuery } = require('../src/queryExecuter'); +const jestConsole = console; + +beforeEach(() => { + global.console = require('console'); +}); + +afterEach(() => { + global.console = jestConsole; +}); + +test('Large File Count(*) - Approximate and Exact', async () => { + // Test Exact Count + + const startMemoryUsageExact = process.memoryUsage().heapUsed; + const startTimeExact = performance.now(); + + const queryExact = "SELECT COUNT(*) FROM student_large"; + const resultExact = await executeSELECTQuery(queryExact); + const exactResult = resultExact[0]['COUNT(*)']; + + const endTimeExact = performance.now(); + const endMemoryUsageExact = process.memoryUsage().heapUsed; + + console.log(`Execution Time for Exact Count: ${(endTimeExact - startTimeExact).toFixed(2)} ms`); + console.log(`Start Memory for Exact Count: ${startMemoryUsageExact / 1024 / 1024} MB`); + console.log(`End Memory for Exact Count: ${endMemoryUsageExact / 1024 / 1024} MB`); + console.log(`Memory Used for Exact Count: ${(endMemoryUsageExact - startMemoryUsageExact) / 1024 / 1024} MB`); + + const startMemoryUsage = process.memoryUsage().heapUsed; + const startTime = performance.now(); + + const query = "SELECT APPROXIMATE_COUNT(*) FROM student_large"; + const result = await executeSELECTQuery(query); + + // Expect the approximate count to be within 5% of the actual count + expect(result[0]['APPROXIMATE_COUNT(*)']).toBeGreaterThan(exactResult - 0.05 * exactResult); + expect(result[0]['APPROXIMATE_COUNT(*)']).toBeLessThan(exactResult + 0.05 * exactResult); + + const endTime = performance.now(); + const endMemoryUsage = process.memoryUsage().heapUsed; + + console.log(`Execution Time for Approximate Count: ${(endTime - startTime).toFixed(2)} ms`); + console.log(`Start Memory: ${startMemoryUsage / 1024 / 1024} MB`); + console.log(`End Memory: ${endMemoryUsage / 1024 / 1024} MB`); + console.log(`Memory Used for Approximate Count: ${(endMemoryUsage - startMemoryUsage) / 1024 / 1024} MB`); + +}, 60000); + +test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { + const queryExact = "SELECT COUNT(DISTINCT (name, age)) FROM student_large"; + const resultExact = await executeSELECTQuery(queryExact); + console.log({ resultExact }); + const exactResult = resultExact[0]['COUNT(DISTINCT (name, age))']; + + const query = "SELECT APPROXIMATE_COUNT(DISTINCT (name, age)) FROM student_large"; + const result = await executeSELECTQuery(query); + + // Expect the approximate count to be within 2% of the actual count + expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeGreaterThan(exactResult - 0.05 * exactResult); + expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeLessThan(exactResult + 0.05 * exactResult); +}, 60000); \ No newline at end of file diff --git a/tests/generateLargeFile.js b/tests/generateLargeFile.js new file mode 100644 index 0000000..d70c913 --- /dev/null +++ b/tests/generateLargeFile.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const { faker, da } = require('@faker-js/faker'); +const { parse } = require('json2csv'); + +async function generateLargeCSV(filename) { + let data = []; + for (let i = 1; i <= 10_000_000; i++) { + const record = { + id: i, + name: faker.person.firstName(), + age: faker.number.int({ min: 18, max: 100 }), + }; + data.push(record); + + let rows; + if (i % 500_000 === 0) { + console.log(`Generated ${i} records`); + if (!fs.existsSync(filename)) { + rows = parse(data, { header: true }); + } else { + // Rows without headers. + rows = parse(data, { header: false }); + } + fs.appendFileSync(filename, rows); + data = []; + } + + } + // Append file function can create new file too. + + // Always add new line if file already exists. + fs.appendFileSync(filename, "\r\n"); +} + +generateLargeCSV('student_large.csv') \ No newline at end of file diff --git a/tests/queryExecuter.test.js b/tests/queryExecuter.test.js index 3dc121c..891f062 100644 --- a/tests/queryExecuter.test.js +++ b/tests/queryExecuter.test.js @@ -418,3 +418,59 @@ test('LIKE with ORDER BY and LIMIT', async () => { // Expecting the first two names alphabetically that contain 'a' expect(result).toEqual([{ name: 'Alice' }, { name: 'Jane' }]); }); + + +test('Execute SQL Query with APPROXIMATE_COUNT Function', async () => { + const query = "SELECT APPROXIMATE_COUNT(id) FROM student"; + const result = await executeSELECTQuery(query); + // Assuming APPROXIMATE_COUNT behaves like COUNT for testing + // Expecting the count of all student records + expect(result).toEqual([{ 'COUNT(id)': 5 }]); // Assuming there are 5 records in student.csv +}); + +test('Execute SQL Query with APPROXIMATE_COUNT and GROUP BY Clauses', async () => { + const query = "SELECT APPROXIMATE_COUNT(id), course FROM enrollment GROUP BY course"; + const result = await executeSELECTQuery(query); + // Assuming APPROXIMATE_COUNT behaves like COUNT for testing + // Expecting the count of student records grouped by course + expect(result).toEqual([ + { 'COUNT(id)': 2, course: 'Mathematics' }, // Assuming 2 students are enrolled in Mathematics + { 'COUNT(id)': 2, course: 'Physics' }, // Assuming 1 student is enrolled in Physics + { 'COUNT(id)': 1, course: 'Chemistry' }, // Assuming 1 student is enrolled in Chemistry + { 'COUNT(id)': 1, course: 'Biology' } // Assuming 1 student is enrolled in Biology + ]); +}); + +test('Execute SQL Query with APPROXIMATE_COUNT, WHERE, and ORDER BY Clauses', async () => { + const query = "SELECT APPROXIMATE_COUNT(id) FROM student WHERE age > '20' ORDER BY age DESC"; + const result = await executeSELECTQuery(query); + // Assuming APPROXIMATE_COUNT behaves like COUNT for testing + // Expecting the count of students older than 20, ordered by age in descending order + // Note: The ORDER BY clause does not affect the outcome for a single aggregated result + expect(result).toEqual([{ 'COUNT(id)': 5 }]); // Assuming there are 4 students older than 20 +}); + + +test('Execute SQL Query with APPROXIMATE_COUNT only', async () => { + const query = "SELECT APPROXIMATE_COUNT(*) FROM student"; + const result = await executeSELECTQuery(query); + expect(result).toEqual([{ 'APPROXIMATE_COUNT(*)': 5 }]); +}); + +test('Execute SQL Query with APPROXIMATE_COUNT with DISTINCT on a column', async () => { + const query = "SELECT APPROXIMATE_COUNT(DISTINCT (name)) FROM student"; + const result = await executeSELECTQuery(query); + expect(result).toEqual([{ 'APPROXIMATE_COUNT(DISTINCT (name))': 4 }]); +}); + +test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { + const query = "SELECT COUNT(DISTINCT (name)) FROM student"; + const result = await executeSELECTQuery(query); + expect(result).toEqual([{ 'COUNT(DISTINCT (name))': 4 }]); +}); + +test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { + const query = "SELECT COUNT(DISTINCT (name, age)) FROM student"; + const result = await executeSELECTQuery(query); + expect(result).toEqual([{ 'COUNT(DISTINCT (name, age))': 5 }]); +}); \ No newline at end of file diff --git a/tests/queryParser.test.js b/tests/queryParser.test.js index 664463b..39e12e5 100644 --- a/tests/queryParser.test.js +++ b/tests/queryParser.test.js @@ -16,6 +16,9 @@ test('Parse SQL Query', () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -38,6 +41,9 @@ test('Parse SQL Query with WHERE Clause', () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -64,6 +70,9 @@ test('Parse SQL Query with Multiple WHERE Clauses', () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -82,6 +91,9 @@ test('Parse SQL Query with INNER JOIN', async () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }) }); @@ -100,6 +112,9 @@ test('Parse SQL Query with INNER JOIN and WHERE Clause', async () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }) }); @@ -160,6 +175,9 @@ test('Parse LEFT Join Query Completely', () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }) }) @@ -178,6 +196,9 @@ test('Parse LEFT Join Query Completely', () => { orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }) }) @@ -196,6 +217,9 @@ test('Parse SQL Query with LEFT JOIN with a WHERE clause filtering the main tabl orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -214,6 +238,9 @@ test('Parse SQL Query with LEFT JOIN with a WHERE clause filtering the join tabl orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -232,6 +259,9 @@ test('Parse SQL Query with RIGHT JOIN with a WHERE clause filtering the main tab orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -250,6 +280,9 @@ test('Parse SQL Query with RIGHT JOIN with a WHERE clause filtering the join tab orderByFields: null, limit: null, isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -268,7 +301,10 @@ test('Parse COUNT Aggregate Query', () => { joinType: null, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -287,7 +323,10 @@ test('Parse SUM Aggregate Query', () => { joinType: null, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -305,7 +344,10 @@ test('Parse AVG Aggregate Query', () => { joinType: null, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -323,7 +365,10 @@ test('Parse MIN Aggregate Query', () => { joinType: null, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -341,7 +386,10 @@ test('Parse MAX Aggregate Query', () => { joinType: null, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -359,7 +407,10 @@ test('Parse basic GROUP BY query', () => { hasAggregateWithoutGroupBy: false, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -377,7 +428,10 @@ test('Parse GROUP BY query with WHERE clause', () => { hasAggregateWithoutGroupBy: false, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -395,7 +449,10 @@ test('Parse GROUP BY query with multiple fields', () => { hasAggregateWithoutGroupBy: false, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -416,7 +473,10 @@ test('Parse GROUP BY query with JOIN and WHERE clauses', () => { hasAggregateWithoutGroupBy: false, orderByFields: null, limit: null, - isDistinct: false + isDistinct: false, + isApproximateCount: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -483,6 +543,7 @@ test('Parse SQL Query with Basic DISTINCT', () => { fields: ['age'], table: 'student', isDistinct: true, + isApproximateCount: false, whereClauses: [], groupByFields: null, joinType: null, @@ -490,7 +551,9 @@ test('Parse SQL Query with Basic DISTINCT', () => { joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -501,6 +564,7 @@ test('Parse SQL Query with DISTINCT and Multiple Columns', () => { fields: ['student_id', 'course'], table: 'enrollment', isDistinct: true, + isApproximateCount: false, whereClauses: [], groupByFields: null, joinType: null, @@ -508,7 +572,9 @@ test('Parse SQL Query with DISTINCT and Multiple Columns', () => { joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -519,6 +585,7 @@ test('Parse SQL Query with DISTINCT and WHERE Clause', () => { fields: ['course'], table: 'enrollment', isDistinct: true, + isApproximateCount: false, whereClauses: [{ field: 'student_id', operator: '=', value: '"1"' }], groupByFields: null, joinType: null, @@ -526,7 +593,9 @@ test('Parse SQL Query with DISTINCT and WHERE Clause', () => { joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -537,6 +606,7 @@ test('Parse SQL Query with DISTINCT and JOIN Operations', () => { fields: ['student.name'], table: 'student', isDistinct: true, + isApproximateCount: false, whereClauses: [], groupByFields: null, joinType: 'INNER', @@ -547,7 +617,9 @@ test('Parse SQL Query with DISTINCT and JOIN Operations', () => { }, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -558,6 +630,7 @@ test('Parse SQL Query with DISTINCT, ORDER BY, and LIMIT', () => { fields: ['age'], table: 'student', isDistinct: true, + isApproximateCount: false, whereClauses: [], groupByFields: null, joinType: null, @@ -565,7 +638,9 @@ test('Parse SQL Query with DISTINCT, ORDER BY, and LIMIT', () => { joinCondition: null, orderByFields: [{ fieldName: 'age', order: 'DESC' }], limit: 2, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -576,6 +651,7 @@ test('Parse SQL Query with DISTINCT on All Columns', () => { fields: ['*'], table: 'student', isDistinct: true, + isApproximateCount: false, whereClauses: [], groupByFields: null, joinType: null, @@ -583,7 +659,9 @@ test('Parse SQL Query with DISTINCT on All Columns', () => { joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -595,13 +673,16 @@ test('Parse SQL Query with LIKE Clause', () => { table: 'student', whereClauses: [{ field: 'name', operator: 'LIKE', value: '%Jane%' }], isDistinct: false, + isApproximateCount: false, groupByFields: null, joinType: null, joinTable: null, joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -613,13 +694,16 @@ test('Parse SQL Query with LIKE Clause and Wildcards', () => { table: 'student', whereClauses: [{ field: 'name', operator: 'LIKE', value: 'J%' }], isDistinct: false, + isApproximateCount: false, groupByFields: null, joinType: null, joinTable: null, joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -634,13 +718,16 @@ test('Parse SQL Query with Multiple LIKE Clauses', () => { { field: 'age', operator: 'LIKE', value: '2%' } ], isDistinct: false, + isApproximateCount: false, groupByFields: null, joinType: null, joinTable: null, joinCondition: null, orderByFields: null, limit: null, - hasAggregateWithoutGroupBy: false + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] }); }); @@ -653,6 +740,95 @@ test('Parse SQL Query with LIKE and ORDER BY Clauses', () => { whereClauses: [{ field: 'name', operator: 'LIKE', value: '%e%' }], orderByFields: [{ fieldName: 'age', order: 'DESC' }], isDistinct: false, + isApproximateCount: false, + groupByFields: null, + joinType: null, + joinTable: null, + joinCondition: null, + limit: null, + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] + }); +}); + +test('Parse SQL Query with APPROXIMATE_COUNT Function', () => { + const query = "SELECT APPROXIMATE_COUNT(id) FROM student"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(id)'], // Assuming APPROXIMATE_COUNT is replaced with COUNT for simplicity + table: 'student', + whereClauses: [], + isDistinct: false, + isApproximateCount: true, // This flag should be true when APPROXIMATE_COUNT is used + groupByFields: null, + joinType: null, + joinTable: null, + joinCondition: null, + orderByFields: null, + limit: null, + hasAggregateWithoutGroupBy: true, + isCountDistinct: false, + distinctFields: [] + }); +}); + +test('Parse SQL Query with APPROXIMATE_COUNT and GROUP BY Clauses', () => { + const query = "SELECT APPROXIMATE_COUNT(id), course FROM enrollment GROUP BY course"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(id)', 'course'], // Assuming APPROXIMATE_COUNT is replaced with COUNT for simplicity + table: 'enrollment', + whereClauses: [], + isDistinct: false, + isApproximateCount: true, // This flag should be true when APPROXIMATE_COUNT is used + groupByFields: ['course'], + joinType: null, + joinTable: null, + joinCondition: null, + orderByFields: null, + limit: null, + hasAggregateWithoutGroupBy: false, + isCountDistinct: false, + distinctFields: [] + }); +}); + +test('Parse SQL Query with APPROXIMATE_COUNT, WHERE, and ORDER BY Clauses', () => { + const query = "SELECT APPROXIMATE_COUNT(id) FROM student WHERE age > 20 ORDER BY age DESC"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(id)'], + table: 'student', + whereClauses: [ + { field: 'age', operator: '>', value: '20' } + ], + orderByFields: [{ fieldName: 'age', order: 'DESC' }], + isDistinct: false, + isApproximateCount: true, + groupByFields: null, + joinType: null, + joinTable: null, + joinCondition: null, + limit: null, + hasAggregateWithoutGroupBy: true, + isCountDistinct: false, + distinctFields: [] + }); +}); + +test('Parse SQL Query with APPROXIMATE_COUNT with DISTINCT on a column', () => { + const query = "SELECT APPROXIMATE_COUNT(DISTINCT (name)) FROM student"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(DISTINCT (name))'], + table: 'student', + whereClauses: [], + orderByFields: null, + isDistinct: false, + distinctFields: ['name'], + isCountDistinct: true, + isApproximateCount: true, groupByFields: null, joinType: null, joinTable: null, @@ -660,4 +836,47 @@ test('Parse SQL Query with LIKE and ORDER BY Clauses', () => { limit: null, hasAggregateWithoutGroupBy: false }); -}); \ No newline at end of file +}); + +test('Parse SQL Query with COUNT with DISTINCT on a column', () => { + const query = "SELECT COUNT(DISTINCT (name)) FROM student"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(DISTINCT (name))'], + table: 'student', + whereClauses: [], + orderByFields: null, + isDistinct: false, + distinctFields: ['name'], + isCountDistinct: true, + isApproximateCount: false, + groupByFields: null, + joinType: null, + joinTable: null, + joinCondition: null, + limit: null, + hasAggregateWithoutGroupBy: false + }); +}); + +test('Parse SQL Query with COUNT with DISTINCT on multiple column', () => { + const query = "SELECT COUNT(DISTINCT (name, age)) FROM student"; + const parsed = parseSelectQuery(query); + expect(parsed).toEqual({ + fields: ['COUNT(DISTINCT (name, age))'], + table: 'student', + whereClauses: [], + orderByFields: null, + isDistinct: false, + distinctFields: ['name', 'age'], + isCountDistinct: true, + isApproximateCount: false, + groupByFields: null, + joinType: null, + joinTable: null, + joinCondition: null, + limit: null, + hasAggregateWithoutGroupBy: false + }); +}); + From 19ade1128812d403ec2925281ee40636c619e4c1 Mon Sep 17 00:00:00 2001 From: Yash Mittal Date: Sat, 9 Mar 2024 16:50:48 +0530 Subject: [PATCH 14/16] docs: add tutorial for step 21 --- docs/tutorials/21.md | 173 ++++++++++++++++++++++++++++++ tests/appoximateLargeFile.test.js | 4 +- 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 docs/tutorials/21.md diff --git a/docs/tutorials/21.md b/docs/tutorials/21.md new file mode 100644 index 0000000..d662cb6 --- /dev/null +++ b/docs/tutorials/21.md @@ -0,0 +1,173 @@ +## Step 21: Add Approximate Counting using `HyperLogLog` + +### 21.1 First things first, create a utility to generate large files + +Since the effect of `HyperLogLog` is best seen on large files, we need to create a utility function which generates large files, let's say with `10_000_000` data points. To do this create a file named `generateLargeFile.js` in a `utils` folder and add the following logic to it. + +```js +const fs = require('fs'); +const { faker, da } = require('@faker-js/faker'); +const { parse } = require('json2csv'); + +async function generateLargeCSV(filename) { + let data = []; + for (let i = 1; i <= 10_000_000; i++) { + const record = { + id: i, + name: faker.person.firstName(), + age: faker.number.int({ min: 18, max: 100 }), + }; + data.push(record); + + let rows; + if (i % 500_000 === 0) { + console.log(`Generated ${i} records`); + if (!fs.existsSync(filename)) { + rows = parse(data, { header: true }); + } else { + // Rows without headers. + rows = parse(data, { header: false }); + } + fs.appendFileSync(filename, rows); + data = []; + } + + } + // Append file function can create new file too. + + // Always add new line if file already exists. + fs.appendFileSync(filename, "\r\n"); +} + +generateLargeCSV('student_large.csv') +``` + +### 21.2 Implement CSV reader for `HyperLogLog` + +Since `HyperLogLog` is a data structure which keeps data stored in a hashed format, we implement a separete CSV reader for it. Create a function named `readCSVforHLL` in your `csvStorage.js`. +Sample logic for it can be found here: +```js +function readCSVForHLL(filePath, bitSampleSize = 12, digestSize = 128) { + const results = []; + var h = hll({ bitSampleSize: bitSampleSize, digestSize: digestSize }); + + return new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(csv()) + .on('data', (data) => h.insert(JSON.stringify(data))) + .on('end', () => { + resolve(h); + }) + .on('error', (error) => { + reject(error); + }); + }); +} +``` + +### 21.3 Update the `queryParser` implementation to identify `COUNT` and `APPROXIMATE_COUNT` as valid tokens + +Since our SQL queries will now be accepting the `COUNT` and `APPROXIMATE_COUNT` tokens as valid tokens, we need to update the logic of our parser to identify and process them accordingly. Update your `queryParser.js` with the required logic (regex) to identify that. + + +### 21.4 Update the `executeSELECTQuery` function to add support for `COUNT` and `APPROXIMATE_COUNT` + +Update the existing logic in the `executeSELECTQuery` function to identify and process the `COUNT` and `APPROXIMATE_COUNT` commands in your SQL query. +Some snippets that might be helpful are: +```js +// getting approx counts +if (isApproximateCount && fields.length === 1 && fields[0] === 'COUNT(*)' && whereClauses.length === 0) { + let hll = await readCSVForHLL(`${table}.csv`); + return [{ 'APPROXIMATE_COUNT(*)': hll.estimate() }]; +} + + // Distinct inside count - example "SELECT COUNT (DISTINCT student.name) FROM student" + if (isCountDistinct) { + + if (isApproximateCount) { + var h = hll({ bitSampleSize: 12, digestSize: 128 }); + orderedResults.forEach(row => h.insert(distinctFields.map(field => row[field]).join('|'))); + return [{ [`APPROXIMATE_${fields[0]}`]: h.estimate() }]; + } + else { + let distinctResults = [...new Map(orderedResults.map(item => [distinctFields.map(field => item[field]).join('|'), item])).values()]; + return [{ [fields[0]]: distinctResults.length }]; + } + } +``` + + +### 21.5 Write a test case for approximate count + +Since we are following `TDD` in this tutorial, we are going to be writing a test case to test our implementation now. +Create a file named `approximateLargeFile.test.js` in your `tests` folder and add the following test cases: + +```js +const fs = require('fs'); +const { executeSELECTQuery } = require('../src/queryExecuter'); +const jestConsole = console; + +beforeEach(() => { + global.console = require('console'); +}); + +afterEach(() => { + global.console = jestConsole; +}); + +test('Large File Count(*) - Approximate and Exact', async () => { + // Test Exact Count + + const startMemoryUsageExact = process.memoryUsage().heapUsed; + const startTimeExact = performance.now(); + + const queryExact = "SELECT COUNT(*) FROM student_large"; + const resultExact = await executeSELECTQuery(queryExact); + const exactResult = resultExact[0]['COUNT(*)']; + + const endTimeExact = performance.now(); + const endMemoryUsageExact = process.memoryUsage().heapUsed; + + console.log(`Execution Time for Exact Count: ${(endTimeExact - startTimeExact).toFixed(2)} ms`); + console.log(`Start Memory for Exact Count: ${startMemoryUsageExact / 1024 / 1024} MB`); + console.log(`End Memory for Exact Count: ${endMemoryUsageExact / 1024 / 1024} MB`); + console.log(`Memory Used for Exact Count: ${(endMemoryUsageExact - startMemoryUsageExact) / 1024 / 1024} MB`); + + const startMemoryUsage = process.memoryUsage().heapUsed; + const startTime = performance.now(); + + const query = "SELECT APPROXIMATE_COUNT(*) FROM student_large"; + const result = await executeSELECTQuery(query); + + // Expect the approximate count to be within 5% of the actual count + expect(result[0]['APPROXIMATE_COUNT(*)']).toBeGreaterThan(exactResult - 0.05 * exactResult); + expect(result[0]['APPROXIMATE_COUNT(*)']).toBeLessThan(exactResult + 0.05 * exactResult); + + const endTime = performance.now(); + const endMemoryUsage = process.memoryUsage().heapUsed; + + console.log(`Execution Time for Approximate Count: ${(endTime - startTime).toFixed(2)} ms`); + console.log(`Start Memory: ${startMemoryUsage / 1024 / 1024} MB`); + console.log(`End Memory: ${endMemoryUsage / 1024 / 1024} MB`); + console.log(`Memory Used for Approximate Count: ${(endMemoryUsage - startMemoryUsage) / 1024 / 1024} MB`); + +}, 120000); + +test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { + const queryExact = "SELECT COUNT(DISTINCT (name, age)) FROM student_large"; + const resultExact = await executeSELECTQuery(queryExact); + console.log({ resultExact }); + const exactResult = resultExact[0]['COUNT(DISTINCT (name, age))']; + + const query = "SELECT APPROXIMATE_COUNT(DISTINCT (name, age)) FROM student_large"; + const result = await executeSELECTQuery(query); + + // Expect the approximate count to be within 2% of the actual count + expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeGreaterThan(exactResult - 0.05 * exactResult); + expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeLessThan(exactResult + 0.05 * exactResult); +}, 120000); +``` + +### 21.6 Update the tests for other files to test for the updates you made in other parts of the implementation + +Since we have made changes to the other parts of the implementation such as the `csvStorage.js`, `queryParser.js` and `queryExecutor.js` we need to update the tests for those files to test for the functionality. \ No newline at end of file diff --git a/tests/appoximateLargeFile.test.js b/tests/appoximateLargeFile.test.js index f8d241a..cb419da 100644 --- a/tests/appoximateLargeFile.test.js +++ b/tests/appoximateLargeFile.test.js @@ -46,7 +46,7 @@ test('Large File Count(*) - Approximate and Exact', async () => { console.log(`End Memory: ${endMemoryUsage / 1024 / 1024} MB`); console.log(`Memory Used for Approximate Count: ${(endMemoryUsage - startMemoryUsage) / 1024 / 1024} MB`); -}, 60000); +}, 120000); test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { const queryExact = "SELECT COUNT(DISTINCT (name, age)) FROM student_large"; @@ -60,4 +60,4 @@ test('Execute SQL Query with COUNT with DISTINCT on a column', async () => { // Expect the approximate count to be within 2% of the actual count expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeGreaterThan(exactResult - 0.05 * exactResult); expect(result[0]['APPROXIMATE_COUNT(DISTINCT (name, age))']).toBeLessThan(exactResult + 0.05 * exactResult); -}, 60000); \ No newline at end of file +}, 120000); \ No newline at end of file From cd269f0170d9724969ee7f31314450ccc9a9fb47 Mon Sep 17 00:00:00 2001 From: Yash Mittal Date: Sat, 9 Mar 2024 16:59:46 +0530 Subject: [PATCH 15/16] fix: generate command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83f7671..ba8cdcc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "doc": "docs" }, "scripts": { - "generate": "node ./tests/generateLargeFile.js", + "generate": "node ./util/generateLargeFile.js", "test": "jest", "test-verbose": "jest --verbose", "server": "node ./src/server.js" From add8143864a4c7d2ed12fbed537fc850a97c5544 Mon Sep 17 00:00:00 2001 From: Yash Mittal Date: Sat, 9 Mar 2024 21:16:49 +0530 Subject: [PATCH 16/16] fix: update workflows to not use node v14 --- .github/workflows/db-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/db-tests.yml b/.github/workflows/db-tests.yml index ddfbe76..2540e7c 100644 --- a/.github/workflows/db-tests.yml +++ b/.github/workflows/db-tests.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x] steps: - uses: actions/checkout@v3