UNPKG

sequel-csv

Version:

Perform sql operations against csv databases.

341 lines (314 loc) 16.4 kB
const fs = require('fs'); const escapeString = require('../utils/escapeString.js'); class Table { _path = null; _name = null; _headers = []; _entries = []; rows = []; constructor(file) { if (!fs.existsSync(file)) return {}; const lines = fs.readFileSync(file).toString().split('\n'); const headers = lines.shift().split(','); this._headers = headers; this._entries = lines; this._path = file; this._name = file.split('/').pop().split('.')[0]; for(let i = 0; i < lines.length; i++){ const row = lines[i].split(','); const obj = {}; for(let j = 0; j < row.length; j++) obj[headers[j].replace(/[\n\r]/g, '')] = row[j].replace(/[\n\r]/g, ''); this.rows.push(obj); } } /** * @returns {Object} []<Row> * @param {String} query Query * @param {Array} [args] []Params **/ async query(query, args){ let rows = [...this.rows]; let filteredRows = [...rows]; if(!args) args = []; for(let i = 0; i < args.length; i++) query = query.replace('?', args[i]); if(query.includes('WHERE')){ let condition; condition = query.split('WHERE')[1].trim(); const conditions = condition.split('AND'); for(let i = 0; i < conditions.length; i++){ let components = conditions[i].split(/(=|<>|<=|>=|<|>|IS NULL|IS NOT NULL)/); let key = components[0].trim(); let comparison = components[1].trim(); let value = components[2].trim().match(/(["'])(?:(?=(\\?))\2.)*?\1/g) ? escapeString(components[2].trim().match(/(["'])(?:(?=(\\?))\2.)*?\1/g)[0].replaceAll(/['"]/g, '')) : escapeString(components[2].trim()); const toRemove = []; switch(comparison){ case '=': filteredRows.forEach(row => { if(row[key] !== value) toRemove.push(row); }); break; case '<>': filteredRows.forEach(row => { if(row[key] === value) toRemove.push(row); }); break; case '>': filteredRows.forEach(row => { if(parseInt(row[key]) <= value) toRemove.push(row); }); break; case '<': filteredRows.forEach(row => { if(parseInt(row[key]) >= value) toRemove.push(row); }); break; case '>=': filteredRows.forEach(row => { if(parseInt(row[key]) < value) toRemove.push(row); }); break; case '<=': filteredRows.forEach(row => { if(parseInt(row[key]) > value) toRemove.push(row); }); break; case 'IS NULL': filteredRows.forEach(row => { if(![undefined, null, 'undefined', 'null'].includes(row[key])) toRemove.push(row); }); break; case 'IS NOT NULL': filteredRows.forEach(row => { if([undefined, null, 'undefined', 'null'].includes(row[key])) toRemove.push(row); }); break; } toRemove.forEach(row => { filteredRows.splice(filteredRows.indexOf(row), 1); }); } } const operation = query.toString().split(' ')[0].toLowerCase(); switch(operation){ case 'select': { const querySequential = query.toString().split(' ').splice(1); const columns = []; for(let i = 0; i < querySequential.length; i++){ if(['WHERE', 'FROM'].includes(querySequential[i])) break else columns.push(querySequential[i]); } if(query.toString().includes('ORDER BY')){ const orderBy = query.toString().split('ORDER BY')[1].trim(); const orderByComponents = orderBy.split(' '); const orderByKey = orderByComponents[0]; const orderByDirection = orderByComponents[1]; filteredRows.sort((a, b) => { if(orderByDirection === 'ASC'){ if(a[orderByKey] < b[orderByKey]) return -1; else if(a[orderByKey] > b[orderByKey]) return 1; else return 0; } else if(orderByDirection === 'DESC'){ if(a[orderByKey] < b[orderByKey]) return 1; else if(a[orderByKey] > b[orderByKey]) return -1; else return 0; } }); } if(query.includes('LIMIT')){ const limit = query.toString().split('LIMIT')[1].trim(); filteredRows = filteredRows.slice(0, parseInt(limit)); } if(columns[0] === '*') return filteredRows; let realColumns = Array.from(columns); for(let i = 0; i < realColumns.length; i++) if(realColumns[i].includes('AS')) realColumns.splice(i, 2); if(realColumns.length > 1){ const returnRows = []; let aliasedAggregateFunction = false; for(let i = 0; i < columns.length; i++){ const column = columns[i].replaceAll(',', ''); const alias = query.split(`${column} AS`)?.[1] ? query.split(`${column} AS`)[1].trim().split(' ')[0].trim().replaceAll(',', '') : null; const maxRegex = /MAX\(([^)]+)\)/g; const minRegex = /MIN\(([^)]+)\)/g; if(maxRegex.test(column)){ const maxColumn = column.match(maxRegex)[0].replace('MAX(', '').replace(')', ''); const returnOBJ = {}; let max = 0; filteredRows.forEach(row => { if(!max || parseInt(row[maxColumn]) > parseInt(max)) max = row[maxColumn]; }); if(alias) returnOBJ[alias] = max; else returnOBJ[maxColumn] = max; returnRows.push(returnOBJ); aliasedAggregateFunction = true; } if(minRegex.test(column)){ const minColumn = column.match(minRegex)[0].replace('MIN(', '').replace(')', '').split(' ')[0]; const returnOBJ = {}; let min = 0; filteredRows.forEach(row => { if(!min || parseInt(row[minColumn]) < parseInt(min)) min = row[minColumn]; }); if(alias) returnOBJ[alias] = min; else returnOBJ[minColumn] = min; returnRows.push(returnOBJ); aliasedAggregateFunction = true; } } for(let i = 0; i < filteredRows.length; i++){ const filteredRow = {}; for(let j = 0; j < columns.length; j++){ const column = columns[j].replaceAll(',', ''); const alias = query.split(`${column} AS`)?.[1] ? query.split(`${column} AS`)[1].trim().split(' ')[0].trim().replaceAll(',', '') : null; if([alias, 'AS'].includes(column)) continue; if(alias && !aliasedAggregateFunction){ filteredRow[alias] = filteredRows[i][column]; }else{ if(filteredRows[i]?.[column]) filteredRow[column] = filteredRows[i][column]; } } returnRows.push(filteredRow); } return returnRows; }else{ const column = columns[0]; const alias = query.split(`${column} AS`)?.[1] ? query.split(`${column} AS`)[1].trim().split(' ')[0].trim() : null; const maxRegex = /MAX\(([^)]+)\)/g; const minRegex = /MIN\(([^)]+)\)/g; const avgRegex = /AVG\(([^)]+)\)/g; const countRegex = /COUNT\(([^)]+)\)/g; const standardDeviationRegex = /STDEV\(([^)]+)\)/g; const sumRegex = /SUM\(([^)]+)\)/g; if(maxRegex.test(column)){ const maxColumn = column.match(maxRegex)[0].replace('MAX(', '').replace(')', ''); const returnOBJ = {}; return await new Promise(resolve => { let max = 0; filteredRows.forEach(row => { if(!max || parseInt(row[maxColumn]) > parseInt(max)) max = row[maxColumn]; }); alias ? returnOBJ[alias] = max : returnOBJ[maxColumn] = max; resolve(returnOBJ); }); } if(minRegex.test(column)){ const minColumn = column.match(minRegex)[0].replace('MIN(', '').replace(')', '').split(' ')[0]; const returnOBJ = {}; return await new Promise(resolve => { let min = 0; filteredRows.forEach(row => { if(!min || parseInt(row[minColumn]) < parseInt(min)) min = row[minColumn]; }); alias ? returnOBJ[alias] = min : returnOBJ[minColumn] = min; resolve(returnOBJ); }); } if(avgRegex.test(column)){ const avgColumn = column.match(avgRegex)[0].replace('AVG(', '').replace(')', '').split(' ')[0]; const returnOBJ = {}; return await new Promise(resolve => { let avg = 0; filteredRows.forEach(row => { avg += parseInt(row[avgColumn]); }); avg = avg / filteredRows.length; alias ? returnOBJ[alias] = avg : returnOBJ[avgColumn] = avg; resolve(returnOBJ); }); } if(countRegex.test(column)){ const countColumn = column.match(countRegex)[0].replace('COUNT(', '').replace(')', '').split(' ')[0]; const obj = {}; if(countColumn === '*') alias ? obj[alias] = filteredRows.length : obj[column] = filteredRows.length; else{ const returnOBJ = {}; return await new Promise(resolve => { let count = 0; filteredRows.forEach(row => { if(row[countColumn]) count++; }); alias ? returnOBJ[alias] = count : returnOBJ[countColumn] = count; resolve(returnOBJ); }); } return obj; } if(standardDeviationRegex.test(column)){ const avgColumn = column.match(standardDeviationRegex)[0].replace('STDEV(', '').replace(')', '').split(' ')[0]; const returnOBJ = {}; return await new Promise(resolve => { let avg = 0; filteredRows.forEach(row => { avg += parseInt(row[avgColumn]); }); avg = avg / filteredRows.length; let sum = 0; filteredRows.forEach(row => { sum += Math.pow(parseInt(row[avgColumn]) - avg, 2); }); const standardDeviation = Math.sqrt(sum / filteredRows.length); alias ? returnOBJ[alias] = standardDeviation : returnOBJ[avgColumn] = standardDeviation; resolve(returnOBJ); }); } if(sumRegex.test(column)){ const sumColumn = column.match(sumRegex)[0].replace('SUM(', '').replace(')', '').split(' ')[0]; const returnOBJ = {}; return await new Promise(resolve => { let sum = 0; filteredRows.forEach(row => { sum += parseInt(row[sumColumn]); }); alias ? returnOBJ[alias] = sum : returnOBJ[sumColumn] = sum; resolve(returnOBJ); }); } const result = []; filteredRows .map(row => row[column]) .forEach(value => { let obj = {}; alias ? obj[alias] = value : obj[column] = value; result.push(obj); }); return result; } } case 'insert': { let columns = query.split('(')[1].split(')')[0].split(','); columns = columns.map(column => column.trim()); let values = query.split('VALUES')[1].toString().replaceAll(/[()]/g, '').trim().split(','); let rowString = ''; for(let i = 0; i < this._headers.length; i++){ if(columns.includes(this._headers[i])) rowString += i === 0 ? values[columns.indexOf(this._headers[i])] : `,${values[columns.indexOf(this._headers[i])]}`; else rowString += ','; } rowString = rowString.split(',').map(value => value.trim()).toString(); this.#appendRow(rowString); break; } case 'delete': { let entries = this._entries; await new Promise(resolve => { filteredRows.forEach(row => { entries.splice(entries.indexOf(Object.values(row).join(',')), 1); }) resolve(); }) fs.writeFileSync(this._path, `${this._headers}\n${entries.join('\n')}`); } } } #appendRow(row){ fs.appendFileSync(this._path, `\n${row}`); } } module.exports = Table;