diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/classroom.yml b/.github/workflows/classroom.yml index dca83b024..8c4fa1b7e 100644 --- a/.github/workflows/classroom.yml +++ b/.github/workflows/classroom.yml @@ -1,19 +1,220 @@ -name: GitHub Classroom Workflow - -on: - - push - - workflow_dispatch - +name: Autograding Tests +'on': +- push +- workflow_dispatch +- repository_dispatch permissions: checks: write actions: read contents: read - jobs: - build: - name: Autograding + run-autograding-tests: runs-on: ubuntu-latest if: github.actor != 'github-classroom[bot]' steps: - - uses: actions/checkout@v4 - - uses: education/autograding@v1 + - name: Checkout code + uses: actions/checkout@v4 + - name: Step-1 Test + id: step-1-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-1 Test + setup-command: npm install + command: npm run test:1 + timeout: 10 + max-score: 10 + - name: Step-2 Test + id: step-2-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-2 Test + setup-command: npm install + command: npm run test:2 + timeout: 10 + max-score: 10 + - name: Step-3 Test + id: step-3-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-3 Test + setup-command: npm install + command: npm run test:3 + timeout: 10 + max-score: 10 + - name: Step-4 Test + id: step-4-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-4 Test + setup-command: npm install + command: npm run test:4 + timeout: 10 + - name: Step-5 Test + id: step-5-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-5 Test + setup-command: npm install + command: npm run test:5 + timeout: 10 + max-score: 10 + - name: Step-6 Test + id: step-6-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-6 Test + setup-command: npm install + command: npm run test:6 + timeout: 10 + max-score: 10 + - name: Step-7 Test + id: step-7-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-7 Test + setup-command: npm install + command: npm run test:7 + timeout: 10 + max-score: 10 + - name: Step-8 Test + id: step-8-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-8 Test + setup-command: npm install + command: npm run test:8 + timeout: 10 + max-score: 10 + - name: Step-9 Test + id: step-9-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-9 Test + setup-command: npm install + command: npm run test:9 + timeout: 10 + max-score: 10 + - name: Step-10 Test + id: step-10-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-10 Test + setup-command: npm install + command: npm run test:10 + timeout: 10 + max-score: 10 + - name: Step-11 Test + id: step-11-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-11 Test + setup-command: npm install + command: npm run test:11 + timeout: 10 + max-score: 10 + - name: Step-12 Test + id: step-12-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-12 Test + setup-command: npm install + command: npm run test:12 + timeout: 10 + max-score: 10 + - name: Step-13 Test + id: step-13-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-13 Test + setup-command: npm install + command: npm run test:13 + timeout: 10 + max-score: 10 + - name: Step-14 Test + id: step-14-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-14 Test + setup-command: npm install + command: npm run test:14 + timeout: 10 + max-score: 10 + - name: Step-15 Test + id: step-15-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-15 Test + setup-command: npm install + command: npm run test:15 + timeout: 10 + max-score: 10 + - name: Step-16 Test + id: step-16-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-16 Test + setup-command: npm install + command: npm run test:16 + timeout: 10 + max-score: 10 + - name: Step-17 Test + id: step-17-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-17 Test + setup-command: npm install + command: npm run test:17 + timeout: 10 + max-score: 10 + - name: Step-18 Test + id: step-18-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-18 Test + setup-command: npm install + command: npm run test:18 + timeout: 10 + max-score: 10 + - name: Step-19 Test + id: step-19-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-19 Test + setup-command: npm install + command: npm run test:19 + timeout: 10 + max-score: 10 + - name: Step-20 Test + id: step-20-test + uses: education/autograding-command-grader@v1 + with: + test-name: Step-20 Test + setup-command: npm install + command: npm run test:20 + timeout: 10 + max-score: 10 + - name: Autograding Reporter + uses: education/autograding-grading-reporter@v1 + env: + STEP-1-TEST_RESULTS: "${{steps.step-1-test.outputs.result}}" + STEP-2-TEST_RESULTS: "${{steps.step-2-test.outputs.result}}" + STEP-3-TEST_RESULTS: "${{steps.step-3-test.outputs.result}}" + STEP-4-TEST_RESULTS: "${{steps.step-4-test.outputs.result}}" + STEP-5-TEST_RESULTS: "${{steps.step-5-test.outputs.result}}" + STEP-6-TEST_RESULTS: "${{steps.step-6-test.outputs.result}}" + STEP-7-TEST_RESULTS: "${{steps.step-7-test.outputs.result}}" + STEP-8-TEST_RESULTS: "${{steps.step-8-test.outputs.result}}" + STEP-9-TEST_RESULTS: "${{steps.step-9-test.outputs.result}}" + STEP-10-TEST_RESULTS: "${{steps.step-10-test.outputs.result}}" + STEP-11-TEST_RESULTS: "${{steps.step-11-test.outputs.result}}" + STEP-12-TEST_RESULTS: "${{steps.step-12-test.outputs.result}}" + STEP-13-TEST_RESULTS: "${{steps.step-13-test.outputs.result}}" + STEP-14-TEST_RESULTS: "${{steps.step-14-test.outputs.result}}" + STEP-15-TEST_RESULTS: "${{steps.step-15-test.outputs.result}}" + STEP-16-TEST_RESULTS: "${{steps.step-16-test.outputs.result}}" + STEP-17-TEST_RESULTS: "${{steps.step-17-test.outputs.result}}" + STEP-18-TEST_RESULTS: "${{steps.step-18-test.outputs.result}}" + STEP-19-TEST_RESULTS: "${{steps.step-19-test.outputs.result}}" + STEP-20-TEST_RESULTS: "${{steps.step-20-test.outputs.result}}" + with: + runners: step-1-test,step-2-test,step-3-test,step-4-test,step-5-test,step-6-test,step-7-test,step-8-test,step-9-test,step-10-test,step-11-test,step-12-test,step-13-test,step-14-test,step-15-test,step-16-test,step-17-test,step-18-test,step-19-test,step-20-test diff --git a/README.md b/README.md index eadfc715a..f6cda1305 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[](https://classroom.github.com/online_ide?assignment_repo_id=14724474&assignment_repo_type=AssignmentRepo)
A SQL database engine written in JavaScript diff --git a/enrollment.csv b/enrollment.csv new file mode 100644 index 000000000..e80af8d93 --- /dev/null +++ b/enrollment.csv @@ -0,0 +1,6 @@ +student_id,course +1,Mathematics +1,Physics +2,Chemistry +3,Mathematics +5,Biology \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3afaec37f..3c1d6eacb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,15 @@ "version": "0.1.6", "license": "ISC", "dependencies": { - "csv-parser": "^3.0.0", "json2csv": "^6.0.0-alpha.2", + "stylusdb-sql": "^0.1.6", "xterm": "^5.3.0" }, "bin": { "stylusdb-cli": "node ./src/cli.js" }, "devDependencies": { + "csv-parser": "^3.0.0", "jest": "^29.7.0" } }, @@ -3431,6 +3432,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylusdb-sql": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/stylusdb-sql/-/stylusdb-sql-0.1.6.tgz", + "integrity": "sha512-lhGPFmx0eFeJtWW9Gp7fC7ZzavkbaO/WPQrv6pyc4TuxQTiYzJkhkAGfXgv8ApSjj6cWjA2RXGQIsIwwe1mTfw==", + "dependencies": { + "csv-parser": "^3.0.0", + "json2csv": "^6.0.0-alpha.2", + "xterm": "^5.3.0" + }, + "bin": { + "stylusdb-cli": "node ./src/cli.js" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index f52103d5c..57ecb1c8e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "csv-parser": "^3.0.0", "json2csv": "^6.0.0-alpha.2", + "stylusdb-sql": "^0.1.6", "xterm": "^5.3.0" } -} \ No newline at end of file +} diff --git a/sample.csv b/sample.csv new file mode 100644 index 000000000..9e7a9fa25 --- /dev/null +++ b/sample.csv @@ -0,0 +1,4 @@ +id,name,age +1,John,30 +2,Jane,25 +3,Bob,22 \ No newline at end of file diff --git a/src/STEP-11/queryExecute.js b/src/STEP-11/queryExecute.js new file mode 100644 index 000000000..59b66c389 --- /dev/null +++ b/src/STEP-11/queryExecute.js @@ -0,0 +1,272 @@ +const { parseQuery } = require('./queryParser'); +const readCSV = require('../csvReader'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, orderByFields } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + + // console.log({ filteredData }) + + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + return groupResults; + } else { + + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + + // Select the specified fields + return orderedResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + } +} + + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/STEP-11/queryParser.js b/src/STEP-11/queryParser.js new file mode 100644 index 000000000..bd95dc846 --- /dev/null +++ b/src/STEP-11/queryParser.js @@ -0,0 +1,99 @@ +function parseQuery(query) { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + + // Updated regex to capture ORDER BY clause + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(',').map(field => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : 'ASC' }; + }); + } + + // Remove ORDER BY clause from the query for further processing + query = query.replace(orderByRegex, ''); + + // Split the query at the GROUP BY clause if it exists + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(',').map(field => field.trim()); + } + // Remove GROUP BY clause from the query for further processing + query = query.replace(groupByRegex, ''); + + // Split the query at the WHERE clause if it exists + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + + // WHERE clause is the second part after splitting, if it exists + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Split the remaining query at the JOIN clause if it exists + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Parse the 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; + // Extract JOIN information + const { joinType, joinTable, joinCondition } = parseJoinClause(queryWithoutWhere); + // Parse the WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for the presence of aggregate functions without GROUP BY + const aggregateFunctionRegex = /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + const hasAggregateWithoutGroupBy = aggregateFunctionRegex.test(query) && !groupByFields; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy + }; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + }); +} +function parseJoinClause(query) { + const joinRegex = /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim() + } + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null + }; +} +module.exports = { parseQuery, parseJoinClause }; \ No newline at end of file diff --git a/src/csvReadWrite.js b/src/csvReadWrite.js new file mode 100644 index 000000000..64ce5c0d1 --- /dev/null +++ b/src/csvReadWrite.js @@ -0,0 +1,25 @@ +const fs = require("fs"); +const csv = require("csv-parser"); +const { parse } = require("json2csv"); + +function readCSV(filePath) { + const results = []; + return new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(csv()) + .on("data", (data) => results.push(data)) + .on("end", () => { + resolve(results); + }) + .on("error", (error) => { + reject(error); + }); + }); +} + +async function writeCSV(filename, data) { + const csv = parse(data); + fs.writeFileSync(filename, csv); +} + +module.exports = { readCSV, writeCSV }; diff --git a/src/csvReader.js b/src/csvReader.js index e69de29bb..d6851c1aa 100644 --- a/src/csvReader.js +++ b/src/csvReader.js @@ -0,0 +1,19 @@ +const fs = require('fs'); +const csv = require('csv-parser'); + +const readCSV = (filepath)=>{ + const result = []; + + return new Promise((resolve,reject)=>{ + fs.createReadStream(filepath).pipe(csv()) + .on('data',(data)=> result.push(data)) + .on('end',()=>{ + resolve(result); + }) + .on('error',(error)=>{ + reject(error); + }) + }) +} + +module.exports = readCSV; \ No newline at end of file diff --git a/src/step-03/queryParser.js b/src/step-03/queryParser.js new file mode 100644 index 000000000..8cba13b8a --- /dev/null +++ b/src/step-03/queryParser.js @@ -0,0 +1,16 @@ +function parseQuery(query) { + const selectRegex = /SELECT (.+) FROM (.+)/i; + const match = query.match(selectRegex); + + if (match) { + const [, fields, table] = match; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim() + }; + } else { + throw new Error('Invalid query format'); + } +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-04/queryExecute.js b/src/step-04/queryExecute.js new file mode 100644 index 000000000..b648b72a7 --- /dev/null +++ b/src/step-04/queryExecute.js @@ -0,0 +1,26 @@ +const parseQuery = require('./queryParse'); +const readCSV = require('../csvReader'); + +async function executeSELECTQuery(query) { + const { fields, table, whereClause } = parseQuery(query); + const data = await readCSV(`${table}.csv`); + + // Filtering based on WHERE clause + const filteredData = whereClause + ? data.filter(row => { + const [field, value] = whereClause.split('=').map(s => s.trim()); + return row[field] === value; + }) + : data; + + // Selecting the specified fields + return filteredData.map(row => { + const selectedRow = {}; + fields.forEach(field => { + selectedRow[field] = row[field]; + }); + return selectedRow; + }); +} + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-04/queryParse.js b/src/step-04/queryParse.js new file mode 100644 index 000000000..8316601fa --- /dev/null +++ b/src/step-04/queryParse.js @@ -0,0 +1,17 @@ +function parseQuery(query) { + const selectRegex = /SELECT (.+?) FROM (.+?)(?: WHERE (.*))?$/i; + const match = query.match(selectRegex); + + if (match) { + const [, fields, table, whereClause] = match; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClause: whereClause ? whereClause.trim() : null + }; + } else { + throw new Error('Invalid query format'); + } +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-05/queryExecute.js b/src/step-05/queryExecute.js new file mode 100644 index 000000000..b648b72a7 --- /dev/null +++ b/src/step-05/queryExecute.js @@ -0,0 +1,26 @@ +const parseQuery = require('./queryParse'); +const readCSV = require('../csvReader'); + +async function executeSELECTQuery(query) { + const { fields, table, whereClause } = parseQuery(query); + const data = await readCSV(`${table}.csv`); + + // Filtering based on WHERE clause + const filteredData = whereClause + ? data.filter(row => { + const [field, value] = whereClause.split('=').map(s => s.trim()); + return row[field] === value; + }) + : data; + + // Selecting the specified fields + return filteredData.map(row => { + const selectedRow = {}; + fields.forEach(field => { + selectedRow[field] = row[field]; + }); + return selectedRow; + }); +} + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-05/queryParse.js b/src/step-05/queryParse.js new file mode 100644 index 000000000..8316601fa --- /dev/null +++ b/src/step-05/queryParse.js @@ -0,0 +1,17 @@ +function parseQuery(query) { + const selectRegex = /SELECT (.+?) FROM (.+?)(?: WHERE (.*))?$/i; + const match = query.match(selectRegex); + + if (match) { + const [, fields, table, whereClause] = match; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClause: whereClause ? whereClause.trim() : null + }; + } else { + throw new Error('Invalid query format'); + } +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-06/queryExecute.js b/src/step-06/queryExecute.js new file mode 100644 index 000000000..b5e046de5 --- /dev/null +++ b/src/step-06/queryExecute.js @@ -0,0 +1,26 @@ +const parseQuery = require('./queryParse'); +const readCSV = require('../csvReader'); + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses } = parseQuery(query); + const data = await readCSV(`${table}.csv`); + + // Apply WHERE clause filtering + const filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => { + // You can expand this to handle different operators + return row[clause.field] === clause.value; + })) + : data; + + // Select the specified fields + return filteredData.map(row => { + const selectedRow = {}; + fields.forEach(field => { + selectedRow[field] = row[field]; + }); + return selectedRow; + }); +} + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-06/queryParse.js b/src/step-06/queryParse.js new file mode 100644 index 000000000..82745d43b --- /dev/null +++ b/src/step-06/queryParse.js @@ -0,0 +1,26 @@ +function parseQuery(query) { + const selectRegex = /SELECT (.+?) FROM (.+?)(?: WHERE (.*))?$/i; + const match = query.match(selectRegex); + + if (match) { + const [, fields, table, whereString] = match; + const whereClauses = whereString ? parseWhereClause(whereString) : []; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses + }; + } else { + throw new Error('Invalid query format'); + } +} + +function parseWhereClause(whereString) { + const conditions = whereString.split(/ AND | OR /i); + return conditions.map(condition => { + const [field, operator, value] = condition.split(/\s+/); + return { field, operator, value }; + }); +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-07/queryExecute.js b/src/step-07/queryExecute.js new file mode 100644 index 000000000..6cb890bbd --- /dev/null +++ b/src/step-07/queryExecute.js @@ -0,0 +1,36 @@ +const parseQuery = require('./queryParse'); +const readCSV = require('../csvReader'); + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses } = parseQuery(query); + const data = await readCSV(`${table}.csv`,fields); + + // Apply WHERE clause filtering + const filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row,clause))) + : data; + + // Select the specified fields + return filteredData.map(row => { + const selectedRow = {}; + fields.forEach(field => { + selectedRow[field] = row[field]; + }); + return selectedRow; + }); +} + +function evaluateCondition(row, clause) { + const { field, operator, value } = clause; + switch (operator) { + case '=': return row[field] === value; + case '!=': return row[field] !== value; + case '>': return row[field] > value; + case '<': return row[field] < value; + case '>=': return row[field] >= value; + case '<=': return row[field] <= value; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-07/queryParse.js b/src/step-07/queryParse.js new file mode 100644 index 000000000..1b21e0121 --- /dev/null +++ b/src/step-07/queryParse.js @@ -0,0 +1,31 @@ +function parseQuery(query) { + const selectRegex = /SELECT (.+?) FROM (.+?)(?: WHERE (.*))?$/i; + const match = query.match(selectRegex); + + if (match) { + const [, fields, table, whereString] = match; + const whereClauses = whereString ? parseWhereClause(whereString) : []; + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses + }; + } else { + throw new Error('Invalid query format'); + } +} + + +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + }); +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-08/queryExecute.js b/src/step-08/queryExecute.js new file mode 100644 index 000000000..b54617277 --- /dev/null +++ b/src/step-08/queryExecute.js @@ -0,0 +1,65 @@ +const parseQuery = require("./queryParse"); +const readCSV = require("../csvReader"); + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses, joinTable, joinCondition } = + parseQuery(query); + let data = await readCSV(`${table}.csv`,fields); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`,fields); + data = data.flatMap((mainRow) => { + return joinData + .filter((joinRow) => { + const mainValue = mainRow[joinCondition.left.split(".")[1]]; + const joinValue = joinRow[joinCondition.right.split(".")[1]]; + return mainValue === joinValue; + }) + .map((joinRow) => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split("."); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); + } + + const filteredData = + whereClauses.length > 0 + ? data.filter((row) => + whereClauses.every((clause) => evaluateCondition(row, clause)) + ) + : data; + return filteredData.map((row) => { + const selectedRow = {}; + fields.forEach(field => { + selectedRow[field] = row[field]; + }) + return selectedRow; + }); + +} + +function evaluateCondition(row, clause) { + const { field, operator, value } = clause; + switch (operator) { + case "=": + return row[field] === value; + case "!=": + return row[field] !== value; + case ">": + return row[field] > value; + case "<": + return row[field] < value; + case ">=": + return row[field] >= value; + case "<=": + return row[field] <= value; + default: + throw new Error(`Unsupported operator: ${operator}`); + } +} + +module.exports = executeSELECTQuery; diff --git a/src/step-08/queryParse.js b/src/step-08/queryParse.js new file mode 100644 index 000000000..26846dfce --- /dev/null +++ b/src/step-08/queryParse.js @@ -0,0 +1,74 @@ +function parseQuery(query) { + // First, let's trim the query to remove any leading/trailing whitespaces + query = query.trim(); + + // Initialize variables for different parts of the query + let selectPart, fromPart; + + // Split the query at the WHERE clause if it exists + const whereSplit = query.split(/\sWHERE\s/i); + query = whereSplit[0]; // Everything before WHERE clause + + // WHERE clause is the second part after splitting, if it exists + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + + // Split the remaining query at the JOIN clause if it exists + const joinSplit = query.split(/\sINNER JOIN\s/i); + selectPart = joinSplit[0].trim(); // Everything before JOIN clause + + // JOIN clause is the second part after splitting, if it exists + const joinPart = joinSplit.length > 1 ? joinSplit[1].trim() : null; + + // Parse the 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; + + // Parse the JOIN part if it exists + let joinTable = null, joinCondition = null; + if (joinPart) { + const joinRegex = /^(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = joinPart.match(joinRegex); + if (!joinMatch) { + throw new Error('Invalid JOIN format'); + } + + joinTable = joinMatch[1].trim(); + joinCondition = { + left: joinMatch[2].trim(), + right: joinMatch[3].trim() + }; + } + + // Parse the WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses, + joinTable, + joinCondition + }; +} + +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + }); +} + +module.exports = parseQuery; \ No newline at end of file diff --git a/src/step-09/queryExecute.js b/src/step-09/queryExecute.js new file mode 100644 index 000000000..f47c79d02 --- /dev/null +++ b/src/step-09/queryExecute.js @@ -0,0 +1,177 @@ + +const {parseQuery, parseJoinClause} = require('../step-09/queryParser'); +const readCSV = require('../csvReader'); + +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} + +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} + +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} + +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} + +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + + return resultRow; +} + +async function executeSELECTQuery(query) { + + const { fields, table, whereClauses, joinType, joinTable, joinCondition } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + + console.log("AFTER JOIN", data); + console.log("WHERE CLAUSES", whereClauses); + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + const filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + + // console.log("AFTER WHERE", filteredData); + + // Select the specified fields + return filteredData.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); +} + +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + + // console.log("EVALUATING", rowValue, operator, conditionValue, typeof (rowValue), typeof (conditionValue)); + + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} + +// Helper function to parse value based on its apparent type +function parseValue(value) { + + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} + + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-09/queryParser.js b/src/step-09/queryParser.js new file mode 100644 index 000000000..0963ef645 --- /dev/null +++ b/src/step-09/queryParser.js @@ -0,0 +1,73 @@ +function parseQuery(query) { + // First, let's trim the query to remove any leading/trailing whitespaces + query = query.trim(); + // Initialize variables for different parts of the query + let selectPart, fromPart; + // Split the query at the WHERE clause if it exists + const whereSplit = query.split(/\sWHERE\s/i); + query = whereSplit[0]; // Everything before WHERE clause + // WHERE clause is the second part after splitting, if it exists + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + + // Split the remaining query at the JOIN clause if it exists + const joinSplit = query.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Parse the 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; + const { joinType, joinTable, joinCondition } = parseJoinClause(query); + + // Parse the WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition + }; + } + function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + }); + } + function parseJoinClause(query) { + const joinRegex = /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim() + } + }; + } + + + return { + joinType: null, + joinTable: null, + joinCondition: null + }; + } + + module.exports = { parseQuery, parseJoinClause }; \ No newline at end of file diff --git a/src/step-10/queryExecute.js b/src/step-10/queryExecute.js new file mode 100644 index 000000000..2c90cf58f --- /dev/null +++ b/src/step-10/queryExecute.js @@ -0,0 +1,254 @@ +const { parseQuery } = require('../step-10/queryParser'); +const readCSV = require('../csvReader'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + + let groupResults = filteredData; + console.log({ hasAggregateWithoutGroupBy }); + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + + console.log({ filteredData }) + + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + return groupResults; + } else { + // Select the specified fields + return groupResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + } +} + +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} + +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + + return finalGroup; + }); +} + + +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-10/queryParser.js b/src/step-10/queryParser.js new file mode 100644 index 000000000..ec59bb51f --- /dev/null +++ b/src/step-10/queryParser.js @@ -0,0 +1,96 @@ +function parseQuery(query) { + query = query.trim(); + + // Split the query at the GROUP BY clause if it exists + const groupBySplit = query.split(/\sGROUP BY\s/i); + const queryWithoutGroupBy = groupBySplit[0]; // Everything before GROUP BY clause + + // GROUP BY clause is the second part after splitting, if it exists + let groupByFields = + groupBySplit.length > 1 + ? groupBySplit[1] + .trim() + .split(",") + .map((field) => field.trim()) + : null; + + // Split the query at the WHERE clause if it exists + const whereSplit = queryWithoutGroupBy.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + + // WHERE clause is the second part after splitting, if it exists + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + + // Split the remaining query at the JOIN clause if it exists + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + + // Parse the 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; + + // Extract JOIN information + const { joinType, joinTable, joinCondition } = + parseJoinClause(queryWithoutWhere); + + // Parse the WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + + // Check for the presence of aggregate functions without GROUP BY + const aggregateFunctionRegex = + /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + const hasAggregateWithoutGroupBy = + aggregateFunctionRegex.test(query) && !groupByFields; + + return { + fields: fields.split(",").map((field) => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + hasAggregateWithoutGroupBy, + }; +} + +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map((conditionString) => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error("Invalid WHERE clause format"); + }); +} +function parseJoinClause(query) { + const joinRegex = + /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim(), + }, + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null, + }; +} +module.exports = { parseQuery, parseJoinClause }; diff --git a/src/step-12/queryExecute.js b/src/step-12/queryExecute.js new file mode 100644 index 000000000..c5e0d7bf6 --- /dev/null +++ b/src/step-12/queryExecute.js @@ -0,0 +1,273 @@ +const { parseQuery } = require('./queryParser'); +const readCSV = require('../csvReader'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} + +async function executeSELECTQuery(query) { + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, orderByFields, limit } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + + // console.log({ filteredData }) + + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + + if (limit !== null) { + orderedResults = orderedResults.slice(0, limit); + } + + // Select the specified fields + return orderedResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + } +} +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-12/queryParser.js b/src/step-12/queryParser.js new file mode 100644 index 000000000..a4d81185c --- /dev/null +++ b/src/step-12/queryParser.js @@ -0,0 +1,121 @@ +function parseQuery(query) { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ""); // Remove LIMIT clause + } + + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(",").map((field) => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : "ASC" }; + }); + query = query.replace(orderByRegex, ""); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(",").map((field) => field.trim()); + query = query.replace(groupByRegex, ""); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + + // WHERE clause is the second part after splitting, if it exists + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + + // Parse the SELECT part + // Extract JOIN information + const { joinType, joinTable, joinCondition } = + parseJoinClause(queryWithoutWhere); + + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy( + query, + groupByFields + ); + + return { + fields: fields.split(",").map((field) => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + }; +} + +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = + /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} + +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map((conditionString) => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error("Invalid WHERE clause format"); + }); +} +function parseJoinClause(query) { + const joinRegex = + /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim(), + }, + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null, + }; +} +module.exports = { parseQuery, parseJoinClause }; diff --git a/src/step-13/queryExecute.js b/src/step-13/queryExecute.js new file mode 100644 index 000000000..aff8771d3 --- /dev/null +++ b/src/step-13/queryExecute.js @@ -0,0 +1,278 @@ +const { parseQuery } = require('./queryParser'); +const readCSV = require('../csvReader'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} + +async function queryExecute(query) { + try { + + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, orderByFields, limit } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + orderedResults = orderedResults.slice(0, limit); + } + + // Select the specified fields + return orderedResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + } + } catch (error) { + throw new Error(`Error executing query: ${error.message}`); + } +} + +module.exports = queryExecute; \ No newline at end of file diff --git a/src/step-13/queryParser.js b/src/step-13/queryParser.js new file mode 100644 index 000000000..a567b8cc9 --- /dev/null +++ b/src/step-13/queryParser.js @@ -0,0 +1,114 @@ +function parseQuery(query) { + try { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ""); // Remove LIMIT clause + } + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(",").map((field) => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : "ASC" }; + }); + query = query.replace(orderByRegex, ""); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(",").map((field) => field.trim()); + query = query.replace(groupByRegex, ""); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Extract JOIN information + const { joinType, joinTable, joinCondition } = + parseJoinClause(queryWithoutWhere); + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy( + query, + groupByFields + ); + + return { + fields: fields.split(",").map((field) => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + }; + } catch (error) { + console.log(error.message); + throw new Error(`Query parsing error: ${error.message}`); + } +} + +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = + /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map((conditionString) => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error("Invalid WHERE clause format"); + }); +} +function parseJoinClause(query) { + const joinRegex = + /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim(), + }, + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null, + }; +} +module.exports = { parseQuery, parseJoinClause }; diff --git a/src/step-14/queryExecute.js b/src/step-14/queryExecute.js new file mode 100644 index 000000000..9a2174ec6 --- /dev/null +++ b/src/step-14/queryExecute.js @@ -0,0 +1,351 @@ +const { parseQuery } = require("./queryParser"); +const readCSV = require("../csvReader"); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap((mainRow) => { + return joinData + .filter((joinRow) => { + const mainValue = mainRow[joinCondition.left.split(".")[1]]; + const joinValue = joinRow[joinCondition.right.split(".")[1]]; + return mainValue === joinValue; + }) + .map((joinRow) => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split("."); + acc[field] = + tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap((mainRow) => { + const matchingJoinRows = joinData.filter((joinRow) => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map((joinRow) => + createResultRow(mainRow, joinRow, fields, table, true) + ); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split("."); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = + data.length > 0 + ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) + : {}; + return joinData.map((joinRow) => { + const mainRowMatch = data.find((mainRow) => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow( + mainRow, + joinRow, + fields, + table, + includeAllMainFields +) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach((key) => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach((field) => { + const [tableName, fieldName] = field.includes(".") + ? field.split(".") + : [table, field]; + resultRow[field] = + tableName === table && mainRow + ? mainRow[fieldName] + : joinRow + ? joinRow[fieldName] + : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + switch (operator) { + case "=": + return rowValue === conditionValue; + case "!=": + return rowValue !== conditionValue; + case ">": + return rowValue > conditionValue; + case "<": + return rowValue < conditionValue; + case ">=": + return rowValue >= conditionValue; + case "<=": + return rowValue <= conditionValue; + default: + throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if ( + typeof value === "string" && + ((value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"'))) + ) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== "") { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach((row) => { + // Generate a key for the group + const groupKey = groupByFields.map((field) => row[field]).join("-"); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach( + (field) => (groupResults[groupKey][field] = row[field]) + ); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach((func) => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case "SUM": + groupResults[groupKey].sums[aggField] = + (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case "MIN": + groupResults[groupKey].mins[aggField] = Math.min( + groupResults[groupKey].mins[aggField] || value, + value + ); + break; + case "MAX": + groupResults[groupKey].maxes[aggField] = Math.max( + groupResults[groupKey].maxes[aggField] || value, + value + ); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map((group) => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach((field) => (finalGroup[field] = group[field])); + aggregateFunctions.forEach((func) => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case "SUM": + finalGroup[func] = group.sums[aggField]; + break; + case "MIN": + finalGroup[func] = group.mins[aggField]; + break; + case "MAX": + finalGroup[func] = group.maxes[aggField]; + break; + case "COUNT": + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} +async function executeSELECTQuery(query) { + try { + const { + fields, + table, + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + hasAggregateWithoutGroupBy, + orderByFields, + limit, + isDistinct, + } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case "INNER": + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case "LEFT": + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case "RIGHT": + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = + whereClauses.length > 0 + ? data.filter((row) => + whereClauses.every((clause) => evaluateCondition(row, clause)) + ) + : data; + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + fields.forEach((field) => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case "COUNT": + result[field] = filteredData.length; + break; + case "SUM": + result[field] = filteredData.reduce( + (acc, row) => acc + parseFloat(row[aggField]), + 0 + ); + break; + case "AVG": + result[field] = + filteredData.reduce( + (acc, row) => acc + parseFloat(row[aggField]), + 0 + ) / filteredData.length; + break; + case "MIN": + result[field] = Math.min( + ...filteredData.map((row) => parseFloat(row[aggField])) + ); + break; + case "MAX": + result[field] = Math.max( + ...filteredData.map((row) => parseFloat(row[aggField])) + ); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === "ASC" ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === "ASC" ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === "ASC" ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === "ASC" ? 1 : -1; + } + return 0; + }); + } + let finalResults = orderedResults.map((row) => { + const selectedRow = {}; + fields.forEach((field) => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + + // Remove duplicates if specified + let distinctResults = finalResults; + if (isDistinct) { + distinctResults = [ + ...new Map( + finalResults.map((item) => [ + fields.map((field) => item[field]).join("|"), + item, + ]) + ).values(), + ]; + } + + let limitResults = distinctResults; + if (limit !== null) { + limitResults = distinctResults.slice(0, limit); + } + + return limitResults; + } + } catch (error) { + throw new Error(`Error executing query: ${error.message}`); + } +} +module.exports = executeSELECTQuery; diff --git a/src/step-14/queryParser.js b/src/step-14/queryParser.js new file mode 100644 index 000000000..bb742d80d --- /dev/null +++ b/src/step-14/queryParser.js @@ -0,0 +1,118 @@ + +function parseQuery(query) { + try { + + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + + // Initialize distinct flag + let isDistinct = false; + + // Check for DISTINCT keyword and update the query + if (query.toUpperCase().includes('SELECT DISTINCT')) { + isDistinct = true; + query = query.replace('SELECT DISTINCT', 'SELECT'); + } + + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ''); // Remove LIMIT clause + } + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(',').map(field => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : 'ASC' }; + }); + query = query.replace(orderByRegex, ''); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(',').map(field => field.trim()); + query = query.replace(groupByRegex, ''); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Extract JOIN information + const { joinType, joinTable, joinCondition } = parseJoinClause(queryWithoutWhere); + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy(query, groupByFields); + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + isDistinct + }; + } catch (error) { + throw new Error(`Query parsing error: ${error.message}`); + } +} +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + }); +} +function parseJoinClause(query) { + const joinRegex = /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim() + } + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null + }; +} +module.exports = { parseQuery, parseJoinClause }; \ No newline at end of file diff --git a/src/step-15&16/queryExecute.js b/src/step-15&16/queryExecute.js new file mode 100644 index 000000000..3d50660d3 --- /dev/null +++ b/src/step-15&16/queryExecute.js @@ -0,0 +1,284 @@ +const { parseQuery } = require('./queryParser'); +const readCSV = require('../csvReader'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + + if (operator === 'LIKE') { + // Transform SQL LIKE pattern to JavaScript RegExp pattern + const regexPattern = '^' + value.replace(/%/g, '.*').replace(/_/g, '.') + '$'; + const regex = new RegExp(regexPattern, 'i'); // 'i' for case-insensitive matching + return regex.test(row[field]); + } + + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} +async function executeSELECTQuery(query) { + try { + const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy, orderByFields, limit, isDistinct } = parseQuery(query); + let data = await readCSV(`${table}.csv`); + // Perform INNER JOIN if specified + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + // Select the specified fields + let finalResults = orderedResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + // Remove duplicates if specified + let distinctResults = finalResults; + if (isDistinct) { + distinctResults = [...new Map(finalResults.map(item => [fields.map(field => item[field]).join('|'), item])).values()]; + } + let limitResults = distinctResults; + if (limit !== null) { + limitResults = distinctResults.slice(0, limit); + } + return limitResults; + } + } catch (error) { + throw new Error(`Error executing query: ${error.message}`); + } +} +module.exports = executeSELECTQuery; \ No newline at end of file diff --git a/src/step-15&16/queryParser.js b/src/step-15&16/queryParser.js new file mode 100644 index 000000000..2824ef0a8 --- /dev/null +++ b/src/step-15&16/queryParser.js @@ -0,0 +1,130 @@ +function parseQuery(query) { + try { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + // Initialize distinct flag + let isDistinct = false; + // Check for DISTINCT keyword and update the query + if (query.toUpperCase().includes("SELECT DISTINCT")) { + isDistinct = true; + query = query.replace("SELECT DISTINCT", "SELECT"); + } + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ""); // Remove LIMIT clause + } + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(",").map((field) => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : "ASC" }; + }); + query = query.replace(orderByRegex, ""); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(",").map((field) => field.trim()); + query = query.replace(groupByRegex, ""); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Extract JOIN information + const { joinType, joinTable, joinCondition } = + parseJoinClause(queryWithoutWhere); + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy( + query, + groupByFields + ); + return { + fields: fields.split(",").map((field) => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + isDistinct, + }; + } catch (error) { + throw new Error(`Query parsing error: ${error.message}`); + } +} +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = + /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>|<|>=|<=)(.*)/; + return whereString.split(/ AND | OR /i).map((conditionString) => { + if (conditionString.includes(" LIKE ")) { + console.log(conditionString); + const [field, pattern] = conditionString.split(/\sLIKE\s/i); + return { + field: field.trim(), + operator: "LIKE", + value: pattern.trim().replace(/^'(.*)'$/, "$1"), + }; + } else { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error("Invalid WHERE clause format"); + } + }); +} + +function parseJoinClause(query) { + const joinRegex = + /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim(), + }, + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null, + }; +} +module.exports = { parseQuery, parseJoinClause }; diff --git a/src/step-17/queryExecute.js b/src/step-17/queryExecute.js new file mode 100644 index 000000000..b14a6399c --- /dev/null +++ b/src/step-17/queryExecute.js @@ -0,0 +1,383 @@ +const { parseSelectQuery, parseInsertQuery } = require('./queryParser'); +const { readCSV, writeCSV } = require('../csvReadWrite'); +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap((mainRow) => { + return joinData + .filter((joinRow) => { + const mainValue = mainRow[joinCondition.left.split(".")[1]]; + const joinValue = joinRow[joinCondition.right.split(".")[1]]; + return mainValue === joinValue; + }) + .map((joinRow) => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split("."); + acc[field] = + tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); + } + function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap((mainRow) => { + const matchingJoinRows = joinData.filter((joinRow) => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map((joinRow) => + createResultRow(mainRow, joinRow, fields, table, true) + ); + }); + } + function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split("."); + return row[`${tableName}.${fieldName}`] || row[fieldName]; + } + function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = + data.length > 0 + ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) + : {}; + return joinData.map((joinRow) => { + const mainRowMatch = data.find((mainRow) => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); + } + function createResultRow( + mainRow, + joinRow, + fields, + table, + includeAllMainFields + ) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach((key) => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach((field) => { + const [tableName, fieldName] = field.includes(".") + ? field.split(".") + : [table, field]; + resultRow[field] = + tableName === table && mainRow + ? mainRow[fieldName] + : joinRow + ? joinRow[fieldName] + : null; + }); + return resultRow; + } + function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + if (operator === "LIKE") { + // Transform SQL LIKE pattern to JavaScript RegExp pattern + const regexPattern = + "^" + value.replace(/%/g, ".*").replace(/_/g, ".") + "$"; + const regex = new RegExp(regexPattern, "i"); // 'i' for case-insensitive matching + return regex.test(row[field]); + } + switch (operator) { + case "=": + return rowValue === conditionValue; + case "!=": + return rowValue !== conditionValue; + case ">": + return rowValue > conditionValue; + case "<": + return rowValue < conditionValue; + case ">=": + return rowValue >= conditionValue; + case "<=": + return rowValue <= conditionValue; + default: + throw new Error(`Unsupported operator: ${operator}`); + } + } + // Helper function to parse value based on its apparent type + function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if ( + typeof value === "string" && + ((value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"'))) + ) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== "") { + return Number(value); + } + // Assume value is a string if not a number + return value; + } + function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach((row) => { + // Generate a key for the group + const groupKey = groupByFields.map((field) => row[field]).join("-"); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach( + (field) => (groupResults[groupKey][field] = row[field]) + ); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach((func) => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case "SUM": + groupResults[groupKey].sums[aggField] = + (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case "MIN": + groupResults[groupKey].mins[aggField] = Math.min( + groupResults[groupKey].mins[aggField] || value, + value + ); + break; + case "MAX": + groupResults[groupKey].maxes[aggField] = Math.max( + groupResults[groupKey].maxes[aggField] || value, + value + ); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map((group) => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach((field) => (finalGroup[field] = group[field])); + aggregateFunctions.forEach((func) => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case "SUM": + finalGroup[func] = group.sums[aggField]; + break; + case "MIN": + finalGroup[func] = group.mins[aggField]; + break; + case "MAX": + finalGroup[func] = group.maxes[aggField]; + break; + case "COUNT": + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); + } + async function executeSELECTQuery(query) { + try { + 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 + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case "INNER": + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case "LEFT": + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case "RIGHT": + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = + whereClauses.length > 0 + ? data.filter((row) => + whereClauses.every((clause) => evaluateCondition(row, clause)) + ) + : data; + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + fields.forEach((field) => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case "COUNT": + result[field] = filteredData.length; + break; + case "SUM": + result[field] = filteredData.reduce( + (acc, row) => acc + parseFloat(row[aggField]), + 0 + ); + break; + case "AVG": + result[field] = + filteredData.reduce( + (acc, row) => acc + parseFloat(row[aggField]), + 0 + ) / filteredData.length; + break; + case "MIN": + result[field] = Math.min( + ...filteredData.map((row) => parseFloat(row[aggField])) + ); + break; + case "MAX": + result[field] = Math.max( + ...filteredData.map((row) => parseFloat(row[aggField])) + ); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === "ASC" ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === "ASC" ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === "ASC" ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === "ASC" ? 1 : -1; + } + return 0; + }); + } + // Select the specified fields + let finalResults = orderedResults.map((row) => { + const selectedRow = {}; + fields.forEach((field) => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + // Remove duplicates if specified + let distinctResults = finalResults; + if (isDistinct) { + distinctResults = [ + ...new Map( + finalResults.map((item) => [ + fields.map((field) => item[field]).join("|"), + item, + ]) + ).values(), + ]; + } + let limitResults = distinctResults; + if (limit !== null) { + limitResults = distinctResults.slice(0, limit); + } + return limitResults; + } + } catch (error) { + throw new Error(`Error executing query: ${error.message}`); + } + } + + async function executeINSERTQuery(query) { + console.log(parseInsertQuery(query)); + const { table, columns, values } = parseInsertQuery(query); + const data = await readCSV(`${table}.csv`); + + // Create a new row object + 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); + } + 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 + + return { message: "Row inserted successfully." }; + } + + module.exports = { executeSELECTQuery, executeINSERTQuery }; + \ No newline at end of file diff --git a/src/step-17/queryParser.js b/src/step-17/queryParser.js new file mode 100644 index 000000000..272fb8d58 --- /dev/null +++ b/src/step-17/queryParser.js @@ -0,0 +1,146 @@ +function parseSelectQuery(query) { + try { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + // Initialize distinct flag + let isDistinct = false; + // Check for DISTINCT keyword and update the query + if (query.toUpperCase().includes("SELECT DISTINCT")) { + isDistinct = true; + query = query.replace("SELECT DISTINCT", "SELECT"); + } + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ""); // Remove LIMIT clause + } + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(",").map((field) => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : "ASC" }; + }); + query = query.replace(orderByRegex, ""); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(",").map((field) => field.trim()); + query = query.replace(groupByRegex, ""); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Extract JOIN information + const { joinType, joinTable, joinCondition } = + parseJoinClause(queryWithoutWhere); + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy( + query, + groupByFields + ); + return { + fields: fields.split(",").map((field) => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + isDistinct, + }; + } catch (error) { + throw new Error(`Query parsing error: ${error.message}`); + } +} +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = + /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>=|<=|>|<)(.*)/; + return whereString.split(/ AND | OR /i).map((conditionString) => { + if (conditionString.includes(" LIKE ")) { + const [field, pattern] = conditionString.split(/\sLIKE\s/i); + return { + field: field.trim(), + operator: "LIKE", + value: pattern.trim().replace(/^'(.*)'$/, "$1"), + }; + } else { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error("Invalid WHERE clause format"); + } + }); +} +function parseJoinClause(query) { + const joinRegex = + /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim(), + }, + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null, + }; +} + +function parseInsertQuery(query) { + const insertRegex = /INSERT INTO (\w+)\s\((.+)\)\sVALUES\s\((.+)\)/i; + const match = query.match(insertRegex); + + if (!match) { + throw new Error("Invalid INSERT INTO syntax."); + } + + const [, table, columns, values] = match; + return { + type: "INSERT", + table: table.trim(), + columns: columns.split(",").map((column) => column.trim()), + values: values.split(",").map((value) => value.trim()), + }; +} + +module.exports = { parseSelectQuery, parseJoinClause, parseInsertQuery }; diff --git a/src/step-18/queryExecute.js b/src/step-18/queryExecute.js new file mode 100644 index 000000000..1bcc0f947 --- /dev/null +++ b/src/step-18/queryExecute.js @@ -0,0 +1,322 @@ +const { parseSelectQuery, parseInsertQuery, parseDeleteQuery } = require('./queryParser'); +const { readCSV, writeCSV } = require('../csvReadWrite'); + +function performInnerJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + return joinData + .filter(joinRow => { + const mainValue = mainRow[joinCondition.left.split('.')[1]]; + const joinValue = joinRow[joinCondition.right.split('.')[1]]; + return mainValue === joinValue; + }) + .map(joinRow => { + return fields.reduce((acc, field) => { + const [tableName, fieldName] = field.split('.'); + acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName]; + return acc; + }, {}); + }); + }); +} +function performLeftJoin(data, joinData, joinCondition, fields, table) { + return data.flatMap(mainRow => { + const matchingJoinRows = joinData.filter(joinRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + if (matchingJoinRows.length === 0) { + return [createResultRow(mainRow, null, fields, table, true)]; + } + return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true)); + }); +} +function getValueFromRow(row, compoundFieldName) { + const [tableName, fieldName] = compoundFieldName.split('.'); + return row[`${tableName}.${fieldName}`] || row[fieldName]; +} +function performRightJoin(data, joinData, joinCondition, fields, table) { + // Cache the structure of a main table row (keys only) + const mainTableRowStructure = data.length > 0 ? Object.keys(data[0]).reduce((acc, key) => { + acc[key] = null; // Set all values to null initially + return acc; + }, {}) : {}; + return joinData.map(joinRow => { + const mainRowMatch = data.find(mainRow => { + const mainValue = getValueFromRow(mainRow, joinCondition.left); + const joinValue = getValueFromRow(joinRow, joinCondition.right); + return mainValue === joinValue; + }); + // Use the cached structure if no match is found + const mainRowToUse = mainRowMatch || mainTableRowStructure; + // Include all necessary fields from the 'student' table + return createResultRow(mainRowToUse, joinRow, fields, table, true); + }); +} +function createResultRow(mainRow, joinRow, fields, table, includeAllMainFields) { + const resultRow = {}; + if (includeAllMainFields) { + // Include all fields from the main table + Object.keys(mainRow || {}).forEach(key => { + const prefixedKey = `${table}.${key}`; + resultRow[prefixedKey] = mainRow ? mainRow[key] : null; + }); + } + // Now, add or overwrite with the fields specified in the query + fields.forEach(field => { + const [tableName, fieldName] = field.includes('.') ? field.split('.') : [table, field]; + resultRow[field] = tableName === table && mainRow ? mainRow[fieldName] : joinRow ? joinRow[fieldName] : null; + }); + return resultRow; +} +function evaluateCondition(row, clause) { + let { field, operator, value } = clause; + // Check if the field exists in the row + if (row[field] === undefined) { + throw new Error(`Invalid field: ${field}`); + } + // Parse row value and condition value based on their actual types + const rowValue = parseValue(row[field]); + let conditionValue = parseValue(value); + if (operator === 'LIKE') { + // Transform SQL LIKE pattern to JavaScript RegExp pattern + const regexPattern = '^' + value.replace(/%/g, '.*').replace(/_/g, '.') + '$'; + const regex = new RegExp(regexPattern, 'i'); // 'i' for case-insensitive matching + return regex.test(row[field]); + } + switch (operator) { + case '=': return rowValue === conditionValue; + case '!=': return rowValue !== conditionValue; + case '>': return rowValue > conditionValue; + case '<': return rowValue < conditionValue; + case '>=': return rowValue >= conditionValue; + case '<=': return rowValue <= conditionValue; + default: throw new Error(`Unsupported operator: ${operator}`); + } +} +// Helper function to parse value based on its apparent type +function parseValue(value) { + // Return null or undefined as is + if (value === null || value === undefined) { + return value; + } + // If the value is a string enclosed in single or double quotes, remove them + if (typeof value === 'string' && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) { + value = value.substring(1, value.length - 1); + } + // Check if value is a number + if (!isNaN(value) && value.trim() !== '') { + return Number(value); + } + // Assume value is a string if not a number + return value; +} +function applyGroupBy(data, groupByFields, aggregateFunctions) { + const groupResults = {}; + data.forEach(row => { + // Generate a key for the group + const groupKey = groupByFields.map(field => row[field]).join('-'); + // Initialize group in results if it doesn't exist + if (!groupResults[groupKey]) { + groupResults[groupKey] = { count: 0, sums: {}, mins: {}, maxes: {} }; + groupByFields.forEach(field => groupResults[groupKey][field] = row[field]); + } + // Aggregate calculations + groupResults[groupKey].count += 1; + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + const value = parseFloat(row[aggField]); + switch (aggFunc.toUpperCase()) { + case 'SUM': + groupResults[groupKey].sums[aggField] = (groupResults[groupKey].sums[aggField] || 0) + value; + break; + case 'MIN': + groupResults[groupKey].mins[aggField] = Math.min(groupResults[groupKey].mins[aggField] || value, value); + break; + case 'MAX': + groupResults[groupKey].maxes[aggField] = Math.max(groupResults[groupKey].maxes[aggField] || value, value); + break; + // Additional aggregate functions can be added here + } + } + }); + }); + // Convert grouped results into an array format + return Object.values(groupResults).map(group => { + // Construct the final grouped object based on required fields + const finalGroup = {}; + groupByFields.forEach(field => finalGroup[field] = group[field]); + aggregateFunctions.forEach(func => { + const match = /(\w+)\((\*|\w+)\)/.exec(func); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'SUM': + finalGroup[func] = group.sums[aggField]; + break; + case 'MIN': + finalGroup[func] = group.mins[aggField]; + break; + case 'MAX': + finalGroup[func] = group.maxes[aggField]; + break; + case 'COUNT': + finalGroup[func] = group.count; + break; + // Additional aggregate functions can be handled here + } + } + }); + return finalGroup; + }); +} +async function executeSELECTQuery(query) { + try { + 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 + if (joinTable && joinCondition) { + const joinData = await readCSV(`${joinTable}.csv`); + switch (joinType.toUpperCase()) { + case 'INNER': + data = performInnerJoin(data, joinData, joinCondition, fields, table); + break; + case 'LEFT': + data = performLeftJoin(data, joinData, joinCondition, fields, table); + break; + case 'RIGHT': + data = performRightJoin(data, joinData, joinCondition, fields, table); + break; + default: + throw new Error(`Unsupported JOIN type: ${joinType}`); + } + } + // Apply WHERE clause filtering after JOIN (or on the original data if no join) + let filteredData = whereClauses.length > 0 + ? data.filter(row => whereClauses.every(clause => evaluateCondition(row, clause))) + : data; + let groupResults = filteredData; + if (hasAggregateWithoutGroupBy) { + // Special handling for queries like 'SELECT COUNT(*) FROM table' + const result = {}; + fields.forEach(field => { + const match = /(\w+)\((\*|\w+)\)/.exec(field); + if (match) { + const [, aggFunc, aggField] = match; + switch (aggFunc.toUpperCase()) { + case 'COUNT': + result[field] = filteredData.length; + break; + case 'SUM': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0); + break; + case 'AVG': + result[field] = filteredData.reduce((acc, row) => acc + parseFloat(row[aggField]), 0) / filteredData.length; + break; + case 'MIN': + result[field] = Math.min(...filteredData.map(row => parseFloat(row[aggField]))); + break; + case 'MAX': + result[field] = Math.max(...filteredData.map(row => parseFloat(row[aggField]))); + break; + // Additional aggregate functions can be handled here + } + } + }); + return [result]; + // Add more cases here if needed for other aggregates + } else if (groupByFields) { + groupResults = applyGroupBy(filteredData, groupByFields, fields); + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + if (limit !== null) { + groupResults = groupResults.slice(0, limit); + } + return groupResults; + } else { + // Order them by the specified fields + let orderedResults = groupResults; + if (orderByFields) { + orderedResults = groupResults.sort((a, b) => { + for (let { fieldName, order } of orderByFields) { + if (a[fieldName] < b[fieldName]) return order === 'ASC' ? -1 : 1; + if (a[fieldName] > b[fieldName]) return order === 'ASC' ? 1 : -1; + } + return 0; + }); + } + // Select the specified fields + let finalResults = orderedResults.map(row => { + const selectedRow = {}; + fields.forEach(field => { + // Assuming 'field' is just the column name without table prefix + selectedRow[field] = row[field]; + }); + return selectedRow; + }); + // Remove duplicates if specified + let distinctResults = finalResults; + if (isDistinct) { + distinctResults = [...new Map(finalResults.map(item => [fields.map(field => item[field]).join('|'), item])).values()]; + } + let limitResults = distinctResults; + if (limit !== null) { + limitResults = distinctResults.slice(0, limit); + } + return limitResults; + } + } catch (error) { + throw new Error(`Error executing query: ${error.message}`); + } +} +async function executeINSERTQuery(query) { + console.log(parseInsertQuery(query)); + const { table, columns, values } = parseInsertQuery(query); + const data = await readCSV(`${table}.csv`); + // Create a new row object + 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); + } + 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 + return { message: "Row inserted successfully." }; +} + +async function executeDELETEQuery(query) { + const { table, whereClauses } = parseDeleteQuery(query); + let data = await readCSV(`${table}.csv`); + if (whereClauses.length > 0) { + // Filter out the rows that meet the where clause conditions + data = data.filter(row => !whereClauses.every(clause => evaluateCondition(row, clause))); + } else { + // If no where clause, clear the entire table + data = []; + } + + // Save the updated data back to the CSV file + await writeCSV(`${table}.csv`, data); + + return { message: "Rows deleted successfully." }; +} + + +module.exports = { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery }; \ No newline at end of file diff --git a/src/step-18/queryParser.js b/src/step-18/queryParser.js new file mode 100644 index 000000000..eba353d5b --- /dev/null +++ b/src/step-18/queryParser.js @@ -0,0 +1,155 @@ +function parseSelectQuery(query) { + try { + // Trim the query to remove any leading/trailing whitespaces + query = query.trim(); + // Initialize distinct flag + let isDistinct = false; + // Check for DISTINCT keyword and update the query + if (query.toUpperCase().includes('SELECT DISTINCT')) { + isDistinct = true; + query = query.replace('SELECT DISTINCT', 'SELECT'); + } + // Updated regex to capture LIMIT clause and remove it for further processing + const limitRegex = /\sLIMIT\s(\d+)/i; + const limitMatch = query.match(limitRegex); + let limit = null; + if (limitMatch) { + limit = parseInt(limitMatch[1], 10); + query = query.replace(limitRegex, ''); // Remove LIMIT clause + } + // Process ORDER BY clause and remove it for further processing + const orderByRegex = /\sORDER BY\s(.+)/i; + const orderByMatch = query.match(orderByRegex); + let orderByFields = null; + if (orderByMatch) { + orderByFields = orderByMatch[1].split(',').map(field => { + const [fieldName, order] = field.trim().split(/\s+/); + return { fieldName, order: order ? order.toUpperCase() : 'ASC' }; + }); + query = query.replace(orderByRegex, ''); + } + // Process GROUP BY clause and remove it for further processing + const groupByRegex = /\sGROUP BY\s(.+)/i; + const groupByMatch = query.match(groupByRegex); + let groupByFields = null; + if (groupByMatch) { + groupByFields = groupByMatch[1].split(',').map(field => field.trim()); + query = query.replace(groupByRegex, ''); + } + // Process WHERE clause + const whereSplit = query.split(/\sWHERE\s/i); + const queryWithoutWhere = whereSplit[0]; // Everything before WHERE clause + const whereClause = whereSplit.length > 1 ? whereSplit[1].trim() : null; + // Process JOIN clause + const joinSplit = queryWithoutWhere.split(/\s(INNER|LEFT|RIGHT) JOIN\s/i); + const selectPart = joinSplit[0].trim(); // Everything before JOIN clause + // Extract JOIN information + const { joinType, joinTable, joinCondition } = parseJoinClause(queryWithoutWhere); + // 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; + // Parse WHERE part if it exists + let whereClauses = []; + if (whereClause) { + whereClauses = parseWhereClause(whereClause); + } + // Check for aggregate functions without GROUP BY + const hasAggregateWithoutGroupBy = checkAggregateWithoutGroupBy(query, groupByFields); + return { + fields: fields.split(',').map(field => field.trim()), + table: table.trim(), + whereClauses, + joinType, + joinTable, + joinCondition, + groupByFields, + orderByFields, + hasAggregateWithoutGroupBy, + limit, + isDistinct + }; + } catch (error) { + throw new Error(`Query parsing error: ${error.message}`); + } +} +function checkAggregateWithoutGroupBy(query, groupByFields) { + const aggregateFunctionRegex = /(\bCOUNT\b|\bAVG\b|\bSUM\b|\bMIN\b|\bMAX\b)\s*\(\s*(\*|\w+)\s*\)/i; + return aggregateFunctionRegex.test(query) && !groupByFields; +} +function parseWhereClause(whereString) { + const conditionRegex = /(.*?)(=|!=|>=|<=|>|<)(.*)/; + return whereString.split(/ AND | OR /i).map(conditionString => { + if (conditionString.includes(' LIKE ')) { + const [field, pattern] = conditionString.split(/\sLIKE\s/i); + return { field: field.trim(), operator: 'LIKE', value: pattern.trim().replace(/^'(.*)'$/, '$1') }; + } else { + const match = conditionString.match(conditionRegex); + if (match) { + const [, field, operator, value] = match; + return { field: field.trim(), operator, value: value.trim() }; + } + throw new Error('Invalid WHERE clause format'); + } + }); +} +function parseJoinClause(query) { + const joinRegex = /\s(INNER|LEFT|RIGHT) JOIN\s(.+?)\sON\s([\w.]+)\s*=\s*([\w.]+)/i; + const joinMatch = query.match(joinRegex); + if (joinMatch) { + return { + joinType: joinMatch[1].trim(), + joinTable: joinMatch[2].trim(), + joinCondition: { + left: joinMatch[3].trim(), + right: joinMatch[4].trim() + } + }; + } + return { + joinType: null, + joinTable: null, + joinCondition: null + }; +} +function parseInsertQuery(query) { + const insertRegex = /INSERT INTO (\w+)\s\((.+)\)\sVALUES\s\((.+)\)/i; + const match = query.match(insertRegex); + if (!match) { + throw new Error("Invalid INSERT INTO syntax."); + } + const [, table, columns, values] = match; + return { + type: 'INSERT', + table: table.trim(), + columns: columns.split(',').map(column => column.trim()), + values: values.split(',').map(value => value.trim()) + }; +} + +function parseDeleteQuery(query) { + const deleteRegex = /DELETE FROM (\w+)( WHERE (.*))?/i; + const match = query.match(deleteRegex); + + if (!match) { + throw new Error("Invalid DELETE syntax."); + } + + const [, table, , whereString] = match; + let whereClauses = []; + if (whereString) { + whereClauses = parseWhereClause(whereString); + } + + return { + type: 'DELETE', + table: table.trim(), + whereClauses + }; +} + + +module.exports = { parseSelectQuery, parseJoinClause, parseInsertQuery, parseDeleteQuery }; \ No newline at end of file diff --git a/src/step-19/cli.js b/src/step-19/cli.js new file mode 100644 index 000000000..c0090b8df --- /dev/null +++ b/src/step-19/cli.js @@ -0,0 +1,41 @@ +const readline = require('readline'); +const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('../step-18/queryExecute'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.setPrompt('SQL> '); +console.log('SQL Query Engine CLI. Enter your SQL commands, or type "exit" to quit.'); + +rl.prompt(); + +rl.on('line', async (line) => { + if (line.toLowerCase() === 'exit') { + rl.close(); + return; + } + + try { + if (line.toLowerCase().startsWith('select')) { + const result = await executeSELECTQuery(line); + console.log('Result:', result); + } else if (line.toLowerCase().startsWith('insert into')) { + const result = await executeINSERTQuery(line); + console.log(result.message); + } else if (line.toLowerCase().startsWith('delete from')) { + const result = await executeDELETEQuery(line); + console.log(result.message); + } else { + console.log('Unsupported command'); + } + } catch (error) { + console.error('Error:', error.message); + } + + rl.prompt(); +}).on('close', () => { + console.log('Exiting SQL CLI'); + process.exit(0); +}); \ No newline at end of file diff --git a/src/step-20/cli.js b/src/step-20/cli.js new file mode 100644 index 000000000..273f7a46d --- /dev/null +++ b/src/step-20/cli.js @@ -0,0 +1,42 @@ +const readline = require("readline"); +const { + executeSELECTQuery, + executeINSERTQuery, + executeDELETEQuery, +} = require("../step-18/queryExecute"); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); +rl.setPrompt("SQL> "); +console.log( + 'SQL Query Engine CLI. Enter your SQL commands, or type "exit" to quit.' +); +rl.prompt(); +rl.on("line", async (line) => { + if (line.toLowerCase() === "exit") { + rl.close(); + return; + } + try { + if (line.toLowerCase().startsWith("select")) { + const result = await executeSELECTQuery(line); + console.log("Result:", result); + } else if (line.toLowerCase().startsWith("insert into")) { + const result = await executeINSERTQuery(line); + console.log(result.message); + } else if (line.toLowerCase().startsWith("delete from")) { + const result = await executeDELETEQuery(line); + console.log(result.message); + } else { + console.log("Unsupported command"); + } + } catch (error) { + console.error("Error:", error.message); + } + rl.prompt(); +}).on("close", () => { + console.log("Exiting SQL CLI"); + process.exit(0); +}); diff --git a/src/step-20/index.js b/src/step-20/index.js new file mode 100644 index 000000000..81ea5acf1 --- /dev/null +++ b/src/step-20/index.js @@ -0,0 +1,16 @@ +const { readCSV, writeCSV } = require('../csvReadWrite'); +const { parseSelectQuery, parseInsertQuery, parseDeleteQuery } = require('../step-18/queryParser'); +const { executeSELECTQuery, executeINSERTQuery, executeDELETEQuery } = require('../step-18/queryExecute'); +const { parseJoinClause} = require('../step-17/queryParser') + +module.exports = { + readCSV, + writeCSV, + executeSELECTQuery, + executeINSERTQuery, + executeDELETEQuery, + parseSelectQuery, + parseInsertQuery, + parseDeleteQuery, + parseJoinClause +} \ No newline at end of file diff --git a/student.csv b/student.csv new file mode 100644 index 000000000..e9c960121 --- /dev/null +++ b/student.csv @@ -0,0 +1,5 @@ +id,name,age +1,John,30 +2,Jane,25 +3,Bob,22 +4,Alice,24 \ No newline at end of file diff --git a/tests/step-03/index.test.js b/tests/step-03/index.test.js index 9145ad3e4..64057095d 100644 --- a/tests/step-03/index.test.js +++ b/tests/step-03/index.test.js @@ -1,12 +1,12 @@ const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); +const parseQuery = require('../../src/step-03/queryParser'); test('Read CSV File', async () => { const data = await readCSV('./sample.csv'); expect(data.length).toBeGreaterThan(0); expect(data.length).toBe(3); expect(data[0].name).toBe('John'); - expect(data[0].age).toBe('30'); //ignore the string type here, we will fix this later + expect(data[0].age).toBe('30'); }); test('Parse SQL Query', () => { diff --git a/tests/step-04/index.test.js b/tests/step-04/index.test.js index bc353dd3d..a9f6f0474 100644 --- a/tests/step-04/index.test.js +++ b/tests/step-04/index.test.js @@ -1,30 +1,32 @@ -const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const readCSV = require("../../src/csvReader"); +const parseQuery = require("../../src/step-04/queryParse"); +const executeSELECTQuery = require("../../src/step-04/queryExecute"); -test('Read CSV File', async () => { - const data = await readCSV('./sample.csv'); - expect(data.length).toBeGreaterThan(0); - expect(data.length).toBe(3); - expect(data[0].name).toBe('John'); - expect(data[0].age).toBe('30'); //ignore the string type here, we will fix this later +test("Read CSV File", async () => { + const data = await readCSV("./sample.csv"); + expect(data.length).toBeGreaterThan(0); + expect(data.length).toBe(3); + expect(data[0].name).toBe("John"); + expect(data[0].age).toBe("30"); //ignore the string type here, we will fix this later }); -test('Parse SQL Query', () => { - const query = 'SELECT id, name FROM sample'; - const parsed = parseQuery(query); - expect(parsed).toEqual({ - fields: ['id', 'name'], - table: 'sample' - }); +test("Parse SQL Query", () => { + const query = "SELECT id, name FROM sample"; + const parsed = parseQuery(query); + const checkWhereClauses = parsed.whereClauses; + expect(parsed).toEqual({ + fields: ["id", "name"], + table: "sample", + whereClause: null, + }); }); -test('Execute SQL Query', async () => { - const query = 'SELECT id, name FROM sample'; - const result = await executeSELECTQuery(query); - expect(result.length).toBeGreaterThan(0); - expect(result[0]).toHaveProperty('id'); - expect(result[0]).toHaveProperty('name'); - expect(result[0]).not.toHaveProperty('age'); - expect(result[0]).toEqual({ id: '1', name: 'John' }); -}); \ No newline at end of file +test("Execute SQL Query", async () => { + const query = "SELECT id, name FROM sample"; + const result = await executeSELECTQuery(query); + expect(result.length).toBeGreaterThan(0); + expect(result[0]).toHaveProperty("id"); + expect(result[0]).toHaveProperty("name"); + expect(result[0]).not.toHaveProperty("age"); + expect(result[0]).toEqual({ id: "1", name: "John" }); +}); diff --git a/tests/step-05/index.test.js b/tests/step-05/index.test.js index 66a77c061..838ae7bfd 100644 --- a/tests/step-05/index.test.js +++ b/tests/step-05/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const parseQuery = require('../../src/step-05/queryParse'); +const executeSELECTQuery = require('../../src/step-05/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./sample.csv'); @@ -36,7 +36,7 @@ test('Parse SQL Query with WHERE Clause', () => { expect(parsed).toEqual({ fields: ['id', 'name'], table: 'sample', - whereClause: 'age = 25' + whereClause:"age = 25" }); }); diff --git a/tests/step-06/index.test.js b/tests/step-06/index.test.js index 2e2ef6416..17ea43387 100644 --- a/tests/step-06/index.test.js +++ b/tests/step-06/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const parseQuery = require('../../src/step-06/queryParse'); +const executeSELECTQuery = require('../../src/step-06/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./sample.csv'); diff --git a/tests/step-07/index.test.js b/tests/step-07/index.test.js index ee0ebed5e..88f903cd5 100644 --- a/tests/step-07/index.test.js +++ b/tests/step-07/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const parseQuery = require('../../src/step-07/queryParse'); +const executeSELECTQuery = require('../../src/step-07/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./sample.csv'); diff --git a/tests/step-08/index.test.js b/tests/step-08/index.test.js index aab1467e6..c9a2ffbe3 100644 --- a/tests/step-08/index.test.js +++ b/tests/step-08/index.test.js @@ -1,11 +1,11 @@ const readCSV = require('../../src/csvReader'); -const parseQuery = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const parseQuery = require('../../src/step-08/queryParse'); +const executeSELECTQuery = require('../../src/step-08/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); expect(data.length).toBeGreaterThan(0); - expect(data.length).toBe(3); + expect(data.length).toBe(4); expect(data[0].name).toBe('John'); expect(data[0].age).toBe('30'); //ignore the string type here, we will fix this later }); @@ -78,23 +78,23 @@ test('Parse SQL Query with Multiple WHERE Clauses', () => { }); test('Execute SQL Query with Complex WHERE Clause', async () => { - const query = 'SELECT id, name FROM student WHERE age = 30 AND name = John'; + const query = 'SELECT id, name, age FROM student WHERE age = 30 AND name = John'; const result = await executeSELECTQuery(query); expect(result.length).toBe(1); - expect(result[0]).toEqual({ id: '1', name: 'John' }); + expect(result[0]).toEqual({ "age":"30" ,id: '1', name: 'John' }); }); test('Execute SQL Query with Greater Than', async () => { const queryWithGT = 'SELECT id FROM student WHERE age > 22'; const result = await executeSELECTQuery(queryWithGT); - expect(result.length).toEqual(2); + expect(result.length).toEqual(3); expect(result[0]).toHaveProperty('id'); }); test('Execute SQL Query with Not Equal to', async () => { const queryWithGT = 'SELECT name FROM student WHERE age != 25'; const result = await executeSELECTQuery(queryWithGT); - expect(result.length).toEqual(2); + expect(result.length).toEqual(3); expect(result[0]).toHaveProperty('name'); }); diff --git a/tests/step-09/index.test.js b/tests/step-09/index.test.js index aaf711f5a..0b82aeab8 100644 --- a/tests/step-09/index.test.js +++ b/tests/step-09/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery} = require('../../src/step-09/queryParser'); +const executeSELECTQuery = require('../../src/step-09/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-10/index.test.js b/tests/step-10/index.test.js index 5e118eda5..ba3a7fdc9 100644 --- a/tests/step-10/index.test.js +++ b/tests/step-10/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-10/queryParser'); +const executeSELECTQuery = require('../../src/step-10/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-11/index.test.js b/tests/step-11/index.test.js index 1cf5f2def..5cc62970b 100644 --- a/tests/step-11/index.test.js +++ b/tests/step-11/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/STEP-11/queryParser'); +const executeSELECTQuery = require('../../src/STEP-11/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-12/index.test.js b/tests/step-12/index.test.js index d15c77ef5..43f3ff722 100644 --- a/tests/step-12/index.test.js +++ b/tests/step-12/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-12/queryParser'); +const executeSELECTQuery = require('../../src/step-12/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-13/index.test.js b/tests/step-13/index.test.js index 0797faaba..f52dcbcd3 100644 --- a/tests/step-13/index.test.js +++ b/tests/step-13/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-13/queryParser'); +const executeSELECTQuery = require('../../src/step-13/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-14/index.test.js b/tests/step-14/index.test.js index 502411fa7..21e1aa53b 100644 --- a/tests/step-14/index.test.js +++ b/tests/step-14/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-14/queryParser'); +const executeSELECTQuery = require('../../src/step-14/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-15/index.test.js b/tests/step-15/index.test.js index a2aa4daee..0f007d3f7 100644 --- a/tests/step-15/index.test.js +++ b/tests/step-15/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-15&16/queryParser'); +const executeSELECTQuery = require('../../src/step-15&16/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-16/index.test.js b/tests/step-16/index.test.js index a2aa4daee..0f007d3f7 100644 --- a/tests/step-16/index.test.js +++ b/tests/step-16/index.test.js @@ -1,6 +1,6 @@ const readCSV = require('../../src/csvReader'); -const {parseQuery, parseJoinClause} = require('../../src/queryParser'); -const executeSELECTQuery = require('../../src/index'); +const {parseQuery, parseJoinClause} = require('../../src/step-15&16/queryParser'); +const executeSELECTQuery = require('../../src/step-15&16/queryExecute'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-17/index.test.js b/tests/step-17/index.test.js index c99d01fbb..985559782 100644 --- a/tests/step-17/index.test.js +++ b/tests/step-17/index.test.js @@ -1,6 +1,6 @@ -const {readCSV} = require('../../src/csvReader'); -const {executeSELECTQuery } = require('../../src/index'); -const { parseJoinClause, parseSelectQuery } = require('../../src/queryParser'); +const {readCSV} = require('../../src/csvReadWrite'); +const {executeSELECTQuery } = require('../../src/step-17/queryExecute'); +const { parseJoinClause, parseSelectQuery } = require('../../src/step-17/queryParser'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-17/insertExecuter.test.js b/tests/step-17/insertExecuter.test.js index 8c405f727..25b073664 100644 --- a/tests/step-17/insertExecuter.test.js +++ b/tests/step-17/insertExecuter.test.js @@ -1,5 +1,5 @@ -const { executeINSERTQuery } = require('../../src/index'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeINSERTQuery } = require('../../src/step-17/queryExecute'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create grades.csv with initial data diff --git a/tests/step-18/deleteExecutor.test.js b/tests/step-18/deleteExecutor.test.js index 11ae617b7..f29c0831a 100644 --- a/tests/step-18/deleteExecutor.test.js +++ b/tests/step-18/deleteExecutor.test.js @@ -1,5 +1,5 @@ -const { executeDELETEQuery } = require('../../src/index'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeDELETEQuery } = require('../../src/step-18/queryExecute'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create courses.csv with initial data diff --git a/tests/step-18/index.test.js b/tests/step-18/index.test.js index c99d01fbb..20bcbd0b3 100644 --- a/tests/step-18/index.test.js +++ b/tests/step-18/index.test.js @@ -1,6 +1,6 @@ -const {readCSV} = require('../../src/csvReader'); -const {executeSELECTQuery } = require('../../src/index'); -const { parseJoinClause, parseSelectQuery } = require('../../src/queryParser'); +const {readCSV} = require('../../src/csvReadWrite'); +const {executeSELECTQuery } = require('../../src/step-18/queryExecute'); +const { parseJoinClause, parseSelectQuery } = require('../../src/step-18/queryParser'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-18/insertExecuter.test.js b/tests/step-18/insertExecuter.test.js index 8c405f727..fb398780b 100644 --- a/tests/step-18/insertExecuter.test.js +++ b/tests/step-18/insertExecuter.test.js @@ -1,5 +1,5 @@ -const { executeINSERTQuery } = require('../../src/index'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeINSERTQuery } = require('../../src/step-18/queryExecute'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create grades.csv with initial data diff --git a/tests/step-19/deleteExecutor.test.js b/tests/step-19/deleteExecutor.test.js index 11ae617b7..f29c0831a 100644 --- a/tests/step-19/deleteExecutor.test.js +++ b/tests/step-19/deleteExecutor.test.js @@ -1,5 +1,5 @@ -const { executeDELETEQuery } = require('../../src/index'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeDELETEQuery } = require('../../src/step-18/queryExecute'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create courses.csv with initial data diff --git a/tests/step-19/index.test.js b/tests/step-19/index.test.js index c99d01fbb..20bcbd0b3 100644 --- a/tests/step-19/index.test.js +++ b/tests/step-19/index.test.js @@ -1,6 +1,6 @@ -const {readCSV} = require('../../src/csvReader'); -const {executeSELECTQuery } = require('../../src/index'); -const { parseJoinClause, parseSelectQuery } = require('../../src/queryParser'); +const {readCSV} = require('../../src/csvReadWrite'); +const {executeSELECTQuery } = require('../../src/step-18/queryExecute'); +const { parseJoinClause, parseSelectQuery } = require('../../src/step-18/queryParser'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-19/insertExecuter.test.js b/tests/step-19/insertExecuter.test.js index 8c405f727..fb398780b 100644 --- a/tests/step-19/insertExecuter.test.js +++ b/tests/step-19/insertExecuter.test.js @@ -1,5 +1,5 @@ -const { executeINSERTQuery } = require('../../src/index'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeINSERTQuery } = require('../../src/step-18/queryExecute'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create grades.csv with initial data diff --git a/tests/step-20/deleteExecutor.test.js b/tests/step-20/deleteExecutor.test.js index 636403858..ca06031bd 100644 --- a/tests/step-20/deleteExecutor.test.js +++ b/tests/step-20/deleteExecutor.test.js @@ -1,5 +1,5 @@ -const { executeDELETEQuery } = require('../../src/queryExecutor'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeDELETEQuery } = require('../../src/step-20/index'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create courses.csv with initial data diff --git a/tests/step-20/index.test.js b/tests/step-20/index.test.js index dc1fa19ae..f603f0e8a 100644 --- a/tests/step-20/index.test.js +++ b/tests/step-20/index.test.js @@ -1,6 +1,6 @@ -const {readCSV} = require('../../src/csvReader'); -const {executeSELECTQuery } = require('../../src/queryExecutor'); -const { parseJoinClause, parseSelectQuery } = require('../../src/queryParser'); +const {readCSV} = require('../../src/csvReadWrite'); +const {executeSELECTQuery } = require('../../src/step-20/index'); +const { parseJoinClause, parseSelectQuery } = require('../../src/step-20/index'); test('Read CSV File', async () => { const data = await readCSV('./student.csv'); diff --git a/tests/step-20/insertExecuter.test.js b/tests/step-20/insertExecuter.test.js index 581d17f73..c5bf26b9c 100644 --- a/tests/step-20/insertExecuter.test.js +++ b/tests/step-20/insertExecuter.test.js @@ -1,5 +1,5 @@ -const { executeINSERTQuery } = require('../../src/queryExecutor'); -const { readCSV, writeCSV } = require('../../src/csvReader'); +const { executeINSERTQuery } = require('../../src/step-20/index'); +const { readCSV, writeCSV } = require('../../src/csvReadWrite'); const fs = require('fs'); // Helper function to create grades.csv with initial data