Skip to content

Commit

Permalink
complete till step 10
Browse files Browse the repository at this point in the history
  • Loading branch information
sourav-sm committed Apr 8, 2024
1 parent e330014 commit 35758a6
Show file tree
Hide file tree
Showing 15 changed files with 948 additions and 153 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
},
"scripts": {
"test": "jest",
"test:82": "jest --testPathPattern=./tests/csvReader.test.js",
"test:83": "jest --testPathPattern=./tests/queryExecutor.test.js",
"test:84": "jest --testPathPattern=./tests/queryParser.test.js",
"test:1": "jest --testPathPattern=./tests/step-01",
"test:2": "jest --testPathPattern=./tests/step-02",
"test:3": "jest --testPathPattern=./tests/step-03",
Expand Down
2 changes: 1 addition & 1 deletion src/csvReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ function readCSV(filePath) {
});
}

module.exports = readCSV;
module.exports = readCSV;
279 changes: 202 additions & 77 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
const readCSV = require('./csvReader');
const { parseQuery } = require('./queryParser');


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}`);
}
}


function performInnerJoin(mainData, joinData, joinCondition, fields, mainTable) {
return mainData.flatMap(mainRow => {
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]];
Expand All @@ -27,70 +11,70 @@ function performInnerJoin(mainData, joinData, joinCondition, fields, mainTable)
.map(joinRow => {
return fields.reduce((acc, field) => {
const [tableName, fieldName] = field.split('.');
acc[field] = tableName === mainTable ? mainRow[fieldName] : joinRow[fieldName];
acc[field] = tableName === table ? mainRow[fieldName] : joinRow[fieldName];
return acc;
}, {});
});
});
}

function performLeftJoin(mainData, joinData, joinCondition, fields, mainTable) {
return mainData.flatMap(mainRow => {
const matchingRows = joinData.filter(joinRow => {
const mainValue = mainRow[joinCondition.left.split('.')[1]];
const joinValue = joinRow[joinCondition.right.split('.')[1]];
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 (matchingRows.length === 0) {
return fields.reduce((acc, field) => {
const [tableName, fieldName] = field.split('.');
acc[field] = tableName === mainTable ? mainRow[fieldName] : null;
return acc;
}, {});
if (matchingJoinRows.length === 0) {
return [createResultRow(mainRow, null, fields, table, true)];
}

return matchingRows.map(joinRow => {
return fields.reduce((acc, field) => {
const [tableName, fieldName] = field.split('.');
acc[field] = tableName === mainTable ? mainRow[fieldName] : joinRow[fieldName];
return acc;
}, {});
});
return matchingJoinRows.map(joinRow => createResultRow(mainRow, joinRow, fields, table, true));
});
}

function performRightJoin(mainData, joinData, joinCondition, fields, mainTable) {
return joinData.flatMap(joinRow => {
const matchingRows = mainData.filter(mainRow => {
const mainValue = mainRow[joinCondition.left.split('.')[1]];
const joinValue = joinRow[joinCondition.right.split('.')[1]];
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;
});

if (matchingRows.length === 0) {
return fields.reduce((acc, field) => {
const [tableName, fieldName] = field.split('.');
acc[field] = tableName !== mainTable ? joinRow[fieldName] : null;
return acc;
}, {});
}

return matchingRows.map(mainRow => {
return fields.reduce((acc, field) => {
const [tableName, fieldName] = field.split('.');
acc[field] = tableName === mainTable ? mainRow[fieldName] : joinRow[fieldName];
return acc;
}, {});
// 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);
const { fields, table, whereClauses, joinType, joinTable, joinCondition, groupByFields, hasAggregateWithoutGroupBy } = parseQuery(query);
let data = await readCSV(`${table}.csv`);

if (joinTable && joinCondition && joinType) { // Ensure joinType is not null
// Perform INNER JOIN if specified
if (joinTable && joinCondition) {
const joinData = await readCSV(`${joinTable}.csv`);
switch (joinType.toUpperCase()) {
case 'INNER':
Expand All @@ -103,26 +87,167 @@ async function executeSELECTQuery(query) {
data = performRightJoin(data, joinData, joinCondition, fields, table);
break;
default:
console.error(`Unsupported JOIN type: ${joinType}`);
break;
throw new Error(`Unsupported JOIN type: ${joinType}`);
}
}


// 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;
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 })

// Prepare the selected fields
return filteredData.map(row => {
const selectedRow = {};
fields.forEach(field => {
// Assuming 'field' is just the column name without table prefix
selectedRow[field] = row[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 selectedRow;
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;
Loading

0 comments on commit 35758a6

Please sign in to comment.