diff --git a/docs/usage/core-concepts.md b/docs/usage/core-concepts.md index 0a28c67..4212272 100644 --- a/docs/usage/core-concepts.md +++ b/docs/usage/core-concepts.md @@ -50,7 +50,16 @@ The main class that ties everything together. It provides a simple API to execut ```typescript const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase'); + +// Execute a query and get all results as an array const results = await queryLeaf.execute('SELECT * FROM users'); + +// Or use a cursor for more control and memory efficiency +const cursor = await queryLeaf.executeCursor('SELECT * FROM users'); +await cursor.forEach(user => { + console.log(`Processing user: ${user.name}`); +}); +await cursor.close(); ``` ### DummyQueryLeaf @@ -61,6 +70,13 @@ A special implementation of QueryLeaf that doesn't execute real MongoDB operatio const dummyLeaf = new DummyQueryLeaf('mydatabase'); await dummyLeaf.execute('SELECT * FROM users'); // [DUMMY MongoDB] FIND in mydatabase.users with filter: {} + +// Cursor support works with DummyQueryLeaf too +const cursor = await dummyLeaf.executeCursor('SELECT * FROM users'); +await cursor.forEach(user => { + // Process mock data +}); +await cursor.close(); ``` ## Relationship Between SQL and MongoDB Concepts @@ -101,14 +117,31 @@ QueryLeaf uses specific naming conventions for mapping SQL to MongoDB: ## Execution Flow +QueryLeaf supports two main execution methods: + +### Standard Execution + When you call `queryLeaf.execute(sqlQuery)`, the following happens: 1. The SQL query is parsed into an AST 2. The AST is compiled into MongoDB commands 3. The commands are executed against the MongoDB database -4. The results are returned +4. All results are loaded into memory and returned as an array + +This is simple to use but can be memory-intensive for large result sets. + +### Cursor Execution + +When you call `queryLeaf.executeCursor(sqlQuery)`, the following happens: + +1. The SQL query is parsed into an AST +2. The AST is compiled into MongoDB commands +3. For SELECT queries, a MongoDB cursor is returned instead of loading all results +4. You control how and when results are processed (streaming/batching) + +This approach is more memory-efficient for large datasets and gives you more control. -If any step fails, an error is thrown with details about what went wrong. +If any step fails in either approach, an error is thrown with details about what went wrong. ## Extending QueryLeaf diff --git a/docs/usage/examples.md b/docs/usage/examples.md index 88be0f7..101a004 100644 --- a/docs/usage/examples.md +++ b/docs/usage/examples.md @@ -73,6 +73,37 @@ const usersInNY = await queryLeaf.execute(` `); ``` +### Using Cursors for Large Result Sets + +```typescript +// Use cursor for memory-efficient processing of large result sets +const cursor = await queryLeaf.executeCursor(` + SELECT _id, customer, total, items + FROM orders + WHERE status = 'completed' +`); + +try { + // Process one document at a time without loading everything in memory + let totalRevenue = 0; + await cursor.forEach(order => { + // Process each order individually + totalRevenue += order.total; + + // Access and process nested data + order.items.forEach(item => { + // Process each item in the order + console.log(`Order ${order._id}: ${item.quantity}x ${item.name}`); + }); + }); + + console.log(`Total revenue: $${totalRevenue}`); +} finally { + // Always close the cursor when done + await cursor.close(); +} +``` + ### Array Element Access ```typescript @@ -169,11 +200,11 @@ async function getUserDashboardData(userId) { } ``` -### Product Catalog with Filtering +### Product Catalog with Filtering and Cursor-Based Pagination ```typescript -// Product catalog with filtering -async function getProductCatalog(filters = {}) { +// Product catalog with filtering and cursor-based pagination +async function getProductCatalog(filters = {}, useCursor = false) { const client = new MongoClient('mongodb://localhost:27017'); await client.connect(); @@ -217,9 +248,52 @@ async function getProductCatalog(filters = {}) { query += ` LIMIT ${filters.limit}`; } - // Execute query - return await queryLeaf.execute(query); + if (filters.offset) { + query += ` OFFSET ${filters.offset}`; + } + + // Execute query with or without cursor based on preference + if (useCursor) { + // Return a cursor for client-side pagination or streaming + return await queryLeaf.executeCursor(query); + } else { + // Return all results at once (traditional approach) + return await queryLeaf.execute(query); + } + } catch (error) { + console.error('Error fetching product catalog:', error); + throw error; + } finally { + if (!useCursor) { + // If we returned a cursor, the caller is responsible for closing the client + // after they are done with the cursor + await client.close(); + } + } +} + +// Example of using the product catalog with cursor +async function streamProductCatalog() { + const client = new MongoClient('mongodb://localhost:27017'); + await client.connect(); + + let cursor = null; + try { + const queryLeaf = new QueryLeaf(client, 'store'); + // Get a cursor for large result set + cursor = await getProductCatalog({ category: 'Electronics', inStock: true }, true); + + // Stream products to client one by one + console.log('Streaming products:'); + await cursor.forEach(product => { + console.log(`- ${product.name}: $${product.price} (${product.stock} in stock)`); + }); + } catch (error) { + console.error('Error:', error); } finally { + // Close cursor if we have one + if (cursor) await cursor.close(); + // Close client connection await client.close(); } } diff --git a/docs/usage/mongodb-client.md b/docs/usage/mongodb-client.md index c0810f0..b4fad7b 100644 --- a/docs/usage/mongodb-client.md +++ b/docs/usage/mongodb-client.md @@ -46,13 +46,25 @@ await mongoClient.connect(); Once you have a MongoDB client, you can create a QueryLeaf instance: ```typescript -import { QueryLeaf } from 'queryleaf'; +import { QueryLeaf } from '@queryleaf/lib'; // Create a QueryLeaf instance with your MongoDB client const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase'); -// Now you can execute SQL queries +// Execute SQL queries and get all results at once const results = await queryLeaf.execute('SELECT * FROM users'); + +// For large result sets, use cursor execution for better memory efficiency +const cursor = await queryLeaf.executeCursor('SELECT * FROM users'); +try { + // Process results one at a time + await cursor.forEach(user => { + console.log(`User: ${user.name}`); + }); +} finally { + // Always close the cursor when done + await cursor.close(); +} ``` ## Connection Management @@ -80,7 +92,7 @@ async function main() { // Create QueryLeaf instance const queryLeaf = new QueryLeaf(client, 'mydatabase'); - // Execute queries + // Execute queries - get all results at once const users = await queryLeaf.execute('SELECT * FROM users LIMIT 10'); console.log(`Found ${users.length} users`); @@ -90,6 +102,23 @@ async function main() { ); console.log(`Found ${products.length} electronic products`); + // For large result sets, use cursor execution + const ordersCursor = await queryLeaf.executeCursor( + 'SELECT * FROM orders WHERE total > 1000' + ); + try { + // Process results in a memory-efficient way + let count = 0; + await ordersCursor.forEach(order => { + console.log(`Processing order #${order.orderId}`); + count++; + }); + console.log(`Processed ${count} high-value orders`); + } finally { + // Always close the cursor when done + await ordersCursor.close(); + } + } catch (error) { console.error('Error:', error); } finally { diff --git a/packages/lib/README.md b/packages/lib/README.md index e5de25d..ff1355a 100644 --- a/packages/lib/README.md +++ b/packages/lib/README.md @@ -21,6 +21,7 @@ - Array element access (e.g., `items[0].name`) - GROUP BY with aggregation functions (COUNT, SUM, AVG, MIN, MAX) - JOINs between collections + - Direct MongoDB cursor access for fine-grained result processing and memory efficiency ## Installation @@ -47,6 +48,13 @@ const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase'); const results = await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); console.log(results); +// Get a MongoDB cursor for more control over result processing and memory efficiency +const cursor = await queryLeaf.executeCursor('SELECT * FROM users WHERE age > 30'); +await cursor.forEach((doc) => { + console.log(`User: ${doc.name}`); +}); +await cursor.close(); + // When you're done, close your MongoDB client await mongoClient.close(); ``` @@ -64,6 +72,13 @@ const queryLeaf = new DummyQueryLeaf('mydatabase'); // Operations will be logged to console but not executed await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); // [DUMMY MongoDB] FIND in mydatabase.users with filter: { "age": { "$gt": 21 } } + +// You can also use cursor functionality with DummyQueryLeaf +const cursor = await queryLeaf.executeCursor('SELECT * FROM users LIMIT 10'); +await cursor.forEach((doc) => { + // Process each document +}); +await cursor.close(); ``` ## SQL Query Examples @@ -85,6 +100,42 @@ SELECT status, COUNT(*) as count FROM orders GROUP BY status SELECT u.name, o.total FROM users u JOIN orders o ON u._id = o.userId ``` +## Working with Cursors + +When working with large result sets, using MongoDB cursors directly can be more memory-efficient and gives you more control over result processing: + +```typescript +// Get a cursor for a SELECT query +const cursor = await queryLeaf.executeCursor('SELECT * FROM products WHERE price > 100'); + +// Option 1: Convert to array (loads all results into memory) +const results = await cursor.toArray(); +console.log(`Found ${results.length} products`); + +// Option 2: Iterate with forEach (memory efficient) +await cursor.forEach(product => { + console.log(`Processing ${product.name}...`); +}); + +// Option 3: Manual iteration with next/hasNext (most control) +while (await cursor.hasNext()) { + const product = await cursor.next(); + // Process each product individually + console.log(`Product: ${product.name}, $${product.price}`); +} + +// Always close the cursor when done +await cursor.close(); +``` + +Features: +- Returns MongoDB `FindCursor` for normal queries and `AggregationCursor` for aggregations +- Supports all cursor methods like `forEach()`, `toArray()`, `next()`, `hasNext()` +- Efficiently handles large result sets with MongoDB's batching system +- Works with all advanced QueryLeaf features (filtering, sorting, aggregations, etc.) +- Only available for read operations (SELECT queries) +``` + ## Links - [Website](https://queryleaf.com) diff --git a/packages/lib/src/compiler.d.ts b/packages/lib/src/compiler.d.ts deleted file mode 100644 index 36842ba..0000000 --- a/packages/lib/src/compiler.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { SqlCompiler, SqlStatement, Command } from './interfaces'; -/** - * SQL to MongoDB compiler implementation - */ -export declare class SqlCompilerImpl implements SqlCompiler { - /** - * Compile a SQL statement into MongoDB commands - * @param statement SQL statement to compile - * @returns Array of MongoDB commands - */ - compile(statement: SqlStatement): Command[]; - /** - * Compile a SELECT statement into a MongoDB FIND command - */ - private compileSelect; - /** - * Compile an INSERT statement into a MongoDB INSERT command - */ - private compileInsert; - /** - * Compile an UPDATE statement into a MongoDB UPDATE command - */ - private compileUpdate; - /** - * Compile a DELETE statement into a MongoDB DELETE command - */ - private compileDelete; - /** - * Extract table name from FROM clause - */ - private extractTableName; - /** - * Convert SQL WHERE clause to MongoDB filter - */ - private convertWhere; - /** - * Convert SQL value to MongoDB value - */ - private convertValue; - /** - * Convert SQL columns to MongoDB projection - */ - private convertColumns; - /** - * Process a field name to handle nested fields and array indexing - * Converts various formats to MongoDB dot notation: - * - address.zip stays as address.zip (MongoDB supports dot notation natively) - * - items__ARRAY_0__name becomes items.0.name - * - items_0_name becomes items.0.name (from aggressive preprocessing) - * - table.column is recognized as a nested field, not a table reference - */ - private processFieldName; - /** - * Special handling for table references that might actually be nested fields - * For example, in "SELECT address.zip FROM users", - * address.zip might be parsed as table "address", column "zip" - */ - private handleNestedFieldReferences; - /** - * Process WHERE clause to handle nested field references - */ - private processWhereClauseForNestedFields; - /** - * Convert SQL ORDER BY to MongoDB sort - */ - private convertOrderBy; - /** - * Convert SQL GROUP BY to MongoDB group stage - */ - private convertGroupBy; - /** - * Create a MongoDB aggregation pipeline from a FindCommand - */ - private createAggregatePipeline; - /** - * Check if projection needs to be converted to $project format - */ - private needsAggregationProjection; - /** - * Convert a MongoDB projection to $project format used in aggregation pipeline - */ - private convertToAggregationProjection; - /** - * Convert SQL JOINs to MongoDB $lookup stages - */ - private convertJoins; - /** - * Extract join conditions from the WHERE clause - */ - private extractJoinConditions; -} diff --git a/packages/lib/src/compiler.js b/packages/lib/src/compiler.js deleted file mode 100644 index acf433e..0000000 --- a/packages/lib/src/compiler.js +++ /dev/null @@ -1,833 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SqlCompilerImpl = void 0; -const debug_1 = __importDefault(require("debug")); -const log = (0, debug_1.default)('queryleaf:compiler'); -/** - * SQL to MongoDB compiler implementation - */ -class SqlCompilerImpl { - /** - * Compile a SQL statement into MongoDB commands - * @param statement SQL statement to compile - * @returns Array of MongoDB commands - */ - compile(statement) { - const ast = statement.ast; - log('Compiling SQL AST:', JSON.stringify(ast, null, 2)); - // Pre-process the AST to handle nested fields that might be parsed as table references - this.handleNestedFieldReferences(ast); - let result; - switch (ast.type) { - case 'select': - result = [this.compileSelect(ast)]; - break; - case 'insert': - result = [this.compileInsert(ast)]; - break; - case 'update': - result = [this.compileUpdate(ast)]; - break; - case 'delete': - result = [this.compileDelete(ast)]; - break; - default: - throw new Error(`Unsupported SQL statement type: ${ast.type}`); - } - log('Compiled to MongoDB command:', JSON.stringify(result, null, 2)); - return result; - } - /** - * Compile a SELECT statement into a MongoDB FIND command - */ - compileSelect(ast) { - if (!ast.from || !Array.isArray(ast.from) || ast.from.length === 0) { - throw new Error('FROM clause is required for SELECT statements'); - } - const collection = this.extractTableName(ast.from[0]); - const command = { - type: 'FIND', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined, - projection: ast.columns ? this.convertColumns(ast.columns) : undefined, - }; - // Check if we need to use aggregate pipeline for column aliases - const hasColumnAliases = ast.columns && Array.isArray(ast.columns) && - ast.columns.some((col) => col.as); - // Handle GROUP BY clause - if (ast.groupby) { - command.group = this.convertGroupBy(ast.groupby, ast.columns); - // Check if we need to use aggregate pipeline instead of simple find - if (command.group) { - command.pipeline = this.createAggregatePipeline(command); - } - } - // Handle JOINs - if (ast.from && ast.from.length > 1) { - command.lookup = this.convertJoins(ast.from, ast.where); - // When using JOINs, we need to use the aggregate pipeline - if (!command.pipeline) { - command.pipeline = this.createAggregatePipeline(command); - } - } - // If we have column aliases, we need to use aggregate pipeline with $project - if (hasColumnAliases && !command.pipeline) { - command.pipeline = this.createAggregatePipeline(command); - } - if (ast.limit) { - log('Limit found in AST:', JSON.stringify(ast.limit, null, 2)); - if (typeof ast.limit === 'object' && 'value' in ast.limit && !Array.isArray(ast.limit.value)) { - // Standard PostgreSQL LIMIT format (without OFFSET) - command.limit = Number(ast.limit.value); - } - else if (typeof ast.limit === 'object' && 'seperator' in ast.limit && Array.isArray(ast.limit.value)) { - // Handle PostgreSQL style LIMIT [OFFSET] - if (ast.limit.value.length > 0) { - if (ast.limit.seperator === 'offset') { - if (ast.limit.value.length === 1) { - // Only OFFSET specified (OFFSET X) - command.skip = Number(ast.limit.value[0].value); - } - else if (ast.limit.value.length >= 2) { - // Both LIMIT and OFFSET specified (LIMIT X OFFSET Y) - command.limit = Number(ast.limit.value[0].value); - command.skip = Number(ast.limit.value[1].value); - } - } - else { - // Regular LIMIT without OFFSET - command.limit = Number(ast.limit.value[0].value); - } - } - // If value array is empty, it means no LIMIT was specified, so we don't set a limit - } - } - if (ast.orderby) { - command.sort = this.convertOrderBy(ast.orderby); - } - return command; - } - /** - * Compile an INSERT statement into a MongoDB INSERT command - */ - compileInsert(ast) { - if (!ast.table) { - throw new Error('Table name is required for INSERT statements'); - } - const collection = ast.table[0].table; - if (!ast.values || !Array.isArray(ast.values)) { - throw new Error('VALUES are required for INSERT statements'); - } - log('INSERT values:', JSON.stringify(ast.values, null, 2)); - log('INSERT columns:', JSON.stringify(ast.columns, null, 2)); - const documents = ast.values.map((valueList) => { - const document = {}; - if (!ast.columns || !Array.isArray(ast.columns)) { - throw new Error('Columns are required for INSERT statements'); - } - // Handle different forms of value lists - let values = []; - if (Array.isArray(valueList)) { - values = valueList; - } - else if (valueList.type === 'expr_list' && Array.isArray(valueList.value)) { - values = valueList.value; - } - else { - console.warn('Unexpected valueList format:', JSON.stringify(valueList, null, 2)); - values = [valueList]; - } - log('Processed values:', JSON.stringify(values, null, 2)); - ast.columns.forEach((column, index) => { - let columnName; - if (typeof column === 'string') { - columnName = column; - } - else if (column.column) { - columnName = column.column; - } - else { - console.warn('Unrecognized column format:', JSON.stringify(column, null, 2)); - return; - } - if (index < values.length) { - document[columnName] = this.convertValue(values[index]); - } - }); - log('Constructed document:', JSON.stringify(document, null, 2)); - return document; - }); - return { - type: 'INSERT', - collection, - documents - }; - } - /** - * Compile an UPDATE statement into a MongoDB UPDATE command - */ - compileUpdate(ast) { - if (!ast.table) { - throw new Error('Table name is required for UPDATE statements'); - } - const collection = ast.table[0].table; - if (!ast.set || !Array.isArray(ast.set)) { - throw new Error('SET clause is required for UPDATE statements'); - } - const update = {}; - ast.set.forEach((setItem) => { - if (setItem.column && setItem.value) { - update[setItem.column] = this.convertValue(setItem.value); - } - }); - return { - type: 'UPDATE', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined, - update - }; - } - /** - * Compile a DELETE statement into a MongoDB DELETE command - */ - compileDelete(ast) { - if (!ast.from || !Array.isArray(ast.from) || ast.from.length === 0) { - throw new Error('FROM clause is required for DELETE statements'); - } - const collection = this.extractTableName(ast.from[0]); - return { - type: 'DELETE', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined - }; - } - /** - * Extract table name from FROM clause - */ - extractTableName(from) { - if (typeof from === 'string') { - return from; - } - else if (from.table) { - return from.table; - } - throw new Error('Invalid FROM clause'); - } - /** - * Convert SQL WHERE clause to MongoDB filter - */ - convertWhere(where) { - if (!where) - return {}; - if (where.type === 'binary_expr') { - const { left, right, operator } = where; - // Handle logical operators (AND, OR) - if (operator === 'AND') { - const leftFilter = this.convertWhere(left); - const rightFilter = this.convertWhere(right); - return { $and: [leftFilter, rightFilter] }; - } - else if (operator === 'OR') { - const leftFilter = this.convertWhere(left); - const rightFilter = this.convertWhere(right); - return { $or: [leftFilter, rightFilter] }; - } - // Handle comparison operators - if (typeof left === 'object' && 'column' in left && left.column) { - const field = this.processFieldName(left.column); - const value = this.convertValue(right); - const filter = {}; - switch (operator) { - case '=': - filter[field] = value; - break; - case '!=': - case '<>': - filter[field] = { $ne: value }; - break; - case '>': - filter[field] = { $gt: value }; - break; - case '>=': - filter[field] = { $gte: value }; - break; - case '<': - filter[field] = { $lt: value }; - break; - case '<=': - filter[field] = { $lte: value }; - break; - case 'IN': - filter[field] = { $in: Array.isArray(value) ? value : [value] }; - break; - case 'NOT IN': - filter[field] = { $nin: Array.isArray(value) ? value : [value] }; - break; - case 'LIKE': - // Convert SQL LIKE pattern to MongoDB regex - // % wildcard in SQL becomes .* in regex - // _ wildcard in SQL becomes . in regex - const pattern = String(value) - .replace(/%/g, '.*') - .replace(/_/g, '.'); - filter[field] = { $regex: new RegExp(`^${pattern}$`, 'i') }; - break; - case 'BETWEEN': - if (Array.isArray(right) && right.length === 2) { - filter[field] = { - $gte: this.convertValue(right[0]), - $lte: this.convertValue(right[1]) - }; - } - else { - throw new Error('BETWEEN operator expects two values'); - } - break; - default: - throw new Error(`Unsupported operator: ${operator}`); - } - return filter; - } - } - else if (where.type === 'unary_expr') { - // Handle NOT, IS NULL, IS NOT NULL - if (where.operator === 'IS NULL' && typeof where.expr === 'object' && 'column' in where.expr) { - const field = this.processFieldName(where.expr.column); - return { [field]: { $eq: null } }; - } - else if (where.operator === 'IS NOT NULL' && typeof where.expr === 'object' && 'column' in where.expr) { - const field = this.processFieldName(where.expr.column); - return { [field]: { $ne: null } }; - } - else if (where.operator === 'NOT') { - const subFilter = this.convertWhere(where.expr); - return { $nor: [subFilter] }; - } - } - // If we can't parse the where clause, return an empty filter - return {}; - } - /** - * Convert SQL value to MongoDB value - */ - convertValue(value) { - if (typeof value === 'object') { - // Handle expression lists (for IN operator) - if (value.type === 'expr_list' && Array.isArray(value.value)) { - return value.value.map((item) => this.convertValue(item)); - } - // Handle single values with value property - else if ('value' in value) { - return value.value; - } - } - return value; - } - /** - * Convert SQL columns to MongoDB projection - */ - convertColumns(columns) { - const projection = {}; - log('Converting columns to projection:', JSON.stringify(columns, null, 2)); - // If * is used, return empty projection (which means all fields) - if (columns.some(col => col === '*' || - (typeof col === 'object' && col.expr && col.expr.type === 'star') || - (typeof col === 'object' && col.expr && col.expr.column === '*'))) { - log('Star (*) detected, returning empty projection'); - return {}; - } - columns.forEach(column => { - if (typeof column === 'object') { - if ('expr' in column && column.expr) { - // Handle dot notation (nested fields) - if ('column' in column.expr && column.expr.column) { - const fieldName = this.processFieldName(column.expr.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - else if (column.expr.type === 'column_ref' && column.expr.column) { - const fieldName = this.processFieldName(column.expr.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - else if (column.expr.type === 'binary_expr' && column.expr.operator === '.' && - column.expr.left && column.expr.right) { - // Handle explicit dot notation like table.column - let fieldName = ''; - if (column.expr.left.column) { - fieldName = column.expr.left.column; - } - if (fieldName && column.expr.right.column) { - fieldName += '.' + column.expr.right.column; - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // Also include the parent field - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - } - else if ('type' in column && column.type === 'column_ref' && column.column) { - const fieldName = this.processFieldName(column.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - else if ('column' in column) { - const fieldName = this.processFieldName(column.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - } - else if (typeof column === 'string') { - const fieldName = this.processFieldName(column); - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - }); - log('Final projection:', JSON.stringify(projection, null, 2)); - return projection; - } - /** - * Process a field name to handle nested fields and array indexing - * Converts various formats to MongoDB dot notation: - * - address.zip stays as address.zip (MongoDB supports dot notation natively) - * - items__ARRAY_0__name becomes items.0.name - * - items_0_name becomes items.0.name (from aggressive preprocessing) - * - table.column is recognized as a nested field, not a table reference - */ - processFieldName(fieldName) { - if (!fieldName) - return fieldName; - log(`Processing field name: "${fieldName}"`); - // First convert our placeholder format back to MongoDB dot notation - // This transforms items__ARRAY_0__name => items.0.name - let processed = fieldName.replace(/__ARRAY_(\d+)__/g, '.$1.'); - // Also handle the case where it's at the end of the string - processed = processed.replace(/__ARRAY_(\d+)$/g, '.$1'); - // Handle the aggressive preprocessing format - items_0_name => items.0.name - processed = processed.replace(/(\w+)_(\d+)_(\w+)/g, '$1.$2.$3'); - processed = processed.replace(/(\w+)_(\d+)$/g, '$1.$2'); - // If there's still array indexing with bracket notation, convert it too - // This handles any direct [0] syntax that might have made it through the parser - processed = processed.replace(/\[(\d+)\]/g, '.$1'); - // Handle nested field access directly - // MongoDB already uses dot notation for nested fields, so we can use it as is - if (processed.includes('.')) { - log(`Using nested field in MongoDB filter: "${processed}"`); - } - return processed; - } - /** - * Special handling for table references that might actually be nested fields - * For example, in "SELECT address.zip FROM users", - * address.zip might be parsed as table "address", column "zip" - */ - handleNestedFieldReferences(ast) { - log('Handling nested field references in AST'); - // Handle column references in SELECT clause - if (ast.columns && Array.isArray(ast.columns)) { - ast.columns.forEach((column) => { - if (column.expr && column.expr.type === 'column_ref' && - column.expr.table && column.expr.column) { - // This could be a nested field - convert table.column to a single column path - column.expr.column = `${column.expr.table}.${column.expr.column}`; - column.expr.table = null; - log(`Converted SELECT column to nested field: ${column.expr.column}`); - } - }); - } - // Handle conditions in WHERE clause - this.processWhereClauseForNestedFields(ast.where); - // For debugging - show the resulting AST after transformation - log('AST after nested field handling:', JSON.stringify(ast?.where, null, 2)); - } - /** - * Process WHERE clause to handle nested field references - */ - processWhereClauseForNestedFields(where) { - if (!where) - return; - log('Processing WHERE clause for nested fields:', JSON.stringify(where, null, 2)); - if (where.type === 'binary_expr') { - // Process left and right sides recursively - this.processWhereClauseForNestedFields(where.left); - this.processWhereClauseForNestedFields(where.right); - // Handle column references in comparison expressions - if (where.left && where.left.type === 'column_ref') { - log('Processing column reference:', JSON.stringify(where.left, null, 2)); - // Handle both direct dot notation in column name and table.column format - if (where.left.column && where.left.column.includes('.')) { - // Already has dot notation, just keep it - log('Column already has dot notation:', where.left.column); - } - else if (where.left.table && where.left.column) { - // Convert table.column format to a nested field path - log('Converting table.column to nested path:', `${where.left.table}.${where.left.column}`); - where.left.column = `${where.left.table}.${where.left.column}`; - where.left.table = null; - } - } - } - else if (where.type === 'unary_expr') { - // Process expression in unary operators - this.processWhereClauseForNestedFields(where.expr); - } - } - /** - * Convert SQL ORDER BY to MongoDB sort - */ - convertOrderBy(orderby) { - const sort = {}; - orderby.forEach(item => { - if (typeof item === 'object' && 'expr' in item && item.expr) { - if ('column' in item.expr && item.expr.column) { - const column = this.processFieldName(item.expr.column); - sort[column] = item.type === 'ASC' ? 1 : -1; - } - } - }); - return sort; - } - /** - * Convert SQL GROUP BY to MongoDB group stage - */ - convertGroupBy(groupby, columns) { - if (!groupby || !Array.isArray(groupby) || groupby.length === 0) { - return undefined; - } - log('Converting GROUP BY:', JSON.stringify(groupby, null, 2)); - log('With columns:', JSON.stringify(columns, null, 2)); - // Create the group stage - let group; - // If there's only one group by field, simplify the _id structure - if (groupby.length === 1) { - // Extract the single field name - let singleField = ''; - if (typeof groupby[0] === 'object') { - // Type 1: { column: 'field' } - if (groupby[0].column) { - singleField = this.processFieldName(groupby[0].column); - } - // Type 2: { type: 'column_ref', column: 'field' } - else if (groupby[0].type === 'column_ref' && groupby[0].column) { - singleField = this.processFieldName(groupby[0].column); - } - // Type 3: { expr: { column: 'field' } } - else if (groupby[0].expr && groupby[0].expr.column) { - singleField = this.processFieldName(groupby[0].expr.column); - } - } - if (singleField) { - // For a single field, use a simplified ID structure - group = { - _id: `$${singleField}`, - [singleField]: { $first: `$${singleField}` } // Include the field in results too - }; - } - else { - // Fallback if we can't extract the field - group = { _id: null }; - } - } - else { - // For multiple fields, use the object structure for _id - const groupFields = {}; - groupby.forEach(item => { - if (typeof item === 'object') { - let field = ''; - // Type 1: { column: 'field' } - if (item.column) { - field = this.processFieldName(item.column); - } - // Type 2: { type: 'column_ref', column: 'field' } - else if (item.type === 'column_ref' && item.column) { - field = this.processFieldName(item.column); - } - // Type 3: { expr: { column: 'field' } } - else if (item.expr && item.expr.column) { - field = this.processFieldName(item.expr.column); - } - if (field) { - groupFields[field] = `$${field}`; - } - } - }); - group = { - _id: groupFields - }; - } - // Add aggregations for other columns - if (columns && Array.isArray(columns)) { - columns.forEach(column => { - if (typeof column === 'object') { - // Check for aggregation functions like COUNT, SUM, AVG, etc. - if (column.expr && column.expr.type && - (column.expr.type === 'function' || column.expr.type === 'aggr_func')) { - const funcName = column.expr.name.toLowerCase(); - const args = column.expr.args && column.expr.args.expr ? - column.expr.args.expr : - column.expr.args; - let field = '*'; - if (args && args.column) { - field = this.processFieldName(args.column); - } - else if (args && args.type === 'star') { - // COUNT(*) case - field = '*'; - } - // Use the specified alias or create one - let alias = column.as || `${funcName}_${field}`; - // Map SQL functions to MongoDB aggregation operators - switch (funcName) { - case 'count': - group[alias] = { $sum: 1 }; - break; - case 'sum': - group[alias] = { $sum: `$${field}` }; - break; - case 'avg': - group[alias] = { $avg: `$${field}` }; - break; - case 'min': - group[alias] = { $min: `$${field}` }; - break; - case 'max': - group[alias] = { $max: `$${field}` }; - break; - } - } - else if (column.expr && column.expr.type === 'column_ref') { - // Include GROUP BY fields directly in the results - const field = this.processFieldName(column.expr.column); - // Only add if this is one of our group by fields - const isGroupByField = groupby.some(g => { - if (typeof g === 'object') { - if (g.column) { - return g.column === column.expr.column; - } - else if (g.type === 'column_ref' && g.column) { - return g.column === column.expr.column; - } - else if (g.expr && g.expr.column) { - return g.expr.column === column.expr.column; - } - } - return false; - }); - if (isGroupByField) { - // Use $first to just take the first value from each group - // since all values in the group should be the same for this field - group[field] = { $first: `$${field}` }; - } - } - } - }); - } - log('Generated group stage:', JSON.stringify(group, null, 2)); - return group; - } - /** - * Create a MongoDB aggregation pipeline from a FindCommand - */ - createAggregatePipeline(command) { - const pipeline = []; - // Start with $match if we have a filter - if (command.filter) { - pipeline.push({ $match: command.filter }); - } - // Add $lookup stages for JOINs - if (command.lookup && command.lookup.length > 0) { - command.lookup.forEach(lookup => { - pipeline.push({ - $lookup: { - from: lookup.from, - localField: lookup.localField, - foreignField: lookup.foreignField, - as: lookup.as - } - }); - // Add $unwind stage to flatten the joined array - pipeline.push({ - $unwind: { - path: "$" + lookup.as, - preserveNullAndEmptyArrays: true - } - }); - }); - } - // Add $group stage if grouping is requested - if (command.group) { - pipeline.push({ $group: command.group }); - } - // Add $sort if sort is specified - if (command.sort) { - pipeline.push({ $sort: command.sort }); - } - // Add $skip if skip is specified - if (command.skip) { - pipeline.push({ $skip: command.skip }); - } - // Add $limit if limit is specified - if (command.limit) { - pipeline.push({ $limit: command.limit }); - } - // Add $project if projection is specified - if (command.projection && Object.keys(command.projection).length > 0) { - const projectionFormat = this.needsAggregationProjection(command.projection) - ? this.convertToAggregationProjection(command.projection) - : command.projection; - pipeline.push({ $project: projectionFormat }); - } - log('Generated aggregate pipeline:', JSON.stringify(pipeline, null, 2)); - return pipeline; - } - /** - * Check if projection needs to be converted to $project format - */ - needsAggregationProjection(projection) { - // Check if any value is a string that starts with $ - return Object.values(projection).some(value => typeof value === 'string' && value.startsWith('$')); - } - /** - * Convert a MongoDB projection to $project format used in aggregation pipeline - */ - convertToAggregationProjection(projection) { - const result = {}; - for (const [key, value] of Object.entries(projection)) { - if (typeof value === 'string' && value.startsWith('$')) { - // This is a field reference, keep it as is - result[key] = value; - } - else if (value === 1) { - // For 1 values, keep as 1 for MongoDB's $project stage - result[key] = 1; - } - else { - // Otherwise, keep as is - result[key] = value; - } - } - return result; - } - /** - * Convert SQL JOINs to MongoDB $lookup stages - */ - convertJoins(from, where) { - if (!from || !Array.isArray(from) || from.length <= 1) { - return []; - } - log('Converting JOINs:', JSON.stringify(from, null, 2)); - log('With WHERE:', JSON.stringify(where, null, 2)); - const lookups = []; - const mainTable = this.extractTableName(from[0]); - // Extract join conditions from the WHERE clause - // This is a simplification that assumes the ON conditions are in the WHERE clause - const joinConditions = this.extractJoinConditions(where, from); - // Process each table after the first one (the main table) - for (let i = 1; i < from.length; i++) { - const joinedTable = this.extractTableName(from[i]); - const alias = from[i].as || joinedTable; - // Look for JOIN condition for this table - const joinCond = joinConditions.find(cond => (cond.leftTable === mainTable && cond.rightTable === joinedTable) || - (cond.leftTable === joinedTable && cond.rightTable === mainTable)); - if (joinCond) { - const localField = joinCond.leftTable === mainTable ? joinCond.leftField : joinCond.rightField; - const foreignField = joinCond.leftTable === mainTable ? joinCond.rightField : joinCond.leftField; - lookups.push({ - from: joinedTable, - localField, - foreignField, - as: alias - }); - } - else { - // If no explicit join condition was found, assume it's a cross join - // or guess based on common naming conventions (e.g., userId -> _id) - let localField = '_id'; - let foreignField = `${mainTable.toLowerCase().replace(/s$/, '')}Id`; - lookups.push({ - from: joinedTable, - localField, - foreignField, - as: alias - }); - } - } - log('Generated lookups:', JSON.stringify(lookups, null, 2)); - return lookups; - } - /** - * Extract join conditions from the WHERE clause - */ - extractJoinConditions(where, tables) { - if (!where) { - return []; - } - const tableNames = tables.map(t => { - if (typeof t === 'string') - return t; - return t.table; - }); - const conditions = []; - // For equality comparisons in the WHERE clause that reference different tables - if (where.type === 'binary_expr' && where.operator === '=') { - if (where.left && where.left.type === 'column_ref' && where.left.table && - where.right && where.right.type === 'column_ref' && where.right.table) { - const leftTable = where.left.table; - const leftField = where.left.column; - const rightTable = where.right.table; - const rightField = where.right.column; - if (tableNames.includes(leftTable) && tableNames.includes(rightTable)) { - conditions.push({ - leftTable, - leftField, - rightTable, - rightField - }); - } - } - } - // For AND conditions, recursively extract join conditions from both sides - else if (where.type === 'binary_expr' && where.operator === 'AND') { - const leftConditions = this.extractJoinConditions(where.left, tables); - const rightConditions = this.extractJoinConditions(where.right, tables); - conditions.push(...leftConditions, ...rightConditions); - } - return conditions; - } -} -exports.SqlCompilerImpl = SqlCompilerImpl; -//# sourceMappingURL=compiler.js.map \ No newline at end of file diff --git a/packages/lib/src/compiler.js.map b/packages/lib/src/compiler.js.map deleted file mode 100644 index 0184e30..0000000 --- a/packages/lib/src/compiler.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"compiler.js","sourceRoot":"","sources":["compiler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/lib/src/examples/basic-usage.ts b/packages/lib/src/examples/basic-usage.ts index c2a411f..5c037d3 100644 --- a/packages/lib/src/examples/basic-usage.ts +++ b/packages/lib/src/examples/basic-usage.ts @@ -1,5 +1,5 @@ -import { QueryLeaf } from '../index'; -import { MongoClient } from 'mongodb'; +import { QueryLeaf, CursorResult } from '../index'; +import { Document, FindCursor, AggregationCursor, MongoClient } from 'mongodb'; /** * Example showing how to use QueryLeaf with an existing MongoDB client @@ -100,6 +100,32 @@ async function main() { console.error('Error:', error instanceof Error ? error.message : String(error)); } } + // Example showing how to use the executeCursor method + console.log('\nUsing the executeCursor method:'); + + let cursor: CursorResult = null; + try { + // Using the executeCursor method to get a MongoDB cursor + cursor = await queryLeaf.executeCursor('SELECT * FROM users WHERE active = true'); + + // Check if we got a cursor back + if (cursor) { + // Now we can use the cursor methods directly + console.log('Iterating through cursor results with forEach:'); + await cursor.forEach((doc: any) => { + console.log(`- User: ${doc.name}, Email: ${doc.email}`); + }); + } else { + console.log('Expected a cursor but got null (not a SELECT/FIND query)'); + } + } catch (error) { + console.error('Cursor error:', error instanceof Error ? error.message : String(error)); + } finally { + // Always close the cursor when done + if (cursor) { + await cursor.close(); + } + } } finally { // Close the MongoDB client that we created // QueryLeaf does not manage MongoDB connections diff --git a/packages/lib/src/executor/dummy-client.d.ts b/packages/lib/src/executor/dummy-client.d.ts deleted file mode 100644 index 1fc5091..0000000 --- a/packages/lib/src/executor/dummy-client.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MongoClient, Db } from 'mongodb'; -/** - * A dummy MongoDB client that mimics the MongoDB client interface - * Logs operations instead of executing them - useful for testing and debugging - */ -export declare class DummyMongoClient extends MongoClient { - private databases; - /** - * Create a new dummy client - */ - constructor(); - /** - * Get a dummy database - * @param dbName Database name - * @returns A dummy database instance - */ - db(dbName: string): Db; - /** - * Simulate connection - no actual connection is made - */ - connect(): Promise; - /** - * Simulate closing the connection - */ - close(): Promise; -} diff --git a/packages/lib/src/executor/dummy-client.js b/packages/lib/src/executor/dummy-client.js deleted file mode 100644 index e0df76a..0000000 --- a/packages/lib/src/executor/dummy-client.js +++ /dev/null @@ -1,210 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DummyMongoClient = void 0; -const mongodb_1 = require("mongodb"); -/** - * Dummy MongoDB database that logs operations instead of executing them - */ -class DummyDb { - constructor(dbName) { - this.collections = new Map(); - this.dbName = dbName; - } - /** - * Get a collection from the dummy database - * @param name Collection name - * @returns A dummy collection - */ - collection(name) { - if (!this.collections.has(name)) { - this.collections.set(name, new DummyCollection(name, this.dbName)); - } - return this.collections.get(name); - } -} -/** - * Dummy MongoDB collection that logs operations instead of executing them - */ -class DummyCollection { - constructor(name, dbName) { - this.name = name; - this.dbName = dbName; - } - /** - * Log a find operation - * @param filter Query filter - * @returns A chainable cursor - */ - find(filter = {}) { - console.log(`[DUMMY MongoDB] FIND in ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - return new DummyCursor(this.name, 'find', filter); - } - /** - * Log an insertMany operation - * @param documents Documents to insert - * @returns A dummy result - */ - async insertMany(documents) { - console.log(`[DUMMY MongoDB] INSERT into ${this.dbName}.${this.name}:`, JSON.stringify(documents, null, 2)); - return { - acknowledged: true, - insertedCount: documents.length, - insertedIds: documents.map((_, i) => i) - }; - } - /** - * Log an updateMany operation - * @param filter Query filter - * @param update Update operation - * @returns A dummy result - */ - async updateMany(filter = {}, update) { - console.log(`[DUMMY MongoDB] UPDATE in ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - console.log(`[DUMMY MongoDB] UPDATE operation:`, JSON.stringify(update, null, 2)); - return { - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - }; - } - /** - * Log a deleteMany operation - * @param filter Query filter - * @returns A dummy result - */ - async deleteMany(filter = {}) { - console.log(`[DUMMY MongoDB] DELETE from ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - return { - acknowledged: true, - deletedCount: 1 - }; - } - /** - * Log an aggregate operation - * @param pipeline Aggregation pipeline - * @returns A chainable cursor - */ - aggregate(pipeline) { - console.log(`[DUMMY MongoDB] AGGREGATE in ${this.dbName}.${this.name} with pipeline:`, JSON.stringify(pipeline, null, 2)); - return new DummyCursor(this.name, 'aggregate', null, pipeline); - } -} -/** - * Dummy MongoDB cursor that logs operations instead of executing them - */ -class DummyCursor { - constructor(collectionName, operation, filter = null, pipeline = null) { - this.projectionObj = null; - this.sortObj = null; - this.limitVal = null; - this.skipVal = null; - this.collectionName = collectionName; - this.operation = operation; - this.filter = filter; - this.pipeline = pipeline; - } - /** - * Add projection to the cursor - * @param projection Projection specification - * @returns The cursor - */ - project(projection) { - console.log(`[DUMMY MongoDB] Adding projection to ${this.operation}:`, JSON.stringify(projection, null, 2)); - this.projectionObj = projection; - return this; - } - /** - * Add sort to the cursor - * @param sort Sort specification - * @returns The cursor - */ - sort(sort) { - console.log(`[DUMMY MongoDB] Adding sort to ${this.operation}:`, JSON.stringify(sort, null, 2)); - this.sortObj = sort; - return this; - } - /** - * Add limit to the cursor - * @param limit Limit value - * @returns The cursor - */ - limit(limit) { - console.log(`[DUMMY MongoDB] Adding limit to ${this.operation}:`, limit); - this.limitVal = limit; - return this; - } - /** - * Add skip to the cursor - * @param skip Skip value - * @returns The cursor - */ - skip(skip) { - console.log(`[DUMMY MongoDB] Adding skip to ${this.operation}:`, skip); - this.skipVal = skip; - return this; - } - /** - * Convert the cursor to an array of results - * @returns A dummy array of results - */ - async toArray() { - console.log(`[DUMMY MongoDB] Executing ${this.operation} on ${this.collectionName}`); - if (this.projectionObj) - console.log(` - Projection:`, JSON.stringify(this.projectionObj, null, 2)); - if (this.sortObj) - console.log(` - Sort:`, JSON.stringify(this.sortObj, null, 2)); - if (this.limitVal !== null && this.limitVal > 0) - console.log(` - Limit:`, this.limitVal); - if (this.skipVal !== null) - console.log(` - Skip:`, this.skipVal); - // Return a dummy result indicating this is a simulation - return [{ - _id: 'dummy-id', - operation: this.operation, - message: 'This is a dummy result from the DummyClient' - }]; - } -} -/** - * A dummy MongoDB client that mimics the MongoDB client interface - * Logs operations instead of executing them - useful for testing and debugging - */ -class DummyMongoClient extends mongodb_1.MongoClient { - /** - * Create a new dummy client - */ - constructor() { - // Pass an empty string since we're not actually connecting - super('mongodb://dummy'); - this.databases = new Map(); - } - /** - * Get a dummy database - * @param dbName Database name - * @returns A dummy database instance - */ - db(dbName) { - console.log(`[DUMMY MongoDB] Using database: ${dbName}`); - if (!this.databases.has(dbName)) { - this.databases.set(dbName, new DummyDb(dbName)); - } - return this.databases.get(dbName); - } - /** - * Simulate connection - no actual connection is made - */ - async connect() { - console.log('[DUMMY MongoDB] Connected to MongoDB (simulated)'); - return this; - } - /** - * Simulate closing the connection - */ - async close() { - console.log('[DUMMY MongoDB] Closed MongoDB connection (simulated)'); - } -} -exports.DummyMongoClient = DummyMongoClient; -//# sourceMappingURL=dummy-client.js.map \ No newline at end of file diff --git a/packages/lib/src/executor/dummy-client.js.map b/packages/lib/src/executor/dummy-client.js.map deleted file mode 100644 index 2683cf8..0000000 --- a/packages/lib/src/executor/dummy-client.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"dummy-client.js","sourceRoot":"","sources":["dummy-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/lib/src/executor/dummy-client.ts b/packages/lib/src/executor/dummy-client.ts index 6c053b9..0f80367 100644 --- a/packages/lib/src/executor/dummy-client.ts +++ b/packages/lib/src/executor/dummy-client.ts @@ -210,6 +210,51 @@ class DummyCursor { }, ]; } + + /** + * Dummy method to simulate cursor forEach + * @param callback Function to execute for each document + */ + async forEach(callback: (doc: any) => void): Promise { + const results = await this.toArray(); + for (const doc of results) { + callback(doc); + } + } + + /** + * Dummy method to simulate cursor next + * @returns The next document or null if none + */ + async next(): Promise { + const results = await this.toArray(); + return results.length > 0 ? results[0] : null; + } + + /** + * Dummy method to simulate cursor hasNext + * @returns Whether there are more documents + */ + async hasNext(): Promise { + const results = await this.toArray(); + return results.length > 0; + } + + /** + * Dummy method to simulate cursor count + * @returns The count of documents in the cursor + */ + async count(): Promise { + const results = await this.toArray(); + return results.length; + } + + /** + * Dummy method to simulate cursor close + */ + async close(): Promise { + console.log(`[DUMMY MongoDB] Closing cursor for ${this.operation} on ${this.collectionName}`); + } } /** diff --git a/packages/lib/src/executor/index.d.ts b/packages/lib/src/executor/index.d.ts deleted file mode 100644 index 9cbf49b..0000000 --- a/packages/lib/src/executor/index.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CommandExecutor, Command } from '../interfaces'; -import { MongoClient } from 'mongodb'; -/** - * MongoDB command executor implementation for Node.js - */ -export declare class MongoExecutor implements CommandExecutor { - private client; - private dbName; - /** - * Create a new MongoDB executor using a MongoDB client - * @param client MongoDB client instance - * @param dbName Database name - */ - constructor(client: MongoClient, dbName: string); - /** - * No-op - client lifecycle is managed by the user - */ - connect(): Promise; - /** - * No-op - client lifecycle is managed by the user - */ - close(): Promise; - /** - * Execute a series of MongoDB commands - * @param commands Array of commands to execute - * @returns Result of the last command - */ - execute(commands: Command[]): Promise; - /** - * Convert string ObjectIds to MongoDB ObjectId instances - * @param obj Object to convert - * @returns Object with converted ObjectIds - */ - private convertObjectIds; -} diff --git a/packages/lib/src/executor/index.js b/packages/lib/src/executor/index.js deleted file mode 100644 index 921f45d..0000000 --- a/packages/lib/src/executor/index.js +++ /dev/null @@ -1,143 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.MongoExecutor = void 0; -const mongodb_1 = require("mongodb"); -/** - * MongoDB command executor implementation for Node.js - */ -class MongoExecutor { - /** - * Create a new MongoDB executor using a MongoDB client - * @param client MongoDB client instance - * @param dbName Database name - */ - constructor(client, dbName) { - this.client = client; - this.dbName = dbName; - } - /** - * No-op - client lifecycle is managed by the user - */ - async connect() { - // Connection is managed by the user - } - /** - * No-op - client lifecycle is managed by the user - */ - async close() { - // Connection is managed by the user - } - /** - * Execute a series of MongoDB commands - * @param commands Array of commands to execute - * @returns Result of the last command - */ - async execute(commands) { - // We assume the client is already connected - const database = this.client.db(this.dbName); - // Execute each command in sequence - let result = null; - for (const command of commands) { - switch (command.type) { - case 'FIND': - const findCursor = database.collection(command.collection) - .find(this.convertObjectIds(command.filter || {})); - // Apply projection if specified - if (command.projection) { - findCursor.project(command.projection); - } - // Apply sorting if specified - if (command.sort) { - findCursor.sort(command.sort); - } - // Apply pagination if specified - if (command.skip) { - findCursor.skip(command.skip); - } - if (command.limit && command.limit > 0) { - findCursor.limit(command.limit); - } - result = await findCursor.toArray(); - break; - case 'INSERT': - result = await database.collection(command.collection) - .insertMany(command.documents.map(doc => this.convertObjectIds(doc))); - break; - case 'UPDATE': - result = await database.collection(command.collection) - .updateMany(this.convertObjectIds(command.filter || {}), { $set: this.convertObjectIds(command.update) }); - break; - case 'DELETE': - result = await database.collection(command.collection) - .deleteMany(this.convertObjectIds(command.filter || {})); - break; - case 'AGGREGATE': - // Handle aggregation commands - const pipeline = command.pipeline.map(stage => this.convertObjectIds(stage)); - result = await database.collection(command.collection) - .aggregate(pipeline).toArray(); - break; - default: - throw new Error(`Unsupported command type: ${command.type}`); - } - } - return result; - } - /** - * Convert string ObjectIds to MongoDB ObjectId instances - * @param obj Object to convert - * @returns Object with converted ObjectIds - */ - convertObjectIds(obj) { - if (!obj) - return obj; - if (Array.isArray(obj)) { - return obj.map(item => this.convertObjectIds(item)); - } - if (typeof obj === 'object') { - const result = {}; - for (const [key, value] of Object.entries(obj)) { - // Special handling for _id field and fields ending with Id - if ((key === '_id' || key.endsWith('Id') || key.endsWith('Ids')) && typeof value === 'string') { - try { - // Check if it's a valid ObjectId string - if (/^[0-9a-fA-F]{24}$/.test(value)) { - result[key] = new mongodb_1.ObjectId(value); - continue; - } - } - catch (error) { - // If it's not a valid ObjectId, keep it as a string - console.warn(`Could not convert ${key} value to ObjectId: ${value}`); - } - } - else if (Array.isArray(value) && (key.endsWith('Ids') || key === 'productIds')) { - // For arrays of IDs - result[key] = value.map((item) => { - if (typeof item === 'string' && /^[0-9a-fA-F]{24}$/.test(item)) { - try { - return new mongodb_1.ObjectId(item); - } - catch (error) { - return item; - } - } - return this.convertObjectIds(item); - }); - continue; - } - else if (typeof value === 'object' && value !== null) { - // Recursively convert nested objects - result[key] = this.convertObjectIds(value); - continue; - } - // Copy other values as is - result[key] = value; - } - return result; - } - return obj; - } -} -exports.MongoExecutor = MongoExecutor; -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/lib/src/executor/index.js.map b/packages/lib/src/executor/index.js.map deleted file mode 100644 index 0a9a297..0000000 --- a/packages/lib/src/executor/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/lib/src/executor/index.ts b/packages/lib/src/executor/index.ts index eb6821e..6646ede 100644 --- a/packages/lib/src/executor/index.ts +++ b/packages/lib/src/executor/index.ts @@ -1,5 +1,5 @@ -import { CommandExecutor, Command } from '../interfaces'; -import { MongoClient, ObjectId } from 'mongodb'; +import { CommandExecutor, Command, ExecutionResult, CursorResult } from '../interfaces'; +import { Document, MongoClient, ObjectId } from 'mongodb'; /** * MongoDB command executor implementation for Node.js @@ -33,13 +33,13 @@ export class MongoExecutor implements CommandExecutor { } /** - * Execute a series of MongoDB commands + * Execute a series of MongoDB commands and return documents * @param commands Array of commands to execute - * @returns Result of the last command + * @returns Result of the last command as documents (not cursors) + * @typeParam T - The type of documents that will be returned (defaults to Document) */ - async execute(commands: Command[]): Promise { + async execute(commands: Command[]): Promise> { // We assume the client is already connected - const database = this.client.db(this.dbName); // Execute each command in sequence @@ -69,6 +69,7 @@ export class MongoExecutor implements CommandExecutor { findCursor.limit(command.limit); } + // Always return array for the regular execute result = await findCursor.toArray(); break; @@ -96,7 +97,10 @@ export class MongoExecutor implements CommandExecutor { case 'AGGREGATE': // Handle aggregation commands const pipeline = command.pipeline.map((stage) => this.convertObjectIds(stage)); - result = await database.collection(command.collection).aggregate(pipeline).toArray(); + const aggregateCursor = database.collection(command.collection).aggregate(pipeline); + + // Always return array for the regular execute + result = await aggregateCursor.toArray(); break; default: @@ -107,6 +111,68 @@ export class MongoExecutor implements CommandExecutor { return result; } + /** + * Execute a series of MongoDB commands and return cursors + * @param commands Array of commands to execute + * @returns Cursor for FIND and AGGREGATE commands, null for other commands + * @typeParam T - The type of documents that will be returned (defaults to Document) + */ + async executeCursor(commands: Command[]): Promise> { + // We assume the client is already connected + const database = this.client.db(this.dbName); + + // Execute each command in sequence, but only return a cursor for the last command + // if it's a FIND or AGGREGATE + let result = null; + + for (const command of commands) { + switch (command.type) { + case 'FIND': + const findCursor = database + .collection(command.collection) + .find(this.convertObjectIds(command.filter || {})); + + // Apply projection if specified + if (command.projection) { + findCursor.project(command.projection); + } + + // Apply sorting if specified + if (command.sort) { + findCursor.sort(command.sort); + } + + // Apply pagination if specified + if (command.skip) { + findCursor.skip(command.skip); + } + if (command.limit && command.limit > 0) { + findCursor.limit(command.limit); + } + + // Return the cursor directly + result = findCursor; + break; + + case 'AGGREGATE': + // Handle aggregation commands + const pipeline = command.pipeline.map((stage) => this.convertObjectIds(stage)); + result = database.collection(command.collection).aggregate(pipeline); + break; + + case 'INSERT': + case 'UPDATE': + case 'DELETE': + // For non-cursor commands, execute them but don't return a cursor + throw new Error(`Cannot return cursor for ${(command as any).type}`); + default: + throw new Error(`Unsupported command type: ${(command as any).type}`); + } + } + + return result as CursorResult; + } + /** * Convert string ObjectIds to MongoDB ObjectId instances * @param obj Object to convert diff --git a/packages/lib/src/index.d.ts b/packages/lib/src/index.d.ts deleted file mode 100644 index ce8cc88..0000000 --- a/packages/lib/src/index.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { SqlStatement, Command, SqlParser, SqlCompiler, CommandExecutor } from './interfaces'; -import { MongoClient } from 'mongodb'; -import { SqlParserImpl } from './parser'; -import { SqlCompilerImpl } from './compiler'; -import { MongoExecutor } from './executor'; -import { DummyMongoClient } from './executor/dummy-client'; -/** - * QueryLeaf: SQL to MongoDB query translator - */ -export declare class QueryLeaf { - private parser; - private compiler; - private executor; - /** - * Create a new QueryLeaf instance with your MongoDB client - * @param client Your MongoDB client - * @param dbName Database name - */ - constructor(client: MongoClient, dbName: string); - /** - * Execute a SQL query on MongoDB - * @param sql SQL query string - * @returns Query results - */ - execute(sql: string): Promise; - /** - * Parse a SQL query string - * @param sql SQL query string - * @returns Parsed SQL statement - */ - parse(sql: string): SqlStatement; - /** - * Compile a SQL statement to MongoDB commands - * @param statement SQL statement - * @returns MongoDB commands - */ - compile(statement: SqlStatement): Command[]; - /** - * Get the command executor instance - * @returns Command executor - */ - getExecutor(): CommandExecutor; - /** - * No-op method for backward compatibility - * QueryLeaf no longer manages MongoDB connections - */ - close(): Promise; -} -/** - * Create a QueryLeaf instance with a dummy client for testing - * No actual MongoDB connection is made - */ -export declare class DummyQueryLeaf extends QueryLeaf { - /** - * Create a new DummyQueryLeaf instance - * @param dbName Database name - */ - constructor(dbName: string); -} -export { - SqlStatement, - Command, - SqlParser, - SqlCompiler, - CommandExecutor, - SqlParserImpl, - SqlCompilerImpl, - MongoExecutor, - DummyMongoClient, -}; -export * from './interfaces'; diff --git a/packages/lib/src/index.js b/packages/lib/src/index.js deleted file mode 100644 index d1628c1..0000000 --- a/packages/lib/src/index.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DummyMongoClient = exports.MongoExecutor = exports.SqlCompilerImpl = exports.SqlParserImpl = exports.DummyQueryLeaf = exports.QueryLeaf = void 0; -const parser_1 = require("./parser"); -Object.defineProperty(exports, "SqlParserImpl", { enumerable: true, get: function () { return parser_1.SqlParserImpl; } }); -const compiler_1 = require("./compiler"); -Object.defineProperty(exports, "SqlCompilerImpl", { enumerable: true, get: function () { return compiler_1.SqlCompilerImpl; } }); -const executor_1 = require("./executor"); -Object.defineProperty(exports, "MongoExecutor", { enumerable: true, get: function () { return executor_1.MongoExecutor; } }); -const dummy_client_1 = require("./executor/dummy-client"); -Object.defineProperty(exports, "DummyMongoClient", { enumerable: true, get: function () { return dummy_client_1.DummyMongoClient; } }); -/** - * QueryLeaf: SQL to MongoDB query translator - */ -class QueryLeaf { - /** - * Create a new QueryLeaf instance with your MongoDB client - * @param client Your MongoDB client - * @param dbName Database name - */ - constructor(client, dbName) { - this.parser = new parser_1.SqlParserImpl(); - this.compiler = new compiler_1.SqlCompilerImpl(); - this.executor = new executor_1.MongoExecutor(client, dbName); - } - /** - * Execute a SQL query on MongoDB - * @param sql SQL query string - * @returns Query results - */ - async execute(sql) { - const statement = this.parse(sql); - const commands = this.compile(statement); - return await this.executor.execute(commands); - } - /** - * Parse a SQL query string - * @param sql SQL query string - * @returns Parsed SQL statement - */ - parse(sql) { - return this.parser.parse(sql); - } - /** - * Compile a SQL statement to MongoDB commands - * @param statement SQL statement - * @returns MongoDB commands - */ - compile(statement) { - return this.compiler.compile(statement); - } - /** - * Get the command executor instance - * @returns Command executor - */ - getExecutor() { - return this.executor; - } - /** - * No-op method for backward compatibility - * QueryLeaf no longer manages MongoDB connections - */ - async close() { - // No-op - MongoDB client is managed by the user - } -} -exports.QueryLeaf = QueryLeaf; -/** - * Create a QueryLeaf instance with a dummy client for testing - * No actual MongoDB connection is made - */ -class DummyQueryLeaf extends QueryLeaf { - /** - * Create a new DummyQueryLeaf instance - * @param dbName Database name - */ - constructor(dbName) { - super(new dummy_client_1.DummyMongoClient(), dbName); - } -} -exports.DummyQueryLeaf = DummyQueryLeaf; -// Re-export interfaces -__exportStar(require("./interfaces"), exports); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/lib/src/index.js.map b/packages/lib/src/index.js.map deleted file mode 100644 index c86a7c8..0000000 --- a/packages/lib/src/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAQA,qCAAyC;AA2FvC,8FA3FO,sBAAa,OA2FP;AA1Ff,yCAA6C;AA2F3C,gGA3FO,0BAAe,OA2FP;AA1FjB,yCAA2C;AA2FzC,8FA3FO,wBAAa,OA2FP;AA1Ff,0DAA2D;AA2FzD,iGA3FO,+BAAgB,OA2FP;AAzFlB;;GAEG;AACH,MAAa,SAAS;IAKpB;;;;OAIG;IACH,YAAY,MAAmB,EAAE,MAAc;QAC7C,IAAI,CAAC,MAAM,GAAG,IAAI,sBAAa,EAAE,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,IAAI,0BAAe,EAAE,CAAC;QACtC,IAAI,CAAC,QAAQ,GAAG,IAAI,wBAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzC,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAW;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,SAAuB;QAC7B,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,gDAAgD;IAClD,CAAC;CACF;AA5DD,8BA4DC;AAED;;;GAGG;AACH,MAAa,cAAe,SAAQ,SAAS;IAC3C;;;OAGG;IACH,YAAY,MAAc;QACxB,KAAK,CAAC,IAAI,+BAAgB,EAAE,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;CACF;AARD,wCAQC;AAeD,uBAAuB;AACvB,+CAA6B"} \ No newline at end of file diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 5f349b7..38b659c 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -1,5 +1,13 @@ -import { SqlStatement, Command, SqlParser, SqlCompiler, CommandExecutor } from './interfaces'; -import { MongoClient } from 'mongodb'; +import { + SqlStatement, + Command, + SqlParser, + SqlCompiler, + CommandExecutor, + ExecutionResult, + CursorResult, +} from './interfaces'; +import { Document, MongoClient } from 'mongodb'; import { SqlParserImpl } from './parser'; import { SqlCompilerImpl } from './compiler'; import { MongoExecutor } from './executor'; @@ -25,16 +33,29 @@ export class QueryLeaf { } /** - * Execute a SQL query on MongoDB + * Execute a SQL query on MongoDB and return documents * @param sql SQL query string - * @returns Query results + * @returns Document results (no cursors) + * @typeParam T - The type of documents that will be returned (defaults to Document) */ - async execute(sql: string): Promise { + async execute(sql: string): Promise> { const statement = this.parse(sql); const commands = this.compile(statement); return await this.executor.execute(commands); } + /** + * Execute a SQL query on MongoDB and return a cursor + * @param sql SQL query string + * @returns Cursor for SELECT queries, null for other queries + * @typeParam T - The type of documents that will be returned (defaults to Document) + */ + async executeCursor(sql: string): Promise> { + const statement = this.parse(sql); + const commands = this.compile(statement); + return await this.executor.executeCursor(commands); + } + /** * Parse a SQL query string * @param sql SQL query string @@ -91,6 +112,8 @@ export { SqlParser, SqlCompiler, CommandExecutor, + ExecutionResult, + CursorResult, SqlParserImpl, SqlCompilerImpl, MongoExecutor, diff --git a/packages/lib/src/interfaces.d.ts b/packages/lib/src/interfaces.d.ts deleted file mode 100644 index 4b8632d..0000000 --- a/packages/lib/src/interfaces.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { AST } from 'node-sql-parser'; -/** - * Represents a parsed SQL statement - */ -export interface SqlStatement { - ast: AST; - text: string; -} -/** - * Command types supported by the MongoDB executor - */ -export type CommandType = 'FIND' | 'INSERT' | 'UPDATE' | 'DELETE' | 'AGGREGATE'; -/** - * Base interface for all MongoDB commands - */ -export interface BaseCommand { - type: CommandType; - collection: string; -} -/** - * Find command for MongoDB - */ -export interface FindCommand extends BaseCommand { - type: 'FIND'; - filter?: Record; - projection?: Record; - sort?: Record; - limit?: number; - skip?: number; - group?: { - _id: any; - [key: string]: any; - }; - pipeline?: Record[]; - lookup?: { - from: string; - localField: string; - foreignField: string; - as: string; - }[]; -} -/** - * Insert command for MongoDB - */ -export interface InsertCommand extends BaseCommand { - type: 'INSERT'; - documents: Record[]; -} -/** - * Update command for MongoDB - */ -export interface UpdateCommand extends BaseCommand { - type: 'UPDATE'; - filter?: Record; - update: Record; - upsert?: boolean; -} -/** - * Delete command for MongoDB - */ -export interface DeleteCommand extends BaseCommand { - type: 'DELETE'; - filter?: Record; -} -/** - * Aggregate command for MongoDB - */ -export interface AggregateCommand extends BaseCommand { - type: 'AGGREGATE'; - pipeline: Record[]; -} -/** - * Union type of all MongoDB commands - */ -export type Command = - | FindCommand - | InsertCommand - | UpdateCommand - | DeleteCommand - | AggregateCommand; -/** - * SQL parser interface - */ -export interface SqlParser { - parse(sql: string): SqlStatement; -} -/** - * SQL to MongoDB compiler interface - */ -export interface SqlCompiler { - compile(statement: SqlStatement): Command[]; -} -/** - * MongoDB command executor interface - */ -export interface CommandExecutor { - connect(): Promise; - close(): Promise; - execute(commands: Command[]): Promise; -} -/** - * Main QueryLeaf interface - */ -export interface QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; - getExecutor(): CommandExecutor; - close(): Promise; -} -export interface Squongo extends QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; - getExecutor(): CommandExecutor; - close(): Promise; -} diff --git a/packages/lib/src/interfaces.js b/packages/lib/src/interfaces.js deleted file mode 100644 index db91911..0000000 --- a/packages/lib/src/interfaces.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=interfaces.js.map \ No newline at end of file diff --git a/packages/lib/src/interfaces.js.map b/packages/lib/src/interfaces.js.map deleted file mode 100644 index 0fc31ba..0000000 --- a/packages/lib/src/interfaces.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["interfaces.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/lib/src/interfaces.ts b/packages/lib/src/interfaces.ts index 481777d..5579168 100644 --- a/packages/lib/src/interfaces.ts +++ b/packages/lib/src/interfaces.ts @@ -1,4 +1,5 @@ import { AST } from 'node-sql-parser'; +import { Document, FindCursor, AggregationCursor } from 'mongodb'; /** * Represents a parsed SQL statement @@ -105,20 +106,61 @@ export interface SqlCompiler { compile(statement: SqlStatement): Command[]; } +/** + * Represents result types that can be returned by the executor + */ +export type ExecutionResult = + | Document[] // Array of documents (default for FIND and AGGREGATE) + | Document // Single document or operation result (for INSERT, UPDATE, DELETE) + | null; // No result + +/** + * Represents cursor types that can be returned by the cursor executor + */ +export type CursorResult = + | FindCursor // Cursor from FIND command + | AggregationCursor // Cursor from AGGREGATE command + | null; // No result + /** * MongoDB command executor interface */ export interface CommandExecutor { connect(): Promise; close(): Promise; - execute(commands: Command[]): Promise; + /** + * Execute MongoDB commands and return documents + * @param commands Array of commands to execute + * @returns Document results (no cursors) + */ + execute(commands: Command[]): Promise>; + + /** + * Execute MongoDB commands and return cursors for FIND and AGGREGATE commands + * @param commands Array of commands to execute + * @returns Cursor for FIND and AGGREGATE commands, null for other commands + */ + executeCursor(commands: Command[]): Promise>; } /** * Main QueryLeaf interface */ export interface QueryLeaf { - execute(sql: string): Promise; + /** + * Execute a SQL query and return documents + * @param sql SQL query string + * @returns Document results (no cursors) + */ + execute(sql: string): Promise>; + + /** + * Execute a SQL query and return a cursor for SELECT queries + * @param sql SQL query string + * @returns Cursor for SELECT queries, null for other queries + */ + executeCursor(sql: string): Promise>; + parse(sql: string): SqlStatement; compile(statement: SqlStatement): Command[]; getExecutor(): CommandExecutor; @@ -126,7 +168,8 @@ export interface QueryLeaf { } export interface Squongo extends QueryLeaf { - execute(sql: string): Promise; + execute(sql: string): Promise>; + executeCursor(sql: string): Promise>; parse(sql: string): SqlStatement; compile(statement: SqlStatement): Command[]; getExecutor(): CommandExecutor; diff --git a/packages/lib/src/parser.d.ts b/packages/lib/src/parser.d.ts deleted file mode 100644 index 53eddeb..0000000 --- a/packages/lib/src/parser.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { SqlParser, SqlStatement } from './interfaces'; -/** - * SQL Parser implementation using node-sql-parser - */ -export declare class SqlParserImpl implements SqlParser { - private parser; - constructor(options?: { database?: string }); - /** - * Parse SQL string into a SqlStatement - * @param sql SQL string to parse - * @returns Parsed SQL statement object - */ - parse(sql: string): SqlStatement; - /** - * Preprocess nested field access in SQL before parsing - * - * This helps ensure that the parser correctly handles nested fields like: - * contact.address.city => becomes a properly parsed reference - * - * For deep nested fields (with more than one dot), we need special handling - * since the SQL parser typically expects table.column format only - */ - private preprocessNestedFields; - private _nestedFieldReplacements; - /** - * Post-process the AST to correctly handle nested fields - * - * This ensures that expressions like "contact.address.city" are correctly - * recognized as a single column reference rather than a table/column pair. - */ - private postProcessAst; - /** - * Process nested fields in the SELECT clause - */ - private processSelectClause; - /** - * Process nested fields in the WHERE clause - */ - private processWhereClause; - /** - * Process WHERE expression recursively to handle nested fields - */ - private processWhereExpr; - /** - * Check if a name is an actual table reference in the FROM clause - * - * This helps distinguish between table.column notation and nested field access - */ - private isActualTableReference; - /** - * Preprocess SQL to transform array index notation into a form the parser can handle - * - * This transforms: - * items[0].name => items__ARRAY_0__name - * - * We'll convert it back to MongoDB's dot notation later in the compiler. - */ - private preprocessArrayIndexes; - /** - * More aggressive preprocessing for SQL that contains array syntax - * This completely removes the array indexing and replaces it with a special column naming pattern - */ - private aggressivePreprocessing; -} diff --git a/packages/lib/src/parser.js b/packages/lib/src/parser.js deleted file mode 100644 index 215f64f..0000000 --- a/packages/lib/src/parser.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SqlParserImpl = void 0; -const node_sql_parser_1 = require("node-sql-parser"); -const debug_1 = __importDefault(require("debug")); -const log = (0, debug_1.default)('queryleaf:parser'); -// Custom PostgreSQL mode with extensions to support our syntax needs -const CUSTOM_DIALECT = { - name: 'QueryLeafPostgreSQL', - reserved: [ - 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', - 'TABLE', 'DATABASE', 'VIEW', 'INDEX', 'TRIGGER', 'PROCEDURE', 'FUNCTION' - ], - literalTokens: { - // Add handling for array indexing syntax - '[': { tokenType: 'BRACKET_OPEN', regex: /\[/ }, - ']': { tokenType: 'BRACKET_CLOSE', regex: /\]/ }, - '.': { tokenType: 'DOT', regex: /\./ } - }, - operators: [ - // Standard operators - '+', '-', '*', '/', '%', '=', '!=', '<>', '>', '<', '>=', '<=', - // Add nested field operators - '.' - ] -}; -/** - * SQL Parser implementation using node-sql-parser - */ -class SqlParserImpl { - constructor(options) { - // Store replacements for later reference - this._nestedFieldReplacements = []; - // Create standard parser with PostgreSQL mode - this.parser = new node_sql_parser_1.Parser(); - } - /** - * Parse SQL string into a SqlStatement - * @param sql SQL string to parse - * @returns Parsed SQL statement object - */ - parse(sql) { - try { - // First, handle nested dot notation in field access - const preprocessedNestedSql = this.preprocessNestedFields(sql); - // Then transform array index notation to a form the parser can handle - const preprocessedSql = this.preprocessArrayIndexes(preprocessedNestedSql); - log('Preprocessed SQL:', preprocessedSql); - // Parse with PostgreSQL mode but try to handle our custom extensions - const ast = this.parser.astify(preprocessedSql, { - database: 'PostgreSQL' - }); - // Process the AST to properly handle nested fields - const processedAst = this.postProcessAst(ast); - return { - ast: Array.isArray(processedAst) ? processedAst[0] : processedAst, - text: sql // Use original SQL for reference - }; - } - catch (error) { - // If error happens and it's related to our extensions, try to handle it - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('[')) { - // Make a more aggressive transformation of the SQL for bracket syntax - const fallbackSql = this.aggressivePreprocessing(sql); - log('Fallback SQL for array syntax:', fallbackSql); - try { - const ast = this.parser.astify(fallbackSql, { database: 'PostgreSQL' }); - const processedAst = this.postProcessAst(ast); - return { - ast: Array.isArray(processedAst) ? processedAst[0] : processedAst, - text: sql - }; - } - catch (fallbackErr) { - const fallbackErrorMsg = fallbackErr instanceof Error ? - fallbackErr.message : String(fallbackErr); - throw new Error(`SQL parsing error (fallback): ${fallbackErrorMsg}`); - } - } - throw new Error(`SQL parsing error: ${errorMessage}`); - } - } - /** - * Preprocess nested field access in SQL before parsing - * - * This helps ensure that the parser correctly handles nested fields like: - * contact.address.city => becomes a properly parsed reference - * - * For deep nested fields (with more than one dot), we need special handling - * since the SQL parser typically expects table.column format only - */ - preprocessNestedFields(sql) { - log('Processing nested fields in SQL:', sql); - // Find deeply nested fields in the WHERE clause (contact.address.city) - // and replace them with a placeholder format that the parser can handle - // This regex matches multi-level nested fields in WHERE conditions - // It looks for patterns like: WHERE contact.address.city = 'Boston' - const whereNestedFieldRegex = /WHERE\s+([a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+){1,})\s*(=|!=|<>|>|<|>=|<=|LIKE|IN|NOT IN)/gi; - // Keep track of replacements to restore them later - const replacements = []; - // First pass: replace deep nested fields in WHERE clause with placeholders - let processedSql = sql.replace(whereNestedFieldRegex, (match, nestedField, _, operator) => { - // Create a placeholder name - const placeholder = `__NESTED_${replacements.length}__`; - // Store the replacement - replacements.push([placeholder, nestedField]); - // Replace with the placeholder - return `WHERE ${placeholder} ${operator}`; - }); - // Add debug info about replacements - if (replacements.length > 0) { - log('Nested field replacements:', JSON.stringify(replacements, null, 2)); - } - // Store the replacements in this instance for later use - this._nestedFieldReplacements = replacements; - return processedSql; - } - /** - * Post-process the AST to correctly handle nested fields - * - * This ensures that expressions like "contact.address.city" are correctly - * recognized as a single column reference rather than a table/column pair. - */ - postProcessAst(ast) { - // Clone the AST to avoid modifying the original - const processed = JSON.parse(JSON.stringify(ast)); - // Handle SELECT clause nested fields - this.processSelectClause(processed); - // Handle WHERE clause nested fields - this.processWhereClause(processed); - log('Post-processed AST:', JSON.stringify(processed, null, 2)); - return processed; - } - /** - * Process nested fields in the SELECT clause - */ - processSelectClause(ast) { - if (!ast || (!Array.isArray(ast) && typeof ast !== 'object')) - return; - // Handle array of statements - if (Array.isArray(ast)) { - ast.forEach(item => this.processSelectClause(item)); - return; - } - // Only process SELECT statements - if (ast.type !== 'select' || !ast.columns) - return; - // Process each column in the SELECT list - ast.columns.forEach((column) => { - if (column.expr && column.expr.type === 'column_ref') { - // If the column has table.field notation, check if it should be a nested field - if (column.expr.table && column.expr.column && - !this.isActualTableReference(column.expr.table, ast)) { - // It's likely a nested field, not a table reference - column.expr.column = `${column.expr.table}.${column.expr.column}`; - column.expr.table = null; - } - } - }); - } - /** - * Process nested fields in the WHERE clause - */ - processWhereClause(ast) { - if (!ast || (!Array.isArray(ast) && typeof ast !== 'object')) - return; - // Handle array of statements - if (Array.isArray(ast)) { - ast.forEach(item => this.processWhereClause(item)); - return; - } - // No WHERE clause to process - if (!ast.where) - return; - // Process the WHERE clause recursively - this.processWhereExpr(ast.where, ast); - } - /** - * Process WHERE expression recursively to handle nested fields - */ - processWhereExpr(expr, ast) { - if (!expr || typeof expr !== 'object') - return; - if (expr.type === 'binary_expr') { - // Process both sides of binary expressions - this.processWhereExpr(expr.left, ast); - this.processWhereExpr(expr.right, ast); - // Check for column references in the left side of the expression - if (expr.left && expr.left.type === 'column_ref') { - // First, check if this is a placeholder that needs to be restored - if (expr.left.column && expr.left.column.startsWith('__NESTED_') && - expr.left.column.endsWith('__')) { - // Find the corresponding replacement - const placeholderIndex = parseInt(expr.left.column.replace('__NESTED_', '').replace('__', '')); - if (this._nestedFieldReplacements.length > placeholderIndex) { - // Restore the original nested field name - const [_, originalField] = this._nestedFieldReplacements[placeholderIndex]; - log(`Restoring nested field: ${expr.left.column} -> ${originalField}`); - expr.left.column = originalField; - expr.left.table = null; - } - } - // Then check for table.column notation that should be a nested field - else if (expr.left.table && expr.left.column && - !this.isActualTableReference(expr.left.table, ast)) { - // Likely a nested field access, not a table reference - expr.left.column = `${expr.left.table}.${expr.left.column}`; - expr.left.table = null; - } - } - } - else if (expr.type === 'unary_expr') { - // Process the expression in unary operators - this.processWhereExpr(expr.expr, ast); - } - } - /** - * Check if a name is an actual table reference in the FROM clause - * - * This helps distinguish between table.column notation and nested field access - */ - isActualTableReference(name, ast) { - if (!ast.from || !Array.isArray(ast.from)) - return false; - // Check if the name appears as a table name or alias in the FROM clause - return ast.from.some((fromItem) => { - return (fromItem.table === name) || (fromItem.as === name); - }); - } - /** - * Preprocess SQL to transform array index notation into a form the parser can handle - * - * This transforms: - * items[0].name => items__ARRAY_0__name - * - * We'll convert it back to MongoDB's dot notation later in the compiler. - */ - preprocessArrayIndexes(sql) { - // Replace array index notation with a placeholder format - // This regex matches field references with array indexes like items[0] or items[0].name - return sql.replace(/(\w+)\[(\d+)\](\.\w+)?/g, (match, field, index, suffix) => { - if (suffix) { - // For nested access like items[0].name => items__ARRAY_0__name - return `${field}__ARRAY_${index}__${suffix.substring(1)}`; - } - else { - // For simple array access like items[0] => items__ARRAY_0 - return `${field}__ARRAY_${index}`; - } - }); - } - /** - * More aggressive preprocessing for SQL that contains array syntax - * This completely removes the array indexing and replaces it with a special column naming pattern - */ - aggressivePreprocessing(sql) { - // Replace items[0].name with items_0_name - // This is a more aggressive approach that completely avoids bracket syntax - return sql.replace(/(\w+)\[(\d+)\](\.(\w+))?/g, (match, field, index, dotPart, subfield) => { - if (subfield) { - return `${field}_${index}_${subfield}`; - } - else { - return `${field}_${index}`; - } - }); - } -} -exports.SqlParserImpl = SqlParserImpl; -//# sourceMappingURL=parser.js.map \ No newline at end of file diff --git a/packages/lib/src/parser.js.map b/packages/lib/src/parser.js.map deleted file mode 100644 index 9b8ba9d..0000000 --- a/packages/lib/src/parser.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"parser.js","sourceRoot":"","sources":["parser.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/lib/tests/integration/array-access.integration.test.ts b/packages/lib/tests/integration/array-access.integration.test.ts index 7c24883..655940b 100644 --- a/packages/lib/tests/integration/array-access.integration.test.ts +++ b/packages/lib/tests/integration/array-access.integration.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; +import { testSetup, createLogger, ensureArray } from './test-setup'; const log = createLogger('array-access'); @@ -45,7 +45,7 @@ describe('Array Access Integration Tests', () => { WHERE items__ARRAY_0__name = 'Widget' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Array access filter results:', JSON.stringify(results, null, 2)); // Assert: Verify that filtering by array element works @@ -93,7 +93,7 @@ describe('Array Access Integration Tests', () => { WHERE items__ARRAY_0__name = 'Widget' AND items__ARRAY_1__inStock = true `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Array indices filtering results:', JSON.stringify(results, null, 2)); // Assert: Verify only the order with Widget as first item and inStock=true for second item @@ -136,7 +136,7 @@ describe('Array Access Integration Tests', () => { WHERE orderId = 'ORD-2001' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Order items query results:', JSON.stringify(results, null, 2)); // Basic validation @@ -187,7 +187,7 @@ describe('Array Access Integration Tests', () => { WHERE items__ARRAY_1__category = 'Electronics' `; - const indexResults = await queryLeaf.execute(indexAccessSql); + const indexResults = ensureArray(await queryLeaf.execute(indexAccessSql)); log('Array index access results:', JSON.stringify(indexResults, null, 2)); // Verify we can find orders by array index properties @@ -198,4 +198,4 @@ describe('Array Access Integration Tests', () => { expect(orderIds).toContain('ORD-2001'); expect(orderIds).toContain('ORD-2002'); }); -}); \ No newline at end of file +}); diff --git a/packages/lib/tests/integration/cursor.integration.test.ts b/packages/lib/tests/integration/cursor.integration.test.ts new file mode 100644 index 0000000..9f5d0c8 --- /dev/null +++ b/packages/lib/tests/integration/cursor.integration.test.ts @@ -0,0 +1,338 @@ +import { testSetup, createLogger } from './test-setup'; +import { Document, FindCursor, AggregationCursor } from 'mongodb'; + +const log = createLogger('cursor'); + +describe('MongoDB Cursor Functionality Tests', () => { + beforeAll(async () => { + await testSetup.init(); + }, 30000); // 30 second timeout for container startup + + afterAll(async () => { + // Make sure to close any outstanding connections + const queryLeaf = testSetup.getQueryLeaf(); + + // Clean up any resources that queryLeaf might be using + if (typeof queryLeaf.close === 'function') { + await queryLeaf.close(); + } + + // Clean up test setup resources + await testSetup.cleanup(); + }, 10000); // Give it more time to clean up + + beforeEach(async () => { + // Clean up and populate test collections before each test + const db = testSetup.getDb(); + await db.collection('test_cursor').deleteMany({}); + + // Generate 30 test products (more than the default MongoDB batch size of 20) + // to properly test cursor batching + const testProducts: any[] = []; + const categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports']; + + for (let i = 1; i <= 30; i++) { + const categoryIndex = i % categories.length; + const price = 10 + (i * 5); // Different prices for variety + + testProducts.push({ + name: `Product ${i}`, + category: categories[categoryIndex], + price: price, + sku: `SKU-${i.toString().padStart(4, '0')}`, + inStock: i % 3 === 0 ? false : true + }); + } + + // Insert test data + await db.collection('test_cursor').insertMany(testProducts); + }); + + afterEach(async () => { + // Clean up test collections after each test + const db = testSetup.getDb(); + await db.collection('test_cursor').deleteMany({}); + }); + + test('should return an array by default', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const result = await queryLeaf.execute('SELECT * FROM test_cursor'); + + // Type checking - we expect this to be a Document[] (not a cursor) + expect(Array.isArray(result)).toBe(true); + + // With the type assertion, TypeScript knows it's an array + if (Array.isArray(result)) { + const results = result; + expect(results.length).toBe(30); + // Check for expected array methods + expect(typeof results.map).toBe('function'); + expect(typeof results.forEach).toBe('function'); + expect(typeof results.filter).toBe('function'); + } + }); + + test('should return a cursor when returnCursor is true', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor'); + + // Assert + expect(cursor).not.toBeNull(); + + if (cursor) { + expect(typeof cursor.toArray).toBe('function'); + expect(typeof cursor.forEach).toBe('function'); + expect(typeof cursor.next).toBe('function'); + expect(typeof cursor.hasNext).toBe('function'); + + // Clean up + await cursor.close(); + } + }); + + test('should be able to iterate through cursor with forEach', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor'); + + expect(cursor).not.toBeNull(); + + const results: any[] = []; + await cursor?.forEach((doc: any) => { + results.push(doc); + }); + + // Assert + expect(results.length).toBe(30); + expect(results[0].name).toBeDefined(); + + // Clean up + await cursor?.close(); + }); + + test('should be able to get all results with toArray from cursor', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor'); + expect(cursor).not.toBeNull(); + const results = await cursor?.toArray(); + + // Assert + expect(Array.isArray(results)).toBe(true); + expect(results?.length).toBe(30); + + // Clean up + await cursor?.close(); + }); + + test('should be able to use next and hasNext with cursor', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor'); + + // Assert + expect(cursor).not.toBeNull(); + // Check if we have documents + expect(await cursor?.hasNext()).toBe(true); + + // Get the first document + const firstDoc = await cursor?.next(); + expect(firstDoc).toBeDefined(); + expect(firstDoc?.name).toBeDefined(); + + // Get remaining documents + const docs: any[] = []; + while (await cursor?.hasNext()) { + const doc = await cursor?.next(); + docs.push(doc); + } + + // We should have 29 remaining documents + expect(docs.length).toBe(29); + + // Clean up + await cursor?.close(); + }); + + test('should work with returnCursor for filtered queries', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor("SELECT * FROM test_cursor WHERE category = 'Electronics'"); + + expect(cursor).not.toBeNull(); + + const results = await cursor?.toArray(); + + // Assert + // With our test data generation logic (5 categories), we expect 6 Electronics products + const expectedElectronicsCount = Math.ceil(30 / 5); // 30 products / 5 categories + expect(results?.length).toBe(expectedElectronicsCount); + // Check that all results are from Electronics category + results?.forEach(product => { + expect(product.category).toBe('Electronics'); + }); + + // Clean up + await cursor?.close(); + }); + + test('should work with returnCursor for sorted queries', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor ORDER BY price DESC'); + + expect(cursor).not.toBeNull() + + const results = await cursor?.toArray(); + + if (!results) throw new Error('failed to get array from cursor'); + + // Assert + expect(results.length).toBe(30); + // Check if sorted in descending order + // With our generation logic: prices = 10 + (i * 5), highest should be for i=30 + expect(results[0].price).toBe(160); // Product 30: 10 + (30 * 5) = 160 + expect(results[1].price).toBe(155); // Product 29: 10 + (29 * 5) = 155 + + // Clean up + await cursor?.close(); + }); + + test('should work with returnCursor for aggregation queries', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + const db = testSetup.getDb(); + + // First, check with a direct MongoDB query how the aggregation works + const directAggregateResult = await db.collection('test_cursor').aggregate([ + { $group: { _id: "$category", count: { $sum: 1 } } } + ]).toArray(); + + log('Direct MongoDB aggregate result:', JSON.stringify(directAggregateResult, null, 2)); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT category, COUNT(*) as count FROM test_cursor GROUP BY category'); + + // Get the results and log them + const results = await cursor?.toArray(); + if (!results) throw new Error('failed to get array from cursor'); + log('GROUP BY cursor results:', JSON.stringify(results, null, 2)); + + // Assert + // We should have some results + expect(results.length).toBeGreaterThan(0); + + // In a MongoDB aggregation with $group, the group key is always in _id + // We expect 5 distinct categories in our test data + const uniqueCategories = new Set(); + + results.forEach(r => { + // With GROUP BY, MongoDB puts the grouping key in _id + if (r._id) { + uniqueCategories.add(r._id); + } + }); + + // We expect at least one unique value which should be in _id + expect(uniqueCategories.size).toBeGreaterThan(0); + + // Clean up + await cursor?.close(); + }); + + test('should respect cursor limit and skip options', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act - Use LIMIT and OFFSET in SQL + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor LIMIT 2 OFFSET 1'); + + const results = await cursor?.toArray(); + if (!results) throw new Error('failed to get array from cursor'); + + // Assert + expect(results.length).toBe(2); // Limited to 2 records + + // Clean up + await cursor?.close(); + }); + + test('should properly close the cursor', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor'); + + // Get some results first to ensure the cursor is actually used + expect(await cursor?.hasNext()).toBe(true); + + // Now close the cursor + await cursor?.close(); + + // Assert - at this point, we can't really test if the cursor is closed + // in a reliable way without accessing MongoDB internals + // The best we can do is ensure no error was thrown when closing + }); + + test('should handle cursor batching properly with next/hasNext', async () => { + // Arrange + const queryLeaf = testSetup.getQueryLeaf(); + + // Act + const cursor = await queryLeaf.executeCursor('SELECT * FROM test_cursor ORDER BY price ASC'); + + // Manual iteration to test batching behavior + // Default MongoDB batch size is 20, so we need to iterate beyond that + + // First, get a batch of items iteratively + const firstBatchItems: any[] = []; + for (let i = 0; i < 25; i++) { + expect(await cursor?.hasNext()).toBe(true); // Should still have more + const item = await cursor?.next(); + expect(item).toBeDefined(); + firstBatchItems.push(item); + } + + // Verify we got 25 items (which crosses the default batch boundary of 20) + expect(firstBatchItems.length).toBe(25); + + // Verify we still have more items + expect(await cursor?.hasNext()).toBe(true); + + // Get the remaining items + const remainingItems: any[] = []; + while (await cursor?.hasNext()) { + const item = await cursor?.next(); + remainingItems.push(item); + } + + // Verify we got all 30 items (25 + 5 more) + expect(remainingItems.length).toBe(5); + expect(firstBatchItems.length + remainingItems.length).toBe(30); + + // Verify items are in ascending price order as requested + for (let i = 1; i < firstBatchItems.length; i++) { + expect(firstBatchItems[i].price).toBeGreaterThanOrEqual(firstBatchItems[i-1].price); + } + + // Clean up + await cursor?.close(); + }); +}); diff --git a/packages/lib/tests/integration/edge-cases.integration.test.ts b/packages/lib/tests/integration/edge-cases.integration.test.ts index e2764d2..a817ee1 100644 --- a/packages/lib/tests/integration/edge-cases.integration.test.ts +++ b/packages/lib/tests/integration/edge-cases.integration.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; +import { testSetup, createLogger, ensureArray } from './test-setup'; const log = createLogger('edge-cases'); @@ -51,7 +51,7 @@ describe('Edge Cases Integration Tests', () => { // to be supported by most SQL parsers const sql = 'SELECT name, field_with_underscores FROM edge_test WHERE field_with_underscores = "value1"'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert expect(results).toHaveLength(1); @@ -104,7 +104,7 @@ describe('Edge Cases Integration Tests', () => { // Try to do numerical comparison on non-numeric data const sql = 'SELECT name FROM edge_test WHERE value > 100'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert - should only find the numeric value that's valid for comparison expect(results).toHaveLength(1); @@ -125,7 +125,7 @@ describe('Edge Cases Integration Tests', () => { // Use the string representation of ObjectId in SQL const sql = `SELECT name FROM edge_test WHERE _id = '${objectId.toString()}'`; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert expect(results).toHaveLength(1); @@ -147,7 +147,7 @@ describe('Edge Cases Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = 'SELECT * FROM edge_test'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert expect(results).toHaveLength(1000); @@ -155,4 +155,4 @@ describe('Edge Cases Integration Tests', () => { expect(results[0]).toHaveProperty('name'); expect(results[0]).toHaveProperty('value'); }); -}); \ No newline at end of file +}); diff --git a/packages/lib/tests/integration/group-by.integration.test.ts b/packages/lib/tests/integration/group-by.integration.test.ts index 320c687..8f0d9b8 100644 --- a/packages/lib/tests/integration/group-by.integration.test.ts +++ b/packages/lib/tests/integration/group-by.integration.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; +import { testSetup, createLogger, ensureArray, ensureDocument } from './test-setup'; const log = createLogger('group-by'); @@ -73,7 +73,7 @@ describe('GROUP BY Integration Tests', () => { GROUP BY category `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Simple GROUP BY results:', JSON.stringify(results, null, 2)); // Basic verification - check we have results @@ -196,7 +196,7 @@ describe('GROUP BY Integration Tests', () => { GROUP BY category, region, year `; - const multiResults = await multiQueryLeaf.execute(multiSql); + const multiResults = ensureArray(await multiQueryLeaf.execute(multiSql)); log('Multi-column GROUP BY results:', JSON.stringify(multiResults, null, 2)); // Create a more resilient verification that's simpler diff --git a/packages/lib/tests/integration/integration.integration.test.ts b/packages/lib/tests/integration/integration.integration.test.ts index 8741219..e3f604e 100644 --- a/packages/lib/tests/integration/integration.integration.test.ts +++ b/packages/lib/tests/integration/integration.integration.test.ts @@ -2,6 +2,7 @@ import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders } import { ObjectId } from 'mongodb'; import { QueryLeaf } from '../../src/index'; import { createLogger } from './test-setup'; +import { ensureArray, ensureDocument } from './test-setup'; const log = createLogger('integration'); @@ -38,7 +39,7 @@ describe('QueryLeaf Integration Tests', () => { const sql = 'SELECT * FROM users'; log('Executing SQL:', sql); - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Results:', JSON.stringify(results, null, 2)); expect(results).toHaveLength(testUsers.length); @@ -70,12 +71,12 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); // We need to first test if we can find the user at all const findSql = "SELECT * FROM users WHERE name = 'Nested User'"; - const findResults = await queryLeaf.execute(findSql); + const findResults = ensureArray(await queryLeaf.execute(findSql)); log('Find by name results:', JSON.stringify(findResults, null, 2)); // Now test the nested fields - use a format that works better with the MongoDB projection const sql = "SELECT name, address FROM users WHERE name = 'Nested User'"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Nested field results:', JSON.stringify(results, null, 2)); expect(results.length).toBeGreaterThan(0); @@ -90,7 +91,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = 'SELECT * FROM users WHERE age > 20'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); expect(results.length).toBeGreaterThan(0); expect(results.length).toBeLessThan(testUsers.length); @@ -101,7 +102,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = 'SELECT name, email FROM users'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // We have added a 'Nested User' in a previous test, so we'll have more than the original test users expect(results.length).toBeGreaterThanOrEqual(testUsers.length); @@ -116,7 +117,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = "SELECT * FROM products WHERE category = 'Electronics'"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); expect(results.length).toBeGreaterThan(0); expect(results.every((product: any) => product.category === 'Electronics')).toBe(true); @@ -126,7 +127,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = "SELECT * FROM users WHERE age >= 25 AND active = true"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); expect(results.length).toBeGreaterThan(0); expect(results.every((user: any) => user.age >= 25 && user.active === true)).toBe(true); @@ -150,7 +151,7 @@ describe('QueryLeaf Integration Tests', () => { // Just fetch the entire document const sql = "SELECT * FROM orders"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Log the results to see the structure log('Array access results:', JSON.stringify(results, null, 2)); @@ -196,7 +197,7 @@ describe('QueryLeaf Integration Tests', () => { // Test with a simplified query that doesn't rely on specific nested field or array syntax const simpleSql = "SELECT metadata, items FROM complex_data WHERE name = 'Complex Object'"; - const results = await queryLeaf.execute(simpleSql); + const results = ensureArray(await queryLeaf.execute(simpleSql)); log('Complex data query results:', JSON.stringify(results, null, 2)); // Verify we have a result @@ -241,7 +242,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = 'SELECT region FROM simple_stats GROUP BY region'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('GROUP BY results:', JSON.stringify(results, null, 2)); // We have 4 distinct regions, but due to the implementation change, @@ -278,12 +279,12 @@ describe('QueryLeaf Integration Tests', () => { // First query to get books const booksSql = `SELECT * FROM books`; - const booksResults = await queryLeaf.execute(booksSql); + const booksResults = ensureArray(await queryLeaf.execute(booksSql)); log('Books results:', JSON.stringify(booksResults, null, 2)); // Second query to get authors const authorsSql = `SELECT * FROM authors`; - const authorsResults = await queryLeaf.execute(authorsSql); + const authorsResults = ensureArray(await queryLeaf.execute(authorsSql)); log('Authors results:', JSON.stringify(authorsResults, null, 2)); // Check that we have the right number of books and authors @@ -347,7 +348,7 @@ describe('QueryLeaf Integration Tests', () => { // Now, using QueryLeaf to search for books more generally (to be safer) const simpleBooksSql = `SELECT * FROM books`; - const allBooksResults = await queryLeaf.execute(simpleBooksSql); + const allBooksResults = ensureArray(await queryLeaf.execute(simpleBooksSql)); log('All books query results:', JSON.stringify(allBooksResults, null, 2)); // As long as we get some books back, this demonstrates querying works @@ -369,7 +370,7 @@ describe('QueryLeaf Integration Tests', () => { const queryLeaf = getQueryLeaf(); const sql = 'SELECT * FROM products ORDER BY price DESC'; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); expect(results).toHaveLength(testProducts.length); @@ -386,7 +387,7 @@ describe('QueryLeaf Integration Tests', () => { const sql = 'SELECT * FROM users'; log('Executing SQL:', sql); - const allResults = await queryLeaf.execute(sql); + const allResults = ensureArray(await queryLeaf.execute(sql)); const limitedResults = allResults.slice(0, 2); // Manually limit to 2 results log('All results count:', allResults.length); @@ -403,13 +404,13 @@ describe('QueryLeaf Integration Tests', () => { // First get all users to know how many we have const allUsersSql = 'SELECT * FROM users'; - const allUsers = await queryLeaf.execute(allUsersSql); + const allUsers = ensureArray(await queryLeaf.execute(allUsersSql)); log('Total users:', allUsers.length); // Execute SQL with OFFSET const offsetSql = 'SELECT * FROM users OFFSET 2'; log('Executing SQL with OFFSET:', offsetSql); - const offsetResults = await queryLeaf.execute(offsetSql); + const offsetResults = ensureArray(await queryLeaf.execute(offsetSql)); // Verify we have the expected number of results expect(offsetResults.length).toBe(allUsers.length - 2); @@ -423,7 +424,7 @@ describe('QueryLeaf Integration Tests', () => { // First get all users ordered by name to have consistent results const allUsersSql = 'SELECT * FROM users ORDER BY name'; - const allUsers = await queryLeaf.execute(allUsersSql); + const allUsers = ensureArray(await queryLeaf.execute(allUsersSql)); log('Total ordered users:', allUsers.length); // Make sure we have enough users for this test @@ -432,7 +433,7 @@ describe('QueryLeaf Integration Tests', () => { // Execute SQL with LIMIT and OFFSET const paginatedSql = 'SELECT * FROM users ORDER BY name LIMIT 2 OFFSET 1'; log('Executing SQL with LIMIT and OFFSET:', paginatedSql); - const paginatedResults = await queryLeaf.execute(paginatedSql); + const paginatedResults = ensureArray(await queryLeaf.execute(paginatedSql)); // Verify we have the expected number of results expect(paginatedResults.length).toBe(2); @@ -450,13 +451,13 @@ describe('QueryLeaf Integration Tests', () => { const sql = `INSERT INTO users (_id, name, age, email, active) VALUES ('${newId.toString()}', 'New User', 28, 'new@example.com', true)`; - const result = await queryLeaf.execute(sql); + const result = ensureDocument(await queryLeaf.execute(sql)); expect(result.acknowledged).toBe(true); expect(result.insertedCount).toBe(1); // Verify the insertion with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM users WHERE _id = '${newId.toString()}'`); + const selectResult = ensureArray(await queryLeaf.execute(`SELECT * FROM users WHERE _id = '${newId.toString()}'`)); expect(selectResult).toHaveLength(1); expect(selectResult[0].name).toBe('New User'); }); @@ -468,13 +469,13 @@ describe('QueryLeaf Integration Tests', () => { const productId = testProducts[0]._id; const sql = `UPDATE products SET price = 1300 WHERE _id = '${productId.toString()}'`; - const result = await queryLeaf.execute(sql); - + const result = ensureDocument(await queryLeaf.execute(sql)); + expect(result.acknowledged).toBe(true); expect(result.modifiedCount).toBe(1); // Verify the update with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM products WHERE _id = '${productId.toString()}'`); + const selectResult = ensureArray(await queryLeaf.execute(`SELECT * FROM products WHERE _id = '${productId.toString()}'`)); expect(selectResult).toHaveLength(1); expect(selectResult[0].price).toBe(1300); }); @@ -486,14 +487,14 @@ describe('QueryLeaf Integration Tests', () => { const orderId = testOrders[4]._id; const sql = `DELETE FROM orders WHERE _id = '${orderId.toString()}'`; - const result = await queryLeaf.execute(sql); - + const result = ensureDocument(await queryLeaf.execute(sql)); + expect(result.acknowledged).toBe(true); expect(result.deletedCount).toBe(1); // Verify the deletion with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM orders WHERE _id = '${orderId.toString()}'`); + const selectResult = ensureArray(await queryLeaf.execute(`SELECT * FROM orders WHERE _id = '${orderId.toString()}'`)); expect(selectResult).toHaveLength(0); }); }); -}); \ No newline at end of file +}); diff --git a/packages/lib/tests/integration/main-features.integration.test.ts b/packages/lib/tests/integration/main-features.integration.test.ts index 4205755..8d16032 100644 --- a/packages/lib/tests/integration/main-features.integration.test.ts +++ b/packages/lib/tests/integration/main-features.integration.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; +import { testSetup, createLogger, ensureArray, ensureDocument } from './test-setup'; const log = createLogger('main-features'); @@ -52,7 +52,7 @@ describe('Main SQL Features Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = "SELECT name FROM simple_products WHERE details.color = 'black'"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert expect(results).toHaveLength(1); @@ -74,7 +74,7 @@ describe('Main SQL Features Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = "SELECT category, COUNT(*) as count FROM simple_products GROUP BY category"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert - verify we have at least the 3 groups (Electronics, Clothing, Books) // Due to implementation changes, the actual number of results might vary @@ -101,7 +101,7 @@ describe('Main SQL Features Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = "SELECT name FROM simple_products WHERE (category = 'Electronics' AND price < 1000) OR (category = 'Clothing' AND price < 50)"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert - we should have 3 rows matching our criteria: // - Smartphone (Electronics < 1000) @@ -131,7 +131,7 @@ describe('Main SQL Features Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = "SELECT name as product_name, category as product_type, price as list_price FROM simple_products"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('Column aliases results:', JSON.stringify(results, null, 2)); // Assert - check that the aliases are present in some form @@ -187,7 +187,7 @@ describe('Main SQL Features Integration Tests', () => { const queryLeaf = testSetup.getQueryLeaf(); const sql = "SELECT name FROM simple_products WHERE category IN ('Electronics')"; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); log('IN operator results:', JSON.stringify(results, null, 2)); // Assert - we should have Electronics products diff --git a/packages/lib/tests/integration/nested-fields.integration.test.ts b/packages/lib/tests/integration/nested-fields.integration.test.ts index d8f7912..495c34f 100644 --- a/packages/lib/tests/integration/nested-fields.integration.test.ts +++ b/packages/lib/tests/integration/nested-fields.integration.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { testSetup } from './test-setup'; +import { testSetup, ensureArray, ensureDocument } from './test-setup'; describe('Nested Fields Integration Tests', () => { beforeAll(async () => { @@ -59,7 +59,7 @@ describe('Nested Fields Integration Tests', () => { WHERE name = 'John Smith' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify we can access the data expect(results).toHaveLength(1); @@ -111,7 +111,7 @@ describe('Nested Fields Integration Tests', () => { WHERE contact.address.city = 'Boston' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify only Bostonians are returned expect(results).toHaveLength(2); @@ -163,7 +163,7 @@ describe('Nested Fields Integration Tests', () => { AND details.price < 1400 `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify only products matching nested criteria are returned expect(results).toHaveLength(2); @@ -225,7 +225,7 @@ describe('Nested Fields Integration Tests', () => { FROM products `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify results count and basic structure expect(results).toHaveLength(2); @@ -349,7 +349,7 @@ describe('Nested Fields Integration Tests', () => { WHERE name = 'Tablet' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify we can access the nested field data expect(results).toHaveLength(1); @@ -393,7 +393,7 @@ describe('Nested Fields Integration Tests', () => { WHERE name = 'Monitor' `; - const results = await queryLeaf.execute(sql); + const results = ensureArray(await queryLeaf.execute(sql)); // Assert: Verify we can access the nested field data at the top level expect(results).toHaveLength(1); diff --git a/packages/lib/tests/integration/test-setup.ts b/packages/lib/tests/integration/test-setup.ts index 1fdfc1f..829eb7d 100644 --- a/packages/lib/tests/integration/test-setup.ts +++ b/packages/lib/tests/integration/test-setup.ts @@ -1,6 +1,6 @@ import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders } from '../utils/mongo-container'; -import { QueryLeaf } from '../../src/index'; -import { Db } from 'mongodb'; +import { QueryLeaf, ExecutionResult } from '../../src/index'; +import { Db, Document } from 'mongodb'; import debug from 'debug'; /** @@ -65,4 +65,31 @@ export { testUsers, testProducts, testOrders }; /** * Create debug logger for tests */ -export const createLogger = (namespace: string) => debug(`queryleaf:test:${namespace}`); \ No newline at end of file +export const createLogger = (namespace: string) => debug(`queryleaf:test:${namespace}`); + +/** + * Helper function to handle type checking for results from queryLeaf.execute() + * This helps ensure proper type checking in tests for array results + * @param result The result from queryLeaf.execute() + * @returns The result as an array (throws if not an array) + */ +export function ensureArray(result: ExecutionResult): Array { + if (!Array.isArray(result)) { + throw new Error('Expected result to be an array, but got: ' + typeof result); + } + return result as Array; +} + +/** + * Helper function to handle type checking for results from queryLeaf.execute() for operation results + * @param result The result from queryLeaf.execute() + * @returns The result as a Document (throws if it's an array or cursor) + */ +export function ensureDocument(result: ExecutionResult): Document { + if (Array.isArray(result) || result === null) { + throw new Error('Expected result to be a document, but got: ' + + (Array.isArray(result) ? 'array' : + result === null ? 'null' : typeof result)); + } + return result; +} diff --git a/packages/postgres-server/src/protocol-handler.ts b/packages/postgres-server/src/protocol-handler.ts index e43c6e4..db75d03 100644 --- a/packages/postgres-server/src/protocol-handler.ts +++ b/packages/postgres-server/src/protocol-handler.ts @@ -2,7 +2,7 @@ import { Socket } from 'net'; import { QueryLeaf } from '@queryleaf/lib'; import { Transform } from 'stream'; import debugLib from 'debug'; -import { MongoClient } from 'mongodb'; +import { MongoClient, Document } from 'mongodb'; const debug = debugLib('queryleaf:pg-server:protocol'); @@ -849,10 +849,18 @@ export class ProtocolHandler { const count = Array.isArray(result) ? result.length : 1; commandTag = `INSERT 0 ${count}`; } else if (queryString.trim().toUpperCase().startsWith('UPDATE')) { - const count = result?.modifiedCount || 0; + // For UPDATE commands, result should be a Document with modifiedCount + const count = + typeof result === 'object' && result !== null && 'modifiedCount' in result + ? (result as Document).modifiedCount || 0 + : 0; commandTag = `UPDATE ${count}`; } else if (queryString.trim().toUpperCase().startsWith('DELETE')) { - const count = result?.deletedCount || 0; + // For DELETE commands, result should be a Document with deletedCount + const count = + typeof result === 'object' && result !== null && 'deletedCount' in result + ? (result as Document).deletedCount || 0 + : 0; commandTag = `DELETE ${count}`; } diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 0da4704..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env node - -import { MongoClient } from 'mongodb'; -import { QueryLeaf } from './index'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import readline from 'readline'; -import fs from 'fs'; -import path from 'path'; -import chalk from 'chalk'; - -// Parse command line arguments -const argv = yargs(hideBin(process.argv)) - .usage('Usage: $0 [options]') - .option('uri', { - describe: 'MongoDB connection URI', - default: 'mongodb://localhost:27017', - type: 'string', - }) - .option('db', { - describe: 'MongoDB database name', - demandOption: true, - type: 'string', - }) - .option('file', { - describe: 'SQL file to execute', - type: 'string', - }) - .option('query', { - alias: 'q', - describe: 'SQL query to execute', - type: 'string', - }) - .option('json', { - describe: 'Output results as JSON', - default: false, - type: 'boolean', - }) - .option('pretty', { - describe: 'Pretty-print JSON output', - default: true, - type: 'boolean', - }) - .option('interactive', { - alias: 'i', - describe: 'Run in interactive mode', - default: false, - type: 'boolean', - }) - .example('$0 --db mydb --query "SELECT * FROM users LIMIT 5"', 'Execute a single query') - .example('$0 --db mydb --file queries.sql', 'Execute queries from a file') - .example('$0 --db mydb --interactive', 'Run in interactive mode') - .epilog('For more information, visit https://github.com/beekeeper-studio/queryleaf') - .help() - .alias('help', 'h') - .version() - .alias('version', 'v') - .parseSync(); - -// Helper function to display query results -function displayResults(results: any, isJson: boolean, isPretty: boolean) { - if (isJson) { - if (isPretty) { - console.log(JSON.stringify(results, null, 2)); - } else { - console.log(JSON.stringify(results)); - } - } else { - // Simple table display for arrays of objects - if (Array.isArray(results) && results.length > 0 && typeof results[0] === 'object') { - // Get all unique keys from all objects - const keys = new Set(); - results.forEach((item) => { - Object.keys(item).forEach((key) => keys.add(key)); - }); - - const headers = Array.from(keys); - - // Calculate column widths (min 10, max 40) - const columnWidths = headers.map((header) => { - const values = results.map((item) => - item[header] !== undefined ? String(item[header]) : '' - ); - - const maxWidth = Math.max( - header.length, - ...values.map((v) => v.length) - ); - - return Math.min(40, Math.max(10, maxWidth)); - }); - - // Print headers - console.log( - headers - .map((header, i) => chalk.bold(header.padEnd(columnWidths[i]))) - .join(' | ') - ); - - // Print separator - console.log( - headers - .map((_, i) => '-'.repeat(columnWidths[i])) - .join('-+-') - ); - - // Print rows - results.forEach((row) => { - console.log( - headers - .map((header, i) => { - const value = row[header] !== undefined ? String(row[header]) : ''; - return value.padEnd(columnWidths[i]); - }) - .join(' | ') - ); - }); - } else { - // For non-array results or empty arrays - console.log(results); - } - } - - // Print record count for arrays - if (Array.isArray(results)) { - console.log(chalk.cyan(`\n${results.length} record(s) returned`)); - } -} - -// Execute a single SQL query -async function executeQuery(queryLeaf: QueryLeaf, sql: string, isJson: boolean, isPretty: boolean) { - try { - console.log(chalk.green(`Executing: ${sql}`)); - const startTime = Date.now(); - const results = await queryLeaf.execute(sql); - const duration = Date.now() - startTime; - - displayResults(results, isJson, isPretty); - console.log(chalk.gray(`\nExecution time: ${duration}ms`)); - return true; - } catch (error) { - console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); - return false; - } -} - -// Main function -async function main() { - const mongoClient = new MongoClient(argv.uri as string); - - try { - console.log(chalk.blue(`Connecting to MongoDB: ${argv.uri}`)); - await mongoClient.connect(); - console.log(chalk.green(`Connected to MongoDB, using database: ${argv.db}`)); - - const queryLeaf = new QueryLeaf(mongoClient, argv.db as string); - - // Execute from file - if (argv.file) { - const filePath = path.resolve(process.cwd(), argv.file as string); - if (!fs.existsSync(filePath)) { - console.error(chalk.red(`File not found: ${filePath}`)); - process.exit(1); - } - - console.log(chalk.blue(`Executing SQL from file: ${filePath}`)); - const sqlContent = fs.readFileSync(filePath, 'utf-8'); - - // Split file content by semicolons to get individual queries - // Ignore semicolons inside quotes - const queries = sqlContent - .match(/(?:[^;"']+|"(?:\\"|[^"])*"|'(?:\\'|[^'])*')+/g) - ?.map(q => q.trim()) - .filter(q => q.length > 0) || []; - - if (queries.length === 0) { - console.log(chalk.yellow('No queries found in file.')); - process.exit(0); - } - - console.log(chalk.blue(`Found ${queries.length} queries in file.`)); - - for (let i = 0; i < queries.length; i++) { - const query = queries[i]; - console.log(chalk.blue(`\nExecuting query ${i + 1}/${queries.length}:`)); - await executeQuery(queryLeaf, query, argv.json as boolean, argv.pretty as boolean); - } - } - // Execute single query - else if (argv.query) { - await executeQuery(queryLeaf, argv.query as string, argv.json as boolean, argv.pretty as boolean); - } - // Interactive mode - else if (argv.interactive) { - console.log(chalk.blue('Starting interactive SQL shell. Type .help for commands, .exit to quit.')); - console.log(chalk.blue('Connected to database: ' + argv.db)); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: 'sql> ', - terminal: true, - }); - - rl.prompt(); - - let multilineQuery = ''; - - rl.on('line', async (line) => { - const trimmedLine = line.trim(); - - // Handle special commands - if (trimmedLine === '.exit' || trimmedLine === '.quit') { - rl.close(); - return; - } - - if (trimmedLine === '.help') { - console.log(chalk.blue('Available commands:')); - console.log(' .help - Show this help message'); - console.log(' .tables - List all collections in the database'); - console.log(' .exit - Exit the shell'); - console.log(' .quit - Exit the shell'); - console.log(' .clear - Clear the current query buffer'); - console.log(' .json - Toggle JSON output mode (currently ' + (argv.json ? 'ON' : 'OFF') + ')'); - console.log('\nSQL queries can span multiple lines. End with a semicolon to execute.'); - rl.prompt(); - return; - } - - if (trimmedLine === '.tables') { - try { - const collections = await mongoClient.db(argv.db as string).listCollections().toArray(); - console.log(chalk.blue('Collections in database:')); - collections.forEach(collection => { - console.log(` ${collection.name}`); - }); - } catch (error) { - console.error(chalk.red(`Error listing collections: ${error instanceof Error ? error.message : String(error)}`)); - } - rl.prompt(); - return; - } - - if (trimmedLine === '.clear') { - multilineQuery = ''; - console.log(chalk.yellow('Query buffer cleared.')); - rl.prompt(); - return; - } - - if (trimmedLine === '.json') { - argv.json = !argv.json; - console.log(chalk.blue(`JSON output mode: ${argv.json ? 'ON' : 'OFF'}`)); - rl.prompt(); - return; - } - - // Handle SQL query - multilineQuery += line + ' '; - - // Execute on semicolon - if (trimmedLine.endsWith(';')) { - const query = multilineQuery.trim(); - multilineQuery = ''; - - if (query.length > 1) { // Handle empty queries (just ";") - await executeQuery(queryLeaf, query, argv.json as boolean, argv.pretty as boolean); - } - } else { - // Show continuation prompt for multiline - process.stdout.write('... '); - return; - } - - rl.prompt(); - }); - - rl.on('close', () => { - console.log(chalk.blue('\nGoodbye!')); - process.exit(0); - }); - - return; // Keep process running for interactive mode - } - else { - console.log(chalk.yellow('No query or file specified and not in interactive mode.')); - console.log(chalk.yellow('Use --help to see available options.')); - } - } catch (error) { - console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); - process.exit(1); - } finally { - if (!argv.interactive) { - await mongoClient.close(); - console.log(chalk.blue('MongoDB connection closed.')); - } - } -} - -// Run the main function -if (require.main === module) { - main().catch(error => { - console.error(chalk.red(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`)); - process.exit(1); - }); -} \ No newline at end of file diff --git a/src/compiler.ts b/src/compiler.ts deleted file mode 100644 index f5f93e9..0000000 --- a/src/compiler.ts +++ /dev/null @@ -1,944 +0,0 @@ -import { - SqlCompiler, - SqlStatement, - Command, - FindCommand, - InsertCommand, - UpdateCommand, - DeleteCommand -} from './interfaces'; -import { From } from 'node-sql-parser'; -import debug from 'debug'; - -const log = debug('queryleaf:compiler'); - -/** - * SQL to MongoDB compiler implementation - */ -export class SqlCompilerImpl implements SqlCompiler { - /** - * Compile a SQL statement into MongoDB commands - * @param statement SQL statement to compile - * @returns Array of MongoDB commands - */ - compile(statement: SqlStatement): Command[] { - const ast = statement.ast; - - log('Compiling SQL AST:', JSON.stringify(ast, null, 2)); - - // Pre-process the AST to handle nested fields that might be parsed as table references - this.handleNestedFieldReferences(ast); - - let result: Command[]; - - switch (ast.type) { - case 'select': - result = [this.compileSelect(ast)]; - break; - case 'insert': - result = [this.compileInsert(ast)]; - break; - case 'update': - result = [this.compileUpdate(ast)]; - break; - case 'delete': - result = [this.compileDelete(ast)]; - break; - default: - throw new Error(`Unsupported SQL statement type: ${ast.type}`); - } - - log('Compiled to MongoDB command:', JSON.stringify(result, null, 2)); - - return result; - } - - /** - * Compile a SELECT statement into a MongoDB FIND command - */ - private compileSelect(ast: any): FindCommand { - if (!ast.from || !Array.isArray(ast.from) || ast.from.length === 0) { - throw new Error('FROM clause is required for SELECT statements'); - } - - const collection = this.extractTableName(ast.from[0]); - - const command: FindCommand = { - type: 'FIND', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined, - projection: ast.columns ? this.convertColumns(ast.columns) : undefined, - }; - - // Check if we need to use aggregate pipeline for column aliases - const hasColumnAliases = ast.columns && Array.isArray(ast.columns) && - ast.columns.some((col: any) => col.as); - - // Handle GROUP BY clause - if (ast.groupby) { - command.group = this.convertGroupBy(ast.groupby, ast.columns); - - // Check if we need to use aggregate pipeline instead of simple find - if (command.group) { - command.pipeline = this.createAggregatePipeline(command); - } - } - - // Handle JOINs - if (ast.from && ast.from.length > 1) { - command.lookup = this.convertJoins(ast.from, ast.where); - - // When using JOINs, we need to use the aggregate pipeline - if (!command.pipeline) { - command.pipeline = this.createAggregatePipeline(command); - } - } - - // If we have column aliases, we need to use aggregate pipeline with $project - if (hasColumnAliases && !command.pipeline) { - command.pipeline = this.createAggregatePipeline(command); - } - - if (ast.limit) { - log('Limit found in AST:', JSON.stringify(ast.limit, null, 2)); - if (typeof ast.limit === 'object' && 'value' in ast.limit && !Array.isArray(ast.limit.value)) { - // Standard PostgreSQL LIMIT format (without OFFSET) - command.limit = Number(ast.limit.value); - } else if (typeof ast.limit === 'object' && 'seperator' in ast.limit && Array.isArray(ast.limit.value)) { - // Handle PostgreSQL style LIMIT [OFFSET] - if (ast.limit.value.length > 0) { - if (ast.limit.seperator === 'offset') { - if (ast.limit.value.length === 1) { - // Only OFFSET specified (OFFSET X) - command.skip = Number(ast.limit.value[0].value); - } else if (ast.limit.value.length >= 2) { - // Both LIMIT and OFFSET specified (LIMIT X OFFSET Y) - command.limit = Number(ast.limit.value[0].value); - command.skip = Number(ast.limit.value[1].value); - } - } else { - // Regular LIMIT without OFFSET - command.limit = Number(ast.limit.value[0].value); - } - } - // If value array is empty, it means no LIMIT was specified, so we don't set a limit - } - } - - if (ast.orderby) { - command.sort = this.convertOrderBy(ast.orderby); - } - - return command; - } - - /** - * Compile an INSERT statement into a MongoDB INSERT command - */ - private compileInsert(ast: any): InsertCommand { - if (!ast.table) { - throw new Error('Table name is required for INSERT statements'); - } - - const collection = ast.table[0].table; - - if (!ast.values || !Array.isArray(ast.values)) { - throw new Error('VALUES are required for INSERT statements'); - } - - log('INSERT values:', JSON.stringify(ast.values, null, 2)); - log('INSERT columns:', JSON.stringify(ast.columns, null, 2)); - - const documents = ast.values.map((valueList: any) => { - const document: Record = {}; - - if (!ast.columns || !Array.isArray(ast.columns)) { - throw new Error('Columns are required for INSERT statements'); - } - - // Handle different forms of value lists - let values: any[] = []; - if (Array.isArray(valueList)) { - values = valueList; - } else if (valueList.type === 'expr_list' && Array.isArray(valueList.value)) { - values = valueList.value; - } else { - console.warn('Unexpected valueList format:', JSON.stringify(valueList, null, 2)); - values = [valueList]; - } - - log('Processed values:', JSON.stringify(values, null, 2)); - - ast.columns.forEach((column: any, index: number) => { - let columnName: string; - if (typeof column === 'string') { - columnName = column; - } else if (column.column) { - columnName = column.column; - } else { - console.warn('Unrecognized column format:', JSON.stringify(column, null, 2)); - return; - } - - if (index < values.length) { - document[columnName] = this.convertValue(values[index]); - } - }); - - log('Constructed document:', JSON.stringify(document, null, 2)); - return document; - }); - - return { - type: 'INSERT', - collection, - documents - }; - } - - /** - * Compile an UPDATE statement into a MongoDB UPDATE command - */ - private compileUpdate(ast: any): UpdateCommand { - if (!ast.table) { - throw new Error('Table name is required for UPDATE statements'); - } - - const collection = ast.table[0].table; - - if (!ast.set || !Array.isArray(ast.set)) { - throw new Error('SET clause is required for UPDATE statements'); - } - - const update: Record = {}; - - ast.set.forEach((setItem: any) => { - if (setItem.column && setItem.value) { - update[setItem.column] = this.convertValue(setItem.value); - } - }); - - return { - type: 'UPDATE', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined, - update - }; - } - - /** - * Compile a DELETE statement into a MongoDB DELETE command - */ - private compileDelete(ast: any): DeleteCommand { - if (!ast.from || !Array.isArray(ast.from) || ast.from.length === 0) { - throw new Error('FROM clause is required for DELETE statements'); - } - - const collection = this.extractTableName(ast.from[0]); - - return { - type: 'DELETE', - collection, - filter: ast.where ? this.convertWhere(ast.where) : undefined - }; - } - - /** - * Extract table name from FROM clause - */ - private extractTableName(from: From): string { - if (typeof from === 'string') { - return from; - } else if (from.table) { - return from.table; - } - throw new Error('Invalid FROM clause'); - } - - /** - * Convert SQL WHERE clause to MongoDB filter - */ - private convertWhere(where: any): Record { - if (!where) return {}; - - if (where.type === 'binary_expr') { - const { left, right, operator } = where; - - // Handle logical operators (AND, OR) - if (operator === 'AND') { - const leftFilter = this.convertWhere(left); - const rightFilter = this.convertWhere(right); - return { $and: [leftFilter, rightFilter] }; - } else if (operator === 'OR') { - const leftFilter = this.convertWhere(left); - const rightFilter = this.convertWhere(right); - return { $or: [leftFilter, rightFilter] }; - } - - // Handle comparison operators - if (typeof left === 'object' && 'column' in left && left.column) { - const field = this.processFieldName(left.column); - const value = this.convertValue(right); - - const filter: Record = {}; - - switch (operator) { - case '=': - filter[field] = value; - break; - case '!=': - case '<>': - filter[field] = { $ne: value }; - break; - case '>': - filter[field] = { $gt: value }; - break; - case '>=': - filter[field] = { $gte: value }; - break; - case '<': - filter[field] = { $lt: value }; - break; - case '<=': - filter[field] = { $lte: value }; - break; - case 'IN': - filter[field] = { $in: Array.isArray(value) ? value : [value] }; - break; - case 'NOT IN': - filter[field] = { $nin: Array.isArray(value) ? value : [value] }; - break; - case 'LIKE': - // Convert SQL LIKE pattern to MongoDB regex - // % wildcard in SQL becomes .* in regex - // _ wildcard in SQL becomes . in regex - const pattern = String(value) - .replace(/%/g, '.*') - .replace(/_/g, '.'); - filter[field] = { $regex: new RegExp(`^${pattern}$`, 'i') }; - break; - case 'BETWEEN': - if (Array.isArray(right) && right.length === 2) { - filter[field] = { - $gte: this.convertValue(right[0]), - $lte: this.convertValue(right[1]) - }; - } else { - throw new Error('BETWEEN operator expects two values'); - } - break; - default: - throw new Error(`Unsupported operator: ${operator}`); - } - - return filter; - } - } else if (where.type === 'unary_expr') { - // Handle NOT, IS NULL, IS NOT NULL - if (where.operator === 'IS NULL' && typeof where.expr === 'object' && 'column' in where.expr) { - const field = this.processFieldName(where.expr.column); - return { [field]: { $eq: null } }; - } else if (where.operator === 'IS NOT NULL' && typeof where.expr === 'object' && 'column' in where.expr) { - const field = this.processFieldName(where.expr.column); - return { [field]: { $ne: null } }; - } else if (where.operator === 'NOT') { - const subFilter = this.convertWhere(where.expr); - return { $nor: [subFilter] }; - } - } - - // If we can't parse the where clause, return an empty filter - return {}; - } - - /** - * Convert SQL value to MongoDB value - */ - private convertValue(value: any): any { - if (typeof value === 'object') { - // Handle expression lists (for IN operator) - if (value.type === 'expr_list' && Array.isArray(value.value)) { - return value.value.map((item: any) => this.convertValue(item)); - } - // Handle single values with value property - else if ('value' in value) { - return value.value; - } - } - return value; - } - - /** - * Convert SQL columns to MongoDB projection - */ - private convertColumns(columns: any[]): Record { - const projection: Record = {}; - - log('Converting columns to projection:', JSON.stringify(columns, null, 2)); - - // If * is used, return empty projection (which means all fields) - if (columns.some(col => col === '*' || - (typeof col === 'object' && col.expr && col.expr.type === 'star') || - (typeof col === 'object' && col.expr && col.expr.column === '*'))) { - log('Star (*) detected, returning empty projection'); - return {}; - } - - columns.forEach(column => { - if (typeof column === 'object') { - if ('expr' in column && column.expr) { - // Handle dot notation (nested fields) - if ('column' in column.expr && column.expr.column) { - const fieldName = this.processFieldName(column.expr.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } else if (column.expr.type === 'column_ref' && column.expr.column) { - const fieldName = this.processFieldName(column.expr.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } else if (column.expr.type === 'binary_expr' && column.expr.operator === '.' && - column.expr.left && column.expr.right) { - // Handle explicit dot notation like table.column - let fieldName = ''; - if (column.expr.left.column) { - fieldName = column.expr.left.column; - } - if (fieldName && column.expr.right.column) { - fieldName += '.' + column.expr.right.column; - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // Also include the parent field - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - } else if ('type' in column && column.type === 'column_ref' && column.column) { - const fieldName = this.processFieldName(column.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } else if ('column' in column) { - const fieldName = this.processFieldName(column.column); - const outputField = column.as || fieldName; - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - } else if (typeof column === 'string') { - const fieldName = this.processFieldName(column); - // For find queries, MongoDB projection uses 1 - projection[fieldName] = 1; - - // For nested fields, also include the parent field - if (fieldName.includes('.')) { - const parentField = fieldName.split('.')[0]; - projection[parentField] = 1; - } - } - }); - - log('Final projection:', JSON.stringify(projection, null, 2)); - - return projection; - } - - /** - * Process a field name to handle nested fields and array indexing - * Converts various formats to MongoDB dot notation: - * - address.zip stays as address.zip (MongoDB supports dot notation natively) - * - items__ARRAY_0__name becomes items.0.name - * - items_0_name becomes items.0.name (from aggressive preprocessing) - * - table.column is recognized as a nested field, not a table reference - */ - private processFieldName(fieldName: string): string { - if (!fieldName) return fieldName; - - log(`Processing field name: "${fieldName}"`); - - // First convert our placeholder format back to MongoDB dot notation - // This transforms items__ARRAY_0__name => items.0.name - let processed = fieldName.replace(/__ARRAY_(\d+)__/g, '.$1.'); - - // Also handle the case where it's at the end of the string - processed = processed.replace(/__ARRAY_(\d+)$/g, '.$1'); - - // Handle the aggressive preprocessing format - items_0_name => items.0.name - processed = processed.replace(/(\w+)_(\d+)_(\w+)/g, '$1.$2.$3'); - processed = processed.replace(/(\w+)_(\d+)$/g, '$1.$2'); - - // If there's still array indexing with bracket notation, convert it too - // This handles any direct [0] syntax that might have made it through the parser - processed = processed.replace(/\[(\d+)\]/g, '.$1'); - - // Handle nested field access directly - // MongoDB already uses dot notation for nested fields, so we can use it as is - if (processed.includes('.')) { - log(`Using nested field in MongoDB filter: "${processed}"`); - } - - return processed; - } - - /** - * Special handling for table references that might actually be nested fields - * For example, in "SELECT address.zip FROM users", - * address.zip might be parsed as table "address", column "zip" - */ - private handleNestedFieldReferences(ast: any): void { - log('Handling nested field references in AST'); - - // Handle column references in SELECT clause - if (ast.columns && Array.isArray(ast.columns)) { - ast.columns.forEach((column: any) => { - if (column.expr && column.expr.type === 'column_ref' && - column.expr.table && column.expr.column) { - // This could be a nested field - convert table.column to a single column path - column.expr.column = `${column.expr.table}.${column.expr.column}`; - column.expr.table = null; - log(`Converted SELECT column to nested field: ${column.expr.column}`); - } - }); - } - - // Handle conditions in WHERE clause - this.processWhereClauseForNestedFields(ast.where); - - // For debugging - show the resulting AST after transformation - log('AST after nested field handling:', JSON.stringify(ast?.where, null, 2)); - } - - /** - * Process WHERE clause to handle nested field references - */ - private processWhereClauseForNestedFields(where: any): void { - if (!where) return; - - log('Processing WHERE clause for nested fields:', JSON.stringify(where, null, 2)); - - if (where.type === 'binary_expr') { - // Process left and right sides recursively - this.processWhereClauseForNestedFields(where.left); - this.processWhereClauseForNestedFields(where.right); - - // Handle column references in comparison expressions - if (where.left && where.left.type === 'column_ref') { - log('Processing column reference:', JSON.stringify(where.left, null, 2)); - - // Handle both direct dot notation in column name and table.column format - if (where.left.column && where.left.column.includes('.')) { - // Already has dot notation, just keep it - log('Column already has dot notation:', where.left.column); - } else if (where.left.table && where.left.column) { - // Convert table.column format to a nested field path - log('Converting table.column to nested path:', - `${where.left.table}.${where.left.column}`); - where.left.column = `${where.left.table}.${where.left.column}`; - where.left.table = null; - } - } - } else if (where.type === 'unary_expr') { - // Process expression in unary operators - this.processWhereClauseForNestedFields(where.expr); - } - } - - /** - * Convert SQL ORDER BY to MongoDB sort - */ - private convertOrderBy(orderby: any[]): Record { - const sort: Record = {}; - - orderby.forEach(item => { - if (typeof item === 'object' && 'expr' in item && item.expr) { - if ('column' in item.expr && item.expr.column) { - const column = this.processFieldName(item.expr.column); - sort[column] = item.type === 'ASC' ? 1 : -1; - } - } - }); - - return sort; - } - - /** - * Convert SQL GROUP BY to MongoDB group stage - */ - private convertGroupBy(groupby: any[], columns: any[]): { _id: any; [key: string]: any } | undefined { - if (!groupby || !Array.isArray(groupby) || groupby.length === 0) { - return undefined; - } - - log('Converting GROUP BY:', JSON.stringify(groupby, null, 2)); - log('With columns:', JSON.stringify(columns, null, 2)); - - // Create the group stage - let group: { _id: any; [key: string]: any }; - - // If there's only one group by field, simplify the _id structure - if (groupby.length === 1) { - // Extract the single field name - let singleField = ''; - if (typeof groupby[0] === 'object') { - // Type 1: { column: 'field' } - if (groupby[0].column) { - singleField = this.processFieldName(groupby[0].column); - } - // Type 2: { type: 'column_ref', column: 'field' } - else if (groupby[0].type === 'column_ref' && groupby[0].column) { - singleField = this.processFieldName(groupby[0].column); - } - // Type 3: { expr: { column: 'field' } } - else if (groupby[0].expr && groupby[0].expr.column) { - singleField = this.processFieldName(groupby[0].expr.column); - } - } - - if (singleField) { - // For a single field, use a simplified ID structure - group = { - _id: `$${singleField}`, - [singleField]: { $first: `$${singleField}` } // Include the field in results too - }; - } else { - // Fallback if we can't extract the field - group = { _id: null }; - } - } else { - // For multiple fields, use the object structure for _id - const groupFields: Record = {}; - groupby.forEach(item => { - if (typeof item === 'object') { - let field = ''; - // Type 1: { column: 'field' } - if (item.column) { - field = this.processFieldName(item.column); - } - // Type 2: { type: 'column_ref', column: 'field' } - else if (item.type === 'column_ref' && item.column) { - field = this.processFieldName(item.column); - } - // Type 3: { expr: { column: 'field' } } - else if (item.expr && item.expr.column) { - field = this.processFieldName(item.expr.column); - } - - if (field) { - groupFields[field] = `$${field}`; - } - } - }); - - group = { - _id: groupFields - }; - } - - // Add aggregations for other columns - if (columns && Array.isArray(columns)) { - columns.forEach(column => { - if (typeof column === 'object') { - // Check for aggregation functions like COUNT, SUM, AVG, etc. - if (column.expr && column.expr.type && - (column.expr.type === 'function' || column.expr.type === 'aggr_func')) { - - const funcName = column.expr.name.toLowerCase(); - const args = column.expr.args && column.expr.args.expr ? - column.expr.args.expr : - column.expr.args; - - let field = '*'; - if (args && args.column) { - field = this.processFieldName(args.column); - } else if (args && args.type === 'star') { - // COUNT(*) case - field = '*'; - } - - // Use the specified alias or create one - let alias = column.as || `${funcName}_${field}`; - - // Map SQL functions to MongoDB aggregation operators - switch (funcName) { - case 'count': - group[alias] = { $sum: 1 }; - break; - case 'sum': - group[alias] = { $sum: `$${field}` }; - break; - case 'avg': - group[alias] = { $avg: `$${field}` }; - break; - case 'min': - group[alias] = { $min: `$${field}` }; - break; - case 'max': - group[alias] = { $max: `$${field}` }; - break; - } - } else if (column.expr && column.expr.type === 'column_ref') { - // Include GROUP BY fields directly in the results - const field = this.processFieldName(column.expr.column); - - // Only add if this is one of our group by fields - const isGroupByField = groupby.some(g => { - if (typeof g === 'object') { - if (g.column) { - return g.column === column.expr.column; - } else if (g.type === 'column_ref' && g.column) { - return g.column === column.expr.column; - } else if (g.expr && g.expr.column) { - return g.expr.column === column.expr.column; - } - } - return false; - }); - - if (isGroupByField) { - // Use $first to just take the first value from each group - // since all values in the group should be the same for this field - group[field] = { $first: `$${field}` }; - } - } - } - }); - } - - log('Generated group stage:', JSON.stringify(group, null, 2)); - return group; - } - - /** - * Create a MongoDB aggregation pipeline from a FindCommand - */ - private createAggregatePipeline(command: FindCommand): Record[] { - const pipeline: Record[] = []; - - // Start with $match if we have a filter - if (command.filter) { - pipeline.push({ $match: command.filter }); - } - - // Add $lookup stages for JOINs - if (command.lookup && command.lookup.length > 0) { - command.lookup.forEach(lookup => { - pipeline.push({ - $lookup: { - from: lookup.from, - localField: lookup.localField, - foreignField: lookup.foreignField, - as: lookup.as - } - }); - - // Add $unwind stage to flatten the joined array - pipeline.push({ - $unwind: { - path: "$" + lookup.as, - preserveNullAndEmptyArrays: true - } - }); - }); - } - - // Add $group stage if grouping is requested - if (command.group) { - pipeline.push({ $group: command.group }); - } - - // Add $sort if sort is specified - if (command.sort) { - pipeline.push({ $sort: command.sort }); - } - - // Add $skip if skip is specified - if (command.skip) { - pipeline.push({ $skip: command.skip }); - } - - // Add $limit if limit is specified - if (command.limit) { - pipeline.push({ $limit: command.limit }); - } - - // Add $project if projection is specified - if (command.projection && Object.keys(command.projection).length > 0) { - const projectionFormat = this.needsAggregationProjection(command.projection) - ? this.convertToAggregationProjection(command.projection) - : command.projection; - pipeline.push({ $project: projectionFormat }); - } - - log('Generated aggregate pipeline:', JSON.stringify(pipeline, null, 2)); - return pipeline; - } - - /** - * Check if projection needs to be converted to $project format - */ - private needsAggregationProjection(projection: Record): boolean { - // Check if any value is a string that starts with $ - return Object.values(projection).some( - value => typeof value === 'string' && value.startsWith('$') - ); - } - - /** - * Convert a MongoDB projection to $project format used in aggregation pipeline - */ - private convertToAggregationProjection(projection: Record): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(projection)) { - if (typeof value === 'string' && value.startsWith('$')) { - // This is a field reference, keep it as is - result[key] = value; - } else if (value === 1) { - // For 1 values, keep as 1 for MongoDB's $project stage - result[key] = 1; - } else { - // Otherwise, keep as is - result[key] = value; - } - } - return result; - } - - /** - * Convert SQL JOINs to MongoDB $lookup stages - */ - private convertJoins(from: any[], where: any): { from: string; localField: string; foreignField: string; as: string }[] { - if (!from || !Array.isArray(from) || from.length <= 1) { - return []; - } - - log('Converting JOINs:', JSON.stringify(from, null, 2)); - log('With WHERE:', JSON.stringify(where, null, 2)); - - const lookups: { from: string; localField: string; foreignField: string; as: string }[] = []; - const mainTable = this.extractTableName(from[0]); - - // Extract join conditions from the WHERE clause - // This is a simplification that assumes the ON conditions are in the WHERE clause - const joinConditions = this.extractJoinConditions(where, from); - - // Process each table after the first one (the main table) - for (let i = 1; i < from.length; i++) { - const joinedTable = this.extractTableName(from[i]); - const alias = from[i].as || joinedTable; - - // Look for JOIN condition for this table - const joinCond = joinConditions.find( - cond => (cond.leftTable === mainTable && cond.rightTable === joinedTable) || - (cond.leftTable === joinedTable && cond.rightTable === mainTable) - ); - - if (joinCond) { - const localField = joinCond.leftTable === mainTable ? joinCond.leftField : joinCond.rightField; - const foreignField = joinCond.leftTable === mainTable ? joinCond.rightField : joinCond.leftField; - - lookups.push({ - from: joinedTable, - localField, - foreignField, - as: alias - }); - } else { - // If no explicit join condition was found, assume it's a cross join - // or guess based on common naming conventions (e.g., userId -> _id) - let localField = '_id'; - let foreignField = `${mainTable.toLowerCase().replace(/s$/, '')}Id`; - - lookups.push({ - from: joinedTable, - localField, - foreignField, - as: alias - }); - } - } - - log('Generated lookups:', JSON.stringify(lookups, null, 2)); - return lookups; - } - - /** - * Extract join conditions from the WHERE clause - */ - private extractJoinConditions(where: any, tables: any[]): Array<{ - leftTable: string; - leftField: string; - rightTable: string; - rightField: string; - }> { - if (!where) { - return []; - } - - const tableNames = tables.map(t => { - if (typeof t === 'string') return t; - return t.table; - }); - - const conditions: Array<{ - leftTable: string; - leftField: string; - rightTable: string; - rightField: string; - }> = []; - - // For equality comparisons in the WHERE clause that reference different tables - if (where.type === 'binary_expr' && where.operator === '=') { - if (where.left && where.left.type === 'column_ref' && where.left.table && - where.right && where.right.type === 'column_ref' && where.right.table) { - - const leftTable = where.left.table; - const leftField = where.left.column; - const rightTable = where.right.table; - const rightField = where.right.column; - - if (tableNames.includes(leftTable) && tableNames.includes(rightTable)) { - conditions.push({ - leftTable, - leftField, - rightTable, - rightField - }); - } - } - } - // For AND conditions, recursively extract join conditions from both sides - else if (where.type === 'binary_expr' && where.operator === 'AND') { - const leftConditions = this.extractJoinConditions(where.left, tables); - const rightConditions = this.extractJoinConditions(where.right, tables); - conditions.push(...leftConditions, ...rightConditions); - } - - return conditions; - } -} \ No newline at end of file diff --git a/src/examples/basic-usage.ts b/src/examples/basic-usage.ts deleted file mode 100644 index bea16d9..0000000 --- a/src/examples/basic-usage.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { QueryLeaf } from '../index'; -import { MongoClient } from 'mongodb'; - -/** - * Example showing how to use QueryLeaf with an existing MongoDB client - */ -async function main() { - // Your existing MongoDB connection - const connectionString = 'mongodb://localhost:27017'; - const dbName = 'example'; - - // In a real application, you would already have a MongoDB client - const mongoClient = new MongoClient(connectionString); - await mongoClient.connect(); - - // Create a QueryLeaf instance with your MongoDB client - const queryLeaf = new QueryLeaf(mongoClient, dbName); - - try { - // Setup sample data with nested structures and arrays - console.log('\nSetting up sample data with nested structures and arrays...'); - - await queryLeaf.execute(` - INSERT INTO users (_id, name, age, email, active, address) VALUES - ('101', 'Nested User', 30, 'nested@example.com', true, { - "street": "123 Main St", - "city": "New York", - "state": "NY", - "zip": "10001" - }) - `); - - await queryLeaf.execute(` - INSERT INTO orders (_id, userId, items, total) VALUES - ('201', '101', [ - { "id": "item1", "name": "Laptop", "price": 1200 }, - { "id": "item2", "name": "Mouse", "price": 25 } - ], 1225) - `); - - // Example SQL queries - const queries = [ - // Basic SELECT - 'SELECT * FROM users LIMIT 5', - - // SELECT with WHERE condition - "SELECT name, email FROM users WHERE age > 21 AND active = true", - - // SELECT with ORDER BY - 'SELECT * FROM products ORDER BY price DESC LIMIT 3', - - // Nested field queries - show accessing address fields - "SELECT name, address.city, address.zip FROM users WHERE _id = '101'", - - // Query with nested field condition - "SELECT * FROM users WHERE address.city = 'New York'", - - // Array element access - query showing array indices - "SELECT _id, items[0].name, items[0].price FROM orders WHERE _id = '201'", - - // Array element condition - "SELECT _id, userId FROM orders WHERE items[0].price > 1000", - - // GROUP BY with aggregation functions - "SELECT status, COUNT(*) as count, SUM(total) as total_amount FROM orders GROUP BY status", - - // JOIN between collections - "SELECT u.name, o._id as order_id, o.total FROM users u JOIN orders o ON u._id = o.userId", - - // INSERT example - "INSERT INTO users (_id, name, age, email, active) VALUES ('100', 'Example User', 25, 'example@example.com', true)", - - // SELECT to verify the insertion - "SELECT * FROM users WHERE _id = '100'", - - // UPDATE example - "UPDATE users SET age = 26 WHERE _id = '100'", - - // SELECT to verify the update - "SELECT * FROM users WHERE _id = '100'", - - // DELETE example - "DELETE FROM users WHERE _id = '100'", - - // SELECT to verify the deletion - "SELECT * FROM users WHERE _id = '100'", - - // Clean up sample data - "DELETE FROM users WHERE _id = '101'", - "DELETE FROM orders WHERE _id = '201'" - ]; - - // Execute each query and display results - for (const sql of queries) { - console.log(`\nExecuting SQL: ${sql}`); - try { - const result = await queryLeaf.execute(sql); - console.log('Result:', JSON.stringify(result, null, 2)); - } catch (error) { - console.error('Error:', error instanceof Error ? error.message : String(error)); - } - } - } finally { - // Close the MongoDB client that we created - // QueryLeaf does not manage MongoDB connections - await mongoClient.close(); - } -} - -// Run the example if this file is executed directly -if (require.main === module) { - main().catch(error => { - console.error('Unhandled error:', error); - process.exit(1); - }); -} \ No newline at end of file diff --git a/src/examples/dummy-client-demo.ts b/src/examples/dummy-client-demo.ts deleted file mode 100644 index 6ecae6e..0000000 --- a/src/examples/dummy-client-demo.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Example demonstrating the use of the DummyQueryLeaf for testing - */ - -import { DummyQueryLeaf } from '../index'; - -async function main() { - console.log('Creating DummyQueryLeaf for testing'); - const queryLeaf = new DummyQueryLeaf('test_database'); - - console.log('Executing SELECT statement'); - await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); - - console.log('Executing INSERT statement'); - await queryLeaf.execute(` - INSERT INTO products (name, price, category) - VALUES ('Laptop', 1299.99, 'Electronics') - `); - - console.log('Executing UPDATE statement'); - await queryLeaf.execute(` - UPDATE users - SET status = 'active', last_login = NOW() - WHERE user_id = '507f1f77bcf86cd799439011' - `); - - console.log('Executing DELETE statement'); - await queryLeaf.execute(` - DELETE FROM orders - WHERE status = 'cancelled' AND created_at < '2023-01-01' - `); - - console.log('Executing GROUP BY with aggregation'); - await queryLeaf.execute(` - SELECT category, AVG(price) as avg_price, COUNT(*) as product_count - FROM products - GROUP BY category - HAVING AVG(price) > 100 - `); - - console.log('Executing JOIN'); - await queryLeaf.execute(` - SELECT o.order_id, u.name, o.total - FROM orders o - JOIN users u ON o.user_id = u.user_id - WHERE o.status = 'shipped' - `); - - console.log('Done with testing'); -} - -// Run the example -main().catch(error => { - console.error('Error in example:', error); -}); \ No newline at end of file diff --git a/src/examples/existing-client-demo.ts b/src/examples/existing-client-demo.ts deleted file mode 100644 index 98c0d82..0000000 --- a/src/examples/existing-client-demo.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Example showing how to use QueryLeaf with an existing MongoDB client - * This is the recommended way to use QueryLeaf in a real application - */ - -import { MongoClient } from 'mongodb'; -import { QueryLeaf } from '../index'; - -// This would be your application's existing MongoDB client setup -class MyApplication { - private mongoClient: MongoClient; - - constructor() { - // Your application's MongoDB client configuration - this.mongoClient = new MongoClient('mongodb://localhost:27017', { - // Your custom options here - connectTimeoutMS: 5000, - // etc. - }); - } - - async initialize() { - console.log('Initializing application...'); - // Connect your MongoDB client - await this.mongoClient.connect(); - console.log('MongoDB client connected'); - } - - async shutdown() { - console.log('Shutting down application...'); - await this.mongoClient.close(); - console.log('MongoDB client disconnected'); - } - - // Your application would use this MongoDB client directly for some operations - getMongoClient() { - return this.mongoClient; - } -} - -async function main() { - // Create your application - const app = new MyApplication(); - - try { - // Initialize your application (connects to MongoDB) - await app.initialize(); - - // Create QueryLeaf using your application's MongoDB client directly - const queryLeaf = new QueryLeaf( - app.getMongoClient(), - 'example_db' - ); - - console.log('\nExecuting SQL query using your existing MongoDB client:'); - - // Example: Create a test collection - const createQuery = ` - INSERT INTO test_collection (name, value) VALUES - ('Example', 42) - `; - - console.log(`\nExecuting SQL: ${createQuery}`); - const createResult = await queryLeaf.execute(createQuery); - console.log('Result:', JSON.stringify(createResult, null, 2)); - - // Example: Query the collection - const selectQuery = 'SELECT * FROM test_collection'; - console.log(`\nExecuting SQL: ${selectQuery}`); - const selectResult = await queryLeaf.execute(selectQuery); - console.log('Result:', JSON.stringify(selectResult, null, 2)); - - // Clean up - const cleanupQuery = 'DELETE FROM test_collection'; - console.log(`\nExecuting SQL: ${cleanupQuery}`); - const cleanupResult = await queryLeaf.execute(cleanupQuery); - console.log('Result:', JSON.stringify(cleanupResult, null, 2)); - - // You can close QueryLeaf, but it won't close your MongoDB client - await queryLeaf.close(); - - } finally { - // Your application manages the MongoDB client lifecycle - await app.shutdown(); - } -} - -// Run the example -if (require.main === module) { - main().catch(error => { - console.error('Error:', error); - process.exit(1); - }); -} \ No newline at end of file diff --git a/src/executor/dummy-client.ts b/src/executor/dummy-client.ts deleted file mode 100644 index 36c3904..0000000 --- a/src/executor/dummy-client.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { MongoClient, Db, Collection } from 'mongodb'; - -/** - * Dummy MongoDB database that logs operations instead of executing them - */ -class DummyDb { - private dbName: string; - private collections: Map = new Map(); - - constructor(dbName: string) { - this.dbName = dbName; - } - - /** - * Get a collection from the dummy database - * @param name Collection name - * @returns A dummy collection - */ - collection(name: string): Collection { - if (!this.collections.has(name)) { - this.collections.set(name, new DummyCollection(name, this.dbName)); - } - return this.collections.get(name) as unknown as Collection; - } -} - -/** - * Dummy MongoDB collection that logs operations instead of executing them - */ -class DummyCollection { - private name: string; - private dbName: string; - - constructor(name: string, dbName: string) { - this.name = name; - this.dbName = dbName; - } - - /** - * Log a find operation - * @param filter Query filter - * @returns A chainable cursor - */ - find(filter: any = {}) { - console.log(`[DUMMY MongoDB] FIND in ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - return new DummyCursor(this.name, 'find', filter); - } - - /** - * Log an insertMany operation - * @param documents Documents to insert - * @returns A dummy result - */ - async insertMany(documents: any[]) { - console.log(`[DUMMY MongoDB] INSERT into ${this.dbName}.${this.name}:`, JSON.stringify(documents, null, 2)); - return { - acknowledged: true, - insertedCount: documents.length, - insertedIds: documents.map((_, i) => i) - }; - } - - /** - * Log an updateMany operation - * @param filter Query filter - * @param update Update operation - * @returns A dummy result - */ - async updateMany(filter: any = {}, update: any) { - console.log(`[DUMMY MongoDB] UPDATE in ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - console.log(`[DUMMY MongoDB] UPDATE operation:`, JSON.stringify(update, null, 2)); - return { - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null - }; - } - - /** - * Log a deleteMany operation - * @param filter Query filter - * @returns A dummy result - */ - async deleteMany(filter: any = {}) { - console.log(`[DUMMY MongoDB] DELETE from ${this.dbName}.${this.name} with filter:`, JSON.stringify(filter, null, 2)); - return { - acknowledged: true, - deletedCount: 1 - }; - } - - /** - * Log an aggregate operation - * @param pipeline Aggregation pipeline - * @returns A chainable cursor - */ - aggregate(pipeline: any[]) { - console.log(`[DUMMY MongoDB] AGGREGATE in ${this.dbName}.${this.name} with pipeline:`, JSON.stringify(pipeline, null, 2)); - return new DummyCursor(this.name, 'aggregate', null, pipeline); - } -} - -/** - * Dummy MongoDB cursor that logs operations instead of executing them - */ -class DummyCursor { - private collectionName: string; - private operation: string; - private filter: any; - private pipeline: any[] | null; - private projectionObj: any = null; - private sortObj: any = null; - private limitVal: number | null = null; - private skipVal: number | null = null; - - constructor(collectionName: string, operation: string, filter: any = null, pipeline: any[] | null = null) { - this.collectionName = collectionName; - this.operation = operation; - this.filter = filter; - this.pipeline = pipeline; - } - - /** - * Add projection to the cursor - * @param projection Projection specification - * @returns The cursor - */ - project(projection: any) { - console.log(`[DUMMY MongoDB] Adding projection to ${this.operation}:`, JSON.stringify(projection, null, 2)); - this.projectionObj = projection; - return this; - } - - /** - * Add sort to the cursor - * @param sort Sort specification - * @returns The cursor - */ - sort(sort: any) { - console.log(`[DUMMY MongoDB] Adding sort to ${this.operation}:`, JSON.stringify(sort, null, 2)); - this.sortObj = sort; - return this; - } - - /** - * Add limit to the cursor - * @param limit Limit value - * @returns The cursor - */ - limit(limit: number) { - console.log(`[DUMMY MongoDB] Adding limit to ${this.operation}:`, limit); - this.limitVal = limit; - return this; - } - - /** - * Add skip to the cursor - * @param skip Skip value - * @returns The cursor - */ - skip(skip: number) { - console.log(`[DUMMY MongoDB] Adding skip to ${this.operation}:`, skip); - this.skipVal = skip; - return this; - } - - /** - * Convert the cursor to an array of results - * @returns A dummy array of results - */ - async toArray() { - console.log(`[DUMMY MongoDB] Executing ${this.operation} on ${this.collectionName}`); - if (this.projectionObj) console.log(` - Projection:`, JSON.stringify(this.projectionObj, null, 2)); - if (this.sortObj) console.log(` - Sort:`, JSON.stringify(this.sortObj, null, 2)); - if (this.limitVal !== null && this.limitVal > 0) console.log(` - Limit:`, this.limitVal); - if (this.skipVal !== null) console.log(` - Skip:`, this.skipVal); - - // Return a dummy result indicating this is a simulation - return [{ - _id: 'dummy-id', - operation: this.operation, - message: 'This is a dummy result from the DummyClient' - }]; - } -} - -/** - * A dummy MongoDB client that mimics the MongoDB client interface - * Logs operations instead of executing them - useful for testing and debugging - */ -export class DummyMongoClient extends MongoClient { - private databases: Map = new Map(); - - /** - * Create a new dummy client - */ - constructor() { - // Pass an empty string since we're not actually connecting - super('mongodb://dummy'); - } - - /** - * Get a dummy database - * @param dbName Database name - * @returns A dummy database instance - */ - override db(dbName: string): Db { - console.log(`[DUMMY MongoDB] Using database: ${dbName}`); - if (!this.databases.has(dbName)) { - this.databases.set(dbName, new DummyDb(dbName)); - } - return this.databases.get(dbName) as unknown as Db; - } - - /** - * Simulate connection - no actual connection is made - */ - override async connect(): Promise { - console.log('[DUMMY MongoDB] Connected to MongoDB (simulated)'); - return this; - } - - /** - * Simulate closing the connection - */ - override async close(): Promise { - console.log('[DUMMY MongoDB] Closed MongoDB connection (simulated)'); - } -} \ No newline at end of file diff --git a/src/executor/index.ts b/src/executor/index.ts deleted file mode 100644 index 769b51b..0000000 --- a/src/executor/index.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { CommandExecutor, Command } from '../interfaces'; -import { MongoClient, ObjectId } from 'mongodb'; - -/** - * MongoDB command executor implementation for Node.js - */ -export class MongoExecutor implements CommandExecutor { - private client: MongoClient; - private dbName: string; - - /** - * Create a new MongoDB executor using a MongoDB client - * @param client MongoDB client instance - * @param dbName Database name - */ - constructor(client: MongoClient, dbName: string) { - this.client = client; - this.dbName = dbName; - } - - /** - * No-op - client lifecycle is managed by the user - */ - async connect(): Promise { - // Connection is managed by the user - } - - /** - * No-op - client lifecycle is managed by the user - */ - async close(): Promise { - // Connection is managed by the user - } - - /** - * Execute a series of MongoDB commands - * @param commands Array of commands to execute - * @returns Result of the last command - */ - async execute(commands: Command[]): Promise { - // We assume the client is already connected - - const database = this.client.db(this.dbName); - - // Execute each command in sequence - let result = null; - for (const command of commands) { - switch (command.type) { - case 'FIND': - const findCursor = database.collection(command.collection) - .find(this.convertObjectIds(command.filter || {})); - - // Apply projection if specified - if (command.projection) { - findCursor.project(command.projection); - } - - // Apply sorting if specified - if (command.sort) { - findCursor.sort(command.sort); - } - - // Apply pagination if specified - if (command.skip) { - findCursor.skip(command.skip); - } - if (command.limit && command.limit > 0) { - findCursor.limit(command.limit); - } - - result = await findCursor.toArray(); - break; - - case 'INSERT': - result = await database.collection(command.collection) - .insertMany(command.documents.map(doc => this.convertObjectIds(doc))); - break; - - case 'UPDATE': - result = await database.collection(command.collection) - .updateMany( - this.convertObjectIds(command.filter || {}), - { $set: this.convertObjectIds(command.update) } - ); - break; - - case 'DELETE': - result = await database.collection(command.collection) - .deleteMany(this.convertObjectIds(command.filter || {})); - break; - - case 'AGGREGATE': - // Handle aggregation commands - const pipeline = command.pipeline.map(stage => this.convertObjectIds(stage)); - result = await database.collection(command.collection) - .aggregate(pipeline).toArray(); - break; - - default: - throw new Error(`Unsupported command type: ${(command as any).type}`); - } - } - - return result; - } - - /** - * Convert string ObjectIds to MongoDB ObjectId instances - * @param obj Object to convert - * @returns Object with converted ObjectIds - */ - private convertObjectIds(obj: any): any { - if (!obj) return obj; - - if (Array.isArray(obj)) { - return obj.map(item => this.convertObjectIds(item)); - } - - if (typeof obj === 'object') { - const result: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - // Special handling for _id field and fields ending with Id - if ((key === '_id' || key.endsWith('Id') || key.endsWith('Ids')) && typeof value === 'string') { - try { - // Check if it's a valid ObjectId string - if (/^[0-9a-fA-F]{24}$/.test(value)) { - result[key] = new ObjectId(value); - continue; - } - } catch (error) { - // If it's not a valid ObjectId, keep it as a string - console.warn(`Could not convert ${key} value to ObjectId: ${value}`); - } - } else if (Array.isArray(value) && (key.endsWith('Ids') || key === 'productIds')) { - // For arrays of IDs - result[key] = value.map((item: any) => { - if (typeof item === 'string' && /^[0-9a-fA-F]{24}$/.test(item)) { - try { - return new ObjectId(item); - } catch (error) { - return item; - } - } - return this.convertObjectIds(item); - }); - continue; - } else if (typeof value === 'object' && value !== null) { - // Recursively convert nested objects - result[key] = this.convertObjectIds(value); - continue; - } - - // Copy other values as is - result[key] = value; - } - - return result; - } - - return obj; - } -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 8d264f0..0000000 --- a/src/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - SqlStatement, - Command, - SqlParser, - SqlCompiler, - CommandExecutor -} from './interfaces'; -import { MongoClient } from 'mongodb'; -import { SqlParserImpl } from './parser'; -import { SqlCompilerImpl } from './compiler'; -import { MongoExecutor } from './executor'; -import { DummyMongoClient } from './executor/dummy-client'; - -/** - * QueryLeaf: SQL to MongoDB query translator - */ -export class QueryLeaf { - private parser: SqlParser; - private compiler: SqlCompiler; - private executor: CommandExecutor; - - /** - * Create a new QueryLeaf instance with your MongoDB client - * @param client Your MongoDB client - * @param dbName Database name - */ - constructor(client: MongoClient, dbName: string) { - this.parser = new SqlParserImpl(); - this.compiler = new SqlCompilerImpl(); - this.executor = new MongoExecutor(client, dbName); - } - - /** - * Execute a SQL query on MongoDB - * @param sql SQL query string - * @returns Query results - */ - async execute(sql: string): Promise { - const statement = this.parse(sql); - const commands = this.compile(statement); - return await this.executor.execute(commands); - } - - /** - * Parse a SQL query string - * @param sql SQL query string - * @returns Parsed SQL statement - */ - parse(sql: string): SqlStatement { - return this.parser.parse(sql); - } - - /** - * Compile a SQL statement to MongoDB commands - * @param statement SQL statement - * @returns MongoDB commands - */ - compile(statement: SqlStatement): Command[] { - return this.compiler.compile(statement); - } - - /** - * Get the command executor instance - * @returns Command executor - */ - getExecutor(): CommandExecutor { - return this.executor; - } - - /** - * No-op method for backward compatibility - * QueryLeaf no longer manages MongoDB connections - */ - async close(): Promise { - // No-op - MongoDB client is managed by the user - } -} - -/** - * Create a QueryLeaf instance with a dummy client for testing - * No actual MongoDB connection is made - */ -export class DummyQueryLeaf extends QueryLeaf { - /** - * Create a new DummyQueryLeaf instance - * @param dbName Database name - */ - constructor(dbName: string) { - super(new DummyMongoClient(), dbName); - } -} - -// Export interfaces and implementation classes -export { - SqlStatement, - Command, - SqlParser, - SqlCompiler, - CommandExecutor, - SqlParserImpl, - SqlCompilerImpl, - MongoExecutor, - DummyMongoClient -}; - -// Re-export interfaces -export * from './interfaces'; \ No newline at end of file diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index 677208b..0000000 --- a/src/interfaces.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { AST } from 'node-sql-parser'; - -/** - * Represents a parsed SQL statement - */ -export interface SqlStatement { - ast: AST; - text: string; -} - -/** - * Command types supported by the MongoDB executor - */ -export type CommandType = 'FIND' | 'INSERT' | 'UPDATE' | 'DELETE' | 'AGGREGATE'; - -/** - * Base interface for all MongoDB commands - */ -export interface BaseCommand { - type: CommandType; - collection: string; -} - -/** - * Find command for MongoDB - */ -export interface FindCommand extends BaseCommand { - type: 'FIND'; - filter?: Record; - projection?: Record; - sort?: Record; - limit?: number; - skip?: number; - group?: { - _id: any; - [key: string]: any; - }; - pipeline?: Record[]; - lookup?: { - from: string; - localField: string; - foreignField: string; - as: string; - }[]; -} - -/** - * Insert command for MongoDB - */ -export interface InsertCommand extends BaseCommand { - type: 'INSERT'; - documents: Record[]; -} - -/** - * Update command for MongoDB - */ -export interface UpdateCommand extends BaseCommand { - type: 'UPDATE'; - filter?: Record; - update: Record; - upsert?: boolean; -} - -/** - * Delete command for MongoDB - */ -export interface DeleteCommand extends BaseCommand { - type: 'DELETE'; - filter?: Record; -} - -/** - * Aggregate command for MongoDB - */ -export interface AggregateCommand extends BaseCommand { - type: 'AGGREGATE'; - pipeline: Record[]; -} - -/** - * Union type of all MongoDB commands - */ -export type Command = FindCommand | InsertCommand | UpdateCommand | DeleteCommand | AggregateCommand; - -/** - * SQL parser interface - */ -export interface SqlParser { - parse(sql: string): SqlStatement; -} - -/** - * SQL to MongoDB compiler interface - */ -export interface SqlCompiler { - compile(statement: SqlStatement): Command[]; -} - -/** - * MongoDB command executor interface - */ -export interface CommandExecutor { - connect(): Promise; - close(): Promise; - execute(commands: Command[]): Promise; -} - -/** - * Main QueryLeaf interface - */ -export interface QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; - getExecutor(): CommandExecutor; - close(): Promise; -} - -export interface Squongo extends QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; - getExecutor(): CommandExecutor; - close(): Promise; -} \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts deleted file mode 100644 index 94e5f8b..0000000 --- a/src/interfaces/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Core interfaces for the Squongo SQL to MongoDB compiler - */ - -/** - * Represents an abstract command in the intermediate representation - */ -export interface Command { - type: string; - [key: string]: any; -} - -/** - * Base interface for all SQL statements - */ -export interface SqlStatement { - type: string; -} - -/** - * Represents a SQL SELECT statement - */ -export interface SelectStatement extends SqlStatement { - type: 'SELECT'; - fields: string[]; - from: string; - where?: WhereClause; - limit?: number; - offset?: number; - orderBy?: OrderByClause[]; -} - -/** - * Represents a SQL INSERT statement - */ -export interface InsertStatement extends SqlStatement { - type: 'INSERT'; - into: string; - columns: string[]; - values: any[][]; -} - -/** - * Represents a SQL UPDATE statement - */ -export interface UpdateStatement extends SqlStatement { - type: 'UPDATE'; - table: string; - set: { [key: string]: any }; - where?: WhereClause; -} - -/** - * Represents a SQL DELETE statement - */ -export interface DeleteStatement extends SqlStatement { - type: 'DELETE'; - from: string; - where?: WhereClause; -} - -/** - * Represents an ORDER BY clause - */ -export interface OrderByClause { - field: string; - direction: 'ASC' | 'DESC'; -} - -/** - * Represents a WHERE clause condition - */ -export interface WhereClause { - operator: 'AND' | 'OR'; - conditions: Condition[]; -} - -/** - * Represents a condition in a WHERE clause - */ -export interface Condition { - type: 'COMPARISON' | 'LOGICAL' | 'IN' | 'BETWEEN' | 'LIKE' | 'NULL'; - field?: string; - operator?: string; - value?: any; - left?: Condition; - right?: Condition; - values?: any[]; - not?: boolean; -} - -/** - * MongoDB command executor interface - */ -export interface CommandExecutor { - execute(commands: Command[]): Promise; -} - -/** - * SQL Parser interface - */ -export interface SqlParser { - parse(sql: string): SqlStatement; -} - -/** - * SQL Compiler interface - converts SQL AST to MongoDB commands - */ -export interface SqlCompiler { - compile(statement: SqlStatement): Command[]; -} - -/** - * Main QueryLeaf interface - */ -export interface QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; - getExecutor?(): any; - close?(): Promise; -} - -/** - * Alias for QueryLeaf (backwards compatibility) - */ -export interface Squongo extends QueryLeaf { - execute(sql: string): Promise; - parse(sql: string): SqlStatement; - compile(statement: SqlStatement): Command[]; -} \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index b0dd507..0000000 --- a/src/parser.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Parser as NodeSqlParser } from 'node-sql-parser'; -import { SqlParser, SqlStatement } from './interfaces'; -import debug from 'debug'; - -const log = debug('queryleaf:parser'); - -// Custom PostgreSQL mode with extensions to support our syntax needs -const CUSTOM_DIALECT = { - name: 'QueryLeafPostgreSQL', - reserved: [ - 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', - 'TABLE', 'DATABASE', 'VIEW', 'INDEX', 'TRIGGER', 'PROCEDURE', 'FUNCTION' - ], - literalTokens: { - // Add handling for array indexing syntax - '[': { tokenType: 'BRACKET_OPEN', regex: /\[/ }, - ']': { tokenType: 'BRACKET_CLOSE', regex: /\]/ }, - '.': { tokenType: 'DOT', regex: /\./ } - }, - operators: [ - // Standard operators - '+', '-', '*', '/', '%', '=', '!=', '<>', '>', '<', '>=', '<=', - // Add nested field operators - '.' - ] -}; - -/** - * SQL Parser implementation using node-sql-parser - */ -export class SqlParserImpl implements SqlParser { - private parser: NodeSqlParser; - - constructor(options?: { database?: string }) { - // Create standard parser with PostgreSQL mode - this.parser = new NodeSqlParser(); - } - - /** - * Parse SQL string into a SqlStatement - * @param sql SQL string to parse - * @returns Parsed SQL statement object - */ - parse(sql: string): SqlStatement { - try { - // First, handle nested dot notation in field access - const preprocessedNestedSql = this.preprocessNestedFields(sql); - - // Then transform array index notation to a form the parser can handle - const preprocessedSql = this.preprocessArrayIndexes(preprocessedNestedSql); - log('Preprocessed SQL:', preprocessedSql); - - // Parse with PostgreSQL mode but try to handle our custom extensions - const ast = this.parser.astify(preprocessedSql, { - database: 'PostgreSQL' - }); - - // Process the AST to properly handle nested fields - const processedAst = this.postProcessAst(ast); - - return { - ast: Array.isArray(processedAst) ? processedAst[0] : processedAst, - text: sql // Use original SQL for reference - }; - } catch (error) { - // If error happens and it's related to our extensions, try to handle it - const errorMessage = error instanceof Error ? error.message : String(error); - - if (errorMessage.includes('[')) { - // Make a more aggressive transformation of the SQL for bracket syntax - const fallbackSql = this.aggressivePreprocessing(sql); - log('Fallback SQL for array syntax:', fallbackSql); - try { - const ast = this.parser.astify(fallbackSql, { database: 'PostgreSQL' }); - const processedAst = this.postProcessAst(ast); - return { - ast: Array.isArray(processedAst) ? processedAst[0] : processedAst, - text: sql - }; - } catch (fallbackErr) { - const fallbackErrorMsg = fallbackErr instanceof Error ? - fallbackErr.message : String(fallbackErr); - throw new Error(`SQL parsing error (fallback): ${fallbackErrorMsg}`); - } - } - - throw new Error(`SQL parsing error: ${errorMessage}`); - } - } - - /** - * Preprocess nested field access in SQL before parsing - * - * This helps ensure that the parser correctly handles nested fields like: - * contact.address.city => becomes a properly parsed reference - * - * For deep nested fields (with more than one dot), we need special handling - * since the SQL parser typically expects table.column format only - */ - private preprocessNestedFields(sql: string): string { - log('Processing nested fields in SQL:', sql); - - // Find deeply nested fields in the WHERE clause (contact.address.city) - // and replace them with a placeholder format that the parser can handle - - // This regex matches multi-level nested fields in WHERE conditions - // It looks for patterns like: WHERE contact.address.city = 'Boston' - const whereNestedFieldRegex = /WHERE\s+([a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+){1,})\s*(=|!=|<>|>|<|>=|<=|LIKE|IN|NOT IN)/gi; - - // Keep track of replacements to restore them later - const replacements: [string, string][] = []; - - // First pass: replace deep nested fields in WHERE clause with placeholders - let processedSql = sql.replace(whereNestedFieldRegex, (match, nestedField, _, operator) => { - // Create a placeholder name - const placeholder = `__NESTED_${replacements.length}__`; - - // Store the replacement - replacements.push([placeholder, nestedField]); - - // Replace with the placeholder - return `WHERE ${placeholder} ${operator}`; - }); - - // Add debug info about replacements - if (replacements.length > 0) { - log('Nested field replacements:', JSON.stringify(replacements, null, 2)); - } - - // Store the replacements in this instance for later use - this._nestedFieldReplacements = replacements; - - return processedSql; - } - - // Store replacements for later reference - private _nestedFieldReplacements: [string, string][] = []; - - /** - * Post-process the AST to correctly handle nested fields - * - * This ensures that expressions like "contact.address.city" are correctly - * recognized as a single column reference rather than a table/column pair. - */ - private postProcessAst(ast: any): any { - // Clone the AST to avoid modifying the original - const processed = JSON.parse(JSON.stringify(ast)); - - // Handle SELECT clause nested fields - this.processSelectClause(processed); - - // Handle WHERE clause nested fields - this.processWhereClause(processed); - - log('Post-processed AST:', JSON.stringify(processed, null, 2)); - return processed; - } - - /** - * Process nested fields in the SELECT clause - */ - private processSelectClause(ast: any): void { - if (!ast || (!Array.isArray(ast) && typeof ast !== 'object')) return; - - // Handle array of statements - if (Array.isArray(ast)) { - ast.forEach(item => this.processSelectClause(item)); - return; - } - - // Only process SELECT statements - if (ast.type !== 'select' || !ast.columns) return; - - // Process each column in the SELECT list - ast.columns.forEach((column: any) => { - if (column.expr && column.expr.type === 'column_ref') { - // If the column has table.field notation, check if it should be a nested field - if (column.expr.table && column.expr.column && - !this.isActualTableReference(column.expr.table, ast)) { - // It's likely a nested field, not a table reference - column.expr.column = `${column.expr.table}.${column.expr.column}`; - column.expr.table = null; - } - } - }); - } - - /** - * Process nested fields in the WHERE clause - */ - private processWhereClause(ast: any): void { - if (!ast || (!Array.isArray(ast) && typeof ast !== 'object')) return; - - // Handle array of statements - if (Array.isArray(ast)) { - ast.forEach(item => this.processWhereClause(item)); - return; - } - - // No WHERE clause to process - if (!ast.where) return; - - // Process the WHERE clause recursively - this.processWhereExpr(ast.where, ast); - } - - /** - * Process WHERE expression recursively to handle nested fields - */ - private processWhereExpr(expr: any, ast: any): void { - if (!expr || typeof expr !== 'object') return; - - if (expr.type === 'binary_expr') { - // Process both sides of binary expressions - this.processWhereExpr(expr.left, ast); - this.processWhereExpr(expr.right, ast); - - // Check for column references in the left side of the expression - if (expr.left && expr.left.type === 'column_ref') { - // First, check if this is a placeholder that needs to be restored - if (expr.left.column && expr.left.column.startsWith('__NESTED_') && - expr.left.column.endsWith('__')) { - // Find the corresponding replacement - const placeholderIndex = parseInt(expr.left.column.replace('__NESTED_', '').replace('__', '')); - if (this._nestedFieldReplacements.length > placeholderIndex) { - // Restore the original nested field name - const [_, originalField] = this._nestedFieldReplacements[placeholderIndex]; - log(`Restoring nested field: ${expr.left.column} -> ${originalField}`); - expr.left.column = originalField; - expr.left.table = null; - } - } - // Then check for table.column notation that should be a nested field - else if (expr.left.table && expr.left.column && - !this.isActualTableReference(expr.left.table, ast)) { - // Likely a nested field access, not a table reference - expr.left.column = `${expr.left.table}.${expr.left.column}`; - expr.left.table = null; - } - } - } else if (expr.type === 'unary_expr') { - // Process the expression in unary operators - this.processWhereExpr(expr.expr, ast); - } - } - - /** - * Check if a name is an actual table reference in the FROM clause - * - * This helps distinguish between table.column notation and nested field access - */ - private isActualTableReference(name: string, ast: any): boolean { - if (!ast.from || !Array.isArray(ast.from)) return false; - - // Check if the name appears as a table name or alias in the FROM clause - return ast.from.some((fromItem: any) => { - return (fromItem.table === name) || (fromItem.as === name); - }); - } - - /** - * Preprocess SQL to transform array index notation into a form the parser can handle - * - * This transforms: - * items[0].name => items__ARRAY_0__name - * - * We'll convert it back to MongoDB's dot notation later in the compiler. - */ - private preprocessArrayIndexes(sql: string): string { - // Replace array index notation with a placeholder format - // This regex matches field references with array indexes like items[0] or items[0].name - return sql.replace(/(\w+)\[(\d+)\](\.\w+)?/g, (match, field, index, suffix) => { - if (suffix) { - // For nested access like items[0].name => items__ARRAY_0__name - return `${field}__ARRAY_${index}__${suffix.substring(1)}`; - } else { - // For simple array access like items[0] => items__ARRAY_0 - return `${field}__ARRAY_${index}`; - } - }); - } - - /** - * More aggressive preprocessing for SQL that contains array syntax - * This completely removes the array indexing and replaces it with a special column naming pattern - */ - private aggressivePreprocessing(sql: string): string { - // Replace items[0].name with items_0_name - // This is a more aggressive approach that completely avoids bracket syntax - return sql.replace(/(\w+)\[(\d+)\](\.(\w+))?/g, (match, field, index, dotPart, subfield) => { - if (subfield) { - return `${field}_${index}_${subfield}`; - } else { - return `${field}_${index}`; - } - }); - } -} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index d8a8eec..0000000 --- a/src/server.ts +++ /dev/null @@ -1,673 +0,0 @@ -import express, { Request, Response } from 'express'; -import { MongoClient } from 'mongodb'; -import { QueryLeaf } from './index'; -import bodyParser from 'body-parser'; -import cors from 'cors'; -import morgan from 'morgan'; -import helmet from 'helmet'; -import { rateLimit } from 'express-rate-limit'; -import swaggerUi from 'swagger-ui-express'; -import path from 'path'; -import fs from 'fs'; - -// Config options -interface ServerConfig { - port: number; - mongoUri: string; - databaseName: string; - logFormat: string; - enableCors: boolean; - corsOrigin: string; - apiRateLimit: number; - apiRateLimitWindow: number; // in minutes - swaggerEnabled: boolean; -} - -// Initialize config with defaults -const config: ServerConfig = { - port: parseInt(process.env.PORT || '3000', 10), - mongoUri: process.env.MONGO_URI || 'mongodb://localhost:27017', - databaseName: process.env.MONGO_DB || '', - logFormat: process.env.LOG_FORMAT || 'dev', - enableCors: process.env.ENABLE_CORS !== 'false', - corsOrigin: process.env.CORS_ORIGIN || '*', - apiRateLimit: parseInt(process.env.API_RATE_LIMIT || '100', 10), - apiRateLimitWindow: parseInt(process.env.API_RATE_LIMIT_WINDOW || '15', 10), - swaggerEnabled: process.env.SWAGGER_ENABLED !== 'false', -}; - -// Swagger documentation -const swaggerDocument = { - openapi: '3.0.0', - info: { - title: 'QueryLeaf API', - version: '1.0.0', - description: 'API for executing SQL queries against MongoDB', - }, - servers: [ - { - url: '/', - description: 'Current server', - }, - ], - paths: { - '/api/query': { - post: { - summary: 'Execute a SQL query', - description: 'Execute a SQL query against the MongoDB database', - operationId: 'executeSqlQuery', - requestBody: { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - required: ['sql'], - properties: { - sql: { - type: 'string', - description: 'SQL query to execute', - example: 'SELECT * FROM users LIMIT 5', - }, - }, - }, - }, - }, - }, - responses: { - '200': { - description: 'Query executed successfully', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - results: { - type: 'array', - items: { - type: 'object', - }, - }, - rowCount: { - type: 'integer', - description: 'Number of rows returned', - }, - executionTime: { - type: 'integer', - description: 'Execution time in milliseconds', - }, - }, - }, - }, - }, - }, - '400': { - description: 'Bad request - Invalid SQL query', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - description: 'Error message', - }, - }, - }, - }, - }, - }, - '500': { - description: 'Server error', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - description: 'Error message', - }, - }, - }, - }, - }, - }, - }, - }, - }, - '/api/tables': { - get: { - summary: 'List available collections', - description: 'Get a list of all collections in the MongoDB database', - operationId: 'listTables', - responses: { - '200': { - description: 'Collections retrieved successfully', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - collections: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - '500': { - description: 'Server error', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - description: 'Error message', - }, - }, - }, - }, - }, - }, - }, - }, - }, - '/api/health': { - get: { - summary: 'Health check', - description: 'Check if the API is running and connected to MongoDB', - operationId: 'healthCheck', - responses: { - '200': { - description: 'API is healthy', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - status: { - type: 'string', - enum: ['ok'], - }, - mongodb: { - type: 'string', - enum: ['connected', 'disconnected'], - }, - }, - }, - }, - }, - }, - '500': { - description: 'API is unhealthy', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - status: { - type: 'string', - enum: ['error'], - }, - mongodb: { - type: 'string', - enum: ['disconnected'], - }, - error: { - type: 'string', - description: 'Error message', - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; - -// Rate limiter middleware -const limiter = rateLimit({ - windowMs: config.apiRateLimitWindow * 60 * 1000, // in milliseconds - limit: config.apiRateLimit, - standardHeaders: 'draft-7', - legacyHeaders: false, -}); - -// Main function to start server -export async function startServer(customConfig?: Partial): Promise<{ app: express.Application; server: any }> { - // Apply custom config if provided - const serverConfig = { ...config, ...customConfig }; - - // Validate database name - if (!serverConfig.databaseName) { - throw new Error('MongoDB database name is required. Set MONGO_DB environment variable or provide in custom config.'); - } - - // Create Express app - const app = express(); - - // Apply middleware - app.use(helmet()); // Security headers - app.use(morgan(serverConfig.logFormat)); // Logging - app.use(bodyParser.json()); // Parse JSON requests - - // Apply CORS if enabled - if (serverConfig.enableCors) { - app.use(cors({ - origin: serverConfig.corsOrigin, - methods: ['GET', 'POST'], - allowedHeaders: ['Content-Type', 'Authorization'], - })); - } - - // Apply rate limiter to API routes - app.use('/api/', limiter); - - // Connect to MongoDB - const mongoClient = new MongoClient(serverConfig.mongoUri); - await mongoClient.connect(); - console.log(`Connected to MongoDB: ${serverConfig.mongoUri}`); - - // Create QueryLeaf instance - const queryLeaf = new QueryLeaf(mongoClient, serverConfig.databaseName); - console.log(`Using database: ${serverConfig.databaseName}`); - - // Health check endpoint - app.get('/api/health', async (req: Request, res: Response) => { - try { - // Check MongoDB connection - await mongoClient.db('admin').command({ ping: 1 }); - - res.json({ - status: 'ok', - mongodb: 'connected', - version: '1.0.0', - database: serverConfig.databaseName, - }); - } catch (error) { - res.status(500).json({ - status: 'error', - mongodb: 'disconnected', - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - }); - - // List tables (collections) endpoint - app.get('/api/tables', async (req: Request, res: Response) => { - try { - const collections = await mongoClient - .db(serverConfig.databaseName) - .listCollections() - .toArray(); - - res.json({ - collections: collections.map(collection => collection.name), - }); - } catch (error) { - res.status(500).json({ - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - }); - - // Execute SQL query endpoint - app.post('/api/query', async (req: Request, res: Response) => { - const { sql } = req.body; - - if (!sql || typeof sql !== 'string') { - return res.status(400).json({ - error: 'SQL query is required in the request body', - }); - } - - try { - const startTime = Date.now(); - const results = await queryLeaf.execute(sql); - const executionTime = Date.now() - startTime; - - res.json({ - results, - rowCount: Array.isArray(results) ? results.length : (results ? 1 : 0), - executionTime, - }); - } catch (error) { - res.status(400).json({ - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - }); - - // Serve Swagger documentation if enabled - if (serverConfig.swaggerEnabled) { - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - console.log('Swagger API documentation available at /api-docs'); - } - - // Simple UI for testing queries - const uiHtml = ` - - - - - - QueryLeaf SQL UI - - - - - - -
-
-
-
SELECT * FROM users LIMIT 10;
-
-
-
- - -
-
-
Collections
-
-
-
- Loading... -
-
-
-
-
-
- -
-
- -
-
- Results - -
-
-
-
-
-
-

No results to display

-
-
-
-
-
-
- - - - - - - `; - - // Serve UI - app.get('/', (req: Request, res: Response) => { - res.setHeader('Content-Type', 'text/html'); - res.send(uiHtml); - }); - - // Start the server - const server = app.listen(serverConfig.port, () => { - console.log(`QueryLeaf SQL server running on port ${serverConfig.port}`); - console.log(`Web UI available at http://localhost:${serverConfig.port}/`); - - if (serverConfig.swaggerEnabled) { - console.log(`API documentation available at http://localhost:${serverConfig.port}/api-docs`); - } - }); - - // Handle graceful shutdown - process.on('SIGTERM', async () => { - console.log('SIGTERM received, shutting down gracefully'); - await mongoClient.close(); - server.close(() => { - console.log('Server closed'); - process.exit(0); - }); - }); - - process.on('SIGINT', async () => { - console.log('SIGINT received, shutting down gracefully'); - await mongoClient.close(); - server.close(() => { - console.log('Server closed'); - process.exit(0); - }); - }); - - return { app, server }; -} - -// Run the server if this file is executed directly -if (require.main === module) { - startServer().catch(error => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} \ No newline at end of file diff --git a/tests.old/cli/cli.test.ts b/tests.old/cli/cli.test.ts deleted file mode 100644 index 460ca5e..0000000 --- a/tests.old/cli/cli.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { spawnSync } from 'child_process'; -import { join } from 'path'; -import { existsSync } from 'fs'; - -describe('CLI tests', () => { - // Very basic test for CLI functionality - it('CLI imports should compile correctly', () => { - // If this test can run, it means the imports are working - expect(true).toBe(true); - }); - - const cliBinPath = join(__dirname, '../../packages/cli/bin/queryleaf'); - - // Skip actual CLI tests if the binary doesn't exist yet - const binExists = existsSync(cliBinPath); - - (binExists ? it : it.skip)('should display help message', () => { - const cli = spawnSync(cliBinPath, ['--help']); - const output = cli.stdout.toString(); - - expect(cli.status).toBe(0); - expect(output).toContain('Usage:'); - expect(output).toContain('Options:'); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/array-access.integration.test.ts b/tests.old/integration/array-access.integration.test.ts deleted file mode 100644 index 7c24883..0000000 --- a/tests.old/integration/array-access.integration.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; - -const log = createLogger('array-access'); - -describe('Array Access Integration Tests', () => { - beforeAll(async () => { - await testSetup.init(); - }, 30000); // 30 second timeout for container startup - - afterAll(async () => { - await testSetup.cleanup(); - }); - - beforeEach(async () => { - // Add test data for array access - const db = testSetup.getDb(); - await db.collection('order_items').deleteMany({}); - }); - - afterEach(async () => { - // Clean up test data - const db = testSetup.getDb(); - await db.collection('order_items').deleteMany({}); - }); - - test('should handle array access syntax for nested field access in queries', async () => { - // Arrange: Insert test data with arrays - keep it very simple - const db = testSetup.getDb(); - await db.collection('order_items').insertOne({ - orderId: 'ORD-1001', - items: [ - { name: 'Widget', price: 10.99 }, - { name: 'Gadget', price: 24.99 } - ] - }); - - // Act: Execute query accessing just the first array element - const queryLeaf = testSetup.getQueryLeaf(); - // Use the __ARRAY_ syntax that Squongo expects for array access - const sql = ` - SELECT - orderId - FROM order_items - WHERE items__ARRAY_0__name = 'Widget' - `; - - const results = await queryLeaf.execute(sql); - log('Array access filter results:', JSON.stringify(results, null, 2)); - - // Assert: Verify that filtering by array element works - // Since the filtering might be handled differently by different implementations, - // we'll just check if we get at least one result with the correct orderId - expect(results.length).toBeGreaterThan(0); - const hasCorrectOrder = results.some((r: any) => r.orderId === 'ORD-1001'); - expect(hasCorrectOrder).toBe(true); - }); - - test('should filter by array element properties at different indices', async () => { - // Arrange: Insert test data with arrays - const db = testSetup.getDb(); - await db.collection('order_items').insertMany([ - { - orderId: 'ORD-1001', - items: [ - { id: 'ITEM-1', name: 'Widget', price: 10.99, inStock: true }, - { id: 'ITEM-2', name: 'Gadget', price: 24.99, inStock: false } - ] - }, - { - orderId: 'ORD-1002', - items: [ - { id: 'ITEM-3', name: 'Tool', price: 15.50, inStock: true }, - { id: 'ITEM-4', name: 'Device', price: 99.99, inStock: true } - ] - }, - { - orderId: 'ORD-1003', - items: [ - { id: 'ITEM-5', name: 'Widget', price: 11.99, inStock: false }, - { id: 'ITEM-6', name: 'Gizmo', price: 34.99, inStock: true } - ] - } - ]); - - // Act: Execute query filtering on different array indices - const queryLeaf = testSetup.getQueryLeaf(); - // Try alternate syntax for array access - the implementation might support - // either items.0.name or items__ARRAY_0__name syntax - const sql = ` - SELECT orderId - FROM order_items - WHERE items__ARRAY_0__name = 'Widget' AND items__ARRAY_1__inStock = true - `; - - const results = await queryLeaf.execute(sql); - log('Array indices filtering results:', JSON.stringify(results, null, 2)); - - // Assert: Verify only the order with Widget as first item and inStock=true for second item - // Since the filtering might be handled differently, we'll check if ORD-1003 is in the results - const hasOrder1003 = results.some((r: any) => r.orderId === 'ORD-1003'); - expect(hasOrder1003).toBe(true); - }); - - test('should query arrays with multiple indices', async () => { - // Arrange: Insert test data with larger arrays - const db = testSetup.getDb(); - await db.collection('order_items').insertMany([ - { - orderId: 'ORD-2001', - items: [ - { id: 'ITEM-A1', name: 'Widget', price: 10.99, category: 'Tools' }, - { id: 'ITEM-A2', name: 'Gadget', price: 24.99, category: 'Electronics' }, - { id: 'ITEM-A3', name: 'Accessory', price: 5.99, category: 'Misc' } - ] - }, - { - orderId: 'ORD-2002', - items: [ - { id: 'ITEM-B1', name: 'Tool', price: 15.50, category: 'Tools' }, - { id: 'ITEM-B2', name: 'Device', price: 99.99, category: 'Electronics' }, - { id: 'ITEM-B3', name: 'Widget', price: 12.99, category: 'Tools' } - ] - } - ]); - - // First verify with a direct MongoDB query to confirm the data structure - const directQueryResult = await db.collection('order_items').findOne({ orderId: 'ORD-2001' }); - log('Direct MongoDB query result:', JSON.stringify(directQueryResult, null, 2)); - - // Execute the query through QueryLeaf - const queryLeaf = testSetup.getQueryLeaf(); - const sql = ` - SELECT * - FROM order_items - WHERE orderId = 'ORD-2001' - `; - - const results = await queryLeaf.execute(sql); - log('Order items query results:', JSON.stringify(results, null, 2)); - - // Basic validation - expect(results.length).toBe(1); - - // Check that the result is for ORD-2001 - const order = results[0]; - expect(order.orderId).toBe('ORD-2001'); - - // Helper function to safely access arrays with different possible structures - const getItems = (result: any): any[] => { - if (Array.isArray(result.items)) return result.items; - if (result._doc && Array.isArray(result._doc.items)) return result._doc.items; - return []; - }; - - // Get the items array - const items = getItems(order); - expect(Array.isArray(items)).toBe(true); - expect(items.length).toBe(3); - - // Verify each item in the array has the correct structure and values - if (items.length >= 3) { - // First item - expect(items[0].id).toBe('ITEM-A1'); - expect(items[0].name).toBe('Widget'); - expect(items[0].category).toBe('Tools'); - expect(Math.abs(items[0].price - 10.99)).toBeLessThan(0.01); - - // Second item - expect(items[1].id).toBe('ITEM-A2'); - expect(items[1].name).toBe('Gadget'); - expect(items[1].category).toBe('Electronics'); - expect(Math.abs(items[1].price - 24.99)).toBeLessThan(0.01); - - // Third item - expect(items[2].id).toBe('ITEM-A3'); - expect(items[2].name).toBe('Accessory'); - expect(items[2].category).toBe('Misc'); - expect(Math.abs(items[2].price - 5.99)).toBeLessThan(0.01); - } - - // Now test a more complex query that accesses array elements by index - // This tests the array access functionality more directly - const indexAccessSql = ` - SELECT orderId - FROM order_items - WHERE items__ARRAY_1__category = 'Electronics' - `; - - const indexResults = await queryLeaf.execute(indexAccessSql); - log('Array index access results:', JSON.stringify(indexResults, null, 2)); - - // Verify we can find orders by array index properties - expect(indexResults.length).toBeGreaterThan(0); - - // Both orders have Electronics as the second item's category - const orderIds = indexResults.map((r: any) => r.orderId); - expect(orderIds).toContain('ORD-2001'); - expect(orderIds).toContain('ORD-2002'); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/edge-cases.integration.test.ts b/tests.old/integration/edge-cases.integration.test.ts deleted file mode 100644 index e2764d2..0000000 --- a/tests.old/integration/edge-cases.integration.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; - -const log = createLogger('edge-cases'); - -describe('Edge Cases Integration Tests', () => { - beforeAll(async () => { - await testSetup.init(); - }, 30000); // 30 second timeout for container startup - - afterAll(async () => { - // Make sure to close any outstanding connections - const queryLeaf = testSetup.getQueryLeaf(); - - // Clean up any resources that squongo might be using - if (typeof queryLeaf.close === 'function') { - await queryLeaf.close(); - } - - // Clean up test setup resources - await testSetup.cleanup(); - }, 10000); // Give it more time to clean up - - beforeEach(async () => { - // Clean up collections before each test - const db = testSetup.getDb(); - await db.collection('edge_test').deleteMany({}); - await db.collection('missing_collection').deleteMany({}); - }); - - afterEach(async () => { - // Clean up collections after each test - const db = testSetup.getDb(); - await db.collection('edge_test').deleteMany({}); - }); - - test('should handle special characters in field names', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('edge_test').insertMany([ - { 'field_with_underscores': 'value1', name: 'item1' }, - { 'field_with_numbers123': 'value2', name: 'item2' }, - { 'UPPERCASE_FIELD': 'value3', name: 'item3' }, - { 'mixedCaseField': 'value4', name: 'item4' }, - { 'snake_case_field': 'value5', name: 'item5' } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - // Since SQL parsers often have issues with special characters, we'll use identifiers that are more likely - // to be supported by most SQL parsers - const sql = 'SELECT name, field_with_underscores FROM edge_test WHERE field_with_underscores = "value1"'; - - const results = await queryLeaf.execute(sql); - - // Assert - expect(results).toHaveLength(1); - expect(results[0].name).toBe('item1'); - expect(results[0].field_with_underscores).toBe('value1'); - }); - - test('should gracefully handle invalid SQL syntax', async () => { - // Arrange - const queryLeaf = testSetup.getQueryLeaf(); - const invalidSql = 'SELECT FROM users WHERE;'; // Missing column and invalid WHERE clause - - // Act & Assert - await expect(queryLeaf.execute(invalidSql)).rejects.toThrow(); - }); - - test('should gracefully handle valid SQL but unsupported features', async () => { - // Arrange - const queryLeaf = testSetup.getQueryLeaf(); - // SQL with PIVOT which is not widely supported in most SQL implementations - const unsupportedSql = 'SELECT * FROM (SELECT category, price FROM products) PIVOT (SUM(price) FOR category IN ("Electronics", "Furniture"))'; - - // Act & Assert - await expect(queryLeaf.execute(unsupportedSql)).rejects.toThrow(); - }); - - test('should handle behavior with missing collections', async () => { - // Arrange - const queryLeaf = testSetup.getQueryLeaf(); - const sql = 'SELECT * FROM nonexistent_collection'; - - // Act - const results = await queryLeaf.execute(sql); - - // Assert - should return empty array rather than throwing an error - expect(Array.isArray(results)).toBe(true); - expect(results).toHaveLength(0); - }); - - test('should handle invalid data types appropriately', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('edge_test').insertMany([ - { name: 'item1', value: 123 }, - { name: 'item2', value: 'not a number' } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - // Try to do numerical comparison on non-numeric data - const sql = 'SELECT name FROM edge_test WHERE value > 100'; - - const results = await queryLeaf.execute(sql); - - // Assert - should only find the numeric value that's valid for comparison - expect(results).toHaveLength(1); - expect(results[0].name).toBe('item1'); - }); - - test('should handle MongoDB ObjectId conversions', async () => { - // Arrange - const db = testSetup.getDb(); - const objectId = new ObjectId(); - await db.collection('edge_test').insertOne({ - _id: objectId, - name: 'ObjectId Test' - }); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - // Use the string representation of ObjectId in SQL - const sql = `SELECT name FROM edge_test WHERE _id = '${objectId.toString()}'`; - - const results = await queryLeaf.execute(sql); - - // Assert - expect(results).toHaveLength(1); - expect(results[0].name).toBe('ObjectId Test'); - }); - - test('should handle extremely large result sets', async () => { - // Arrange - const db = testSetup.getDb(); - const largeDataset = Array.from({ length: 1000 }, (_, i) => ({ - index: i, - name: `Item ${i}`, - value: Math.random() * 1000 - })); - - await db.collection('edge_test').insertMany(largeDataset); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = 'SELECT * FROM edge_test'; - - const results = await queryLeaf.execute(sql); - - // Assert - expect(results).toHaveLength(1000); - expect(results[0]).toHaveProperty('index'); - expect(results[0]).toHaveProperty('name'); - expect(results[0]).toHaveProperty('value'); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/group-by.integration.test.ts b/tests.old/integration/group-by.integration.test.ts deleted file mode 100644 index 320c687..0000000 --- a/tests.old/integration/group-by.integration.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; - -const log = createLogger('group-by'); - -interface GroupData { - category: string; - region: string; - year: number; - count: number; - total: number; -} - -describe('GROUP BY Integration Tests', () => { - beforeAll(async () => { - await testSetup.init(); - }, 30000); // 30 second timeout for container startup - - afterAll(async () => { - await testSetup.cleanup(); - }); - - beforeEach(async () => { - // Clean up test data - const db = testSetup.getDb(); - await db.collection('sales').deleteMany({}); - }); - - afterEach(async () => { - // Clean up test data - const db = testSetup.getDb(); - await db.collection('sales').deleteMany({}); - }); - - // Simplify to a single test that just checks the basic GROUP BY functionality - test('should execute a simple GROUP BY query with count', async () => { - // Arrange: Insert test data - const db = testSetup.getDb(); - - await db.collection('sales').insertMany([ - { category: 'A', region: 'North' }, - { category: 'A', region: 'North' }, - { category: 'A', region: 'South' }, - { category: 'B', region: 'North' }, - { category: 'B', region: 'South' } - ]); - - // Act: First make a direct MongoDB query to compare with - const directAggregateResult = await db.collection('sales').aggregate([ - { $group: { _id: "$category", count: { $sum: 1 } } } - ]).toArray(); - - log('Direct MongoDB aggregate result:', JSON.stringify(directAggregateResult, null, 2)); - - // Get categories and counts for direct verification - const directCounts = new Map(); - - for (const result of directAggregateResult) { - if (result._id) { - directCounts.set(result._id, result.count || 0); - } - } - - log('Direct MongoDB counts:', Object.fromEntries(directCounts.entries())); - - // Execute SQL query with QueryLeaf - const queryLeaf = testSetup.getQueryLeaf(); - const sql = ` - SELECT - category, - COUNT(*) as count - FROM sales - GROUP BY category - `; - - const results = await queryLeaf.execute(sql); - log('Simple GROUP BY results:', JSON.stringify(results, null, 2)); - - // Basic verification - check we have results - expect(results.length).toBeGreaterThan(0); - - // Simplify our verification - check that we have both A and B categories - // somewhere in the results, without worrying about exact format - const categoryAExists = results.some((r: any) => { - // Try all possible locations for category values - return (r.category === 'A') || - (r._id === 'A') || - (r._id && r._id.category === 'A'); - }); - - const categoryBExists = results.some((r: any) => { - // Try all possible locations for category values - return (r.category === 'B') || - (r._id === 'B') || - (r._id && r._id.category === 'B'); - }); - - // Verify both categories exist - expect(categoryAExists).toBe(true); - expect(categoryBExists).toBe(true); - - // Find count values for approximate comparison - const countA = results.find((r: any) => - (r.category === 'A') || (r._id === 'A') || (r._id && r._id.category === 'A') - ); - - const countB = results.find((r: any) => - (r.category === 'B') || (r._id === 'B') || (r._id && r._id.category === 'B') - ); - - // Just verify we have count info in some form - expect(countA).toBeDefined(); - expect(countB).toBeDefined(); - - // Check we match what's directly in the database - expect(directCounts.has('A')).toBe(true); - expect(directCounts.has('B')).toBe(true); - - // Basic verification - A should have more than B based on our test data - expect(directCounts.get('A')).toBeGreaterThan(directCounts.get('B') || 0); - }); - - test('should execute GROUP BY with multiple columns', async () => { - // Arrange: Insert test data with multiple dimensions to group by - const db = testSetup.getDb(); - - await db.collection('sales').insertMany([ - { category: 'Electronics', region: 'North', year: 2022, amount: 1200 }, - { category: 'Electronics', region: 'North', year: 2022, amount: 800 }, - { category: 'Electronics', region: 'South', year: 2022, amount: 1500 }, - { category: 'Electronics', region: 'North', year: 2023, amount: 1300 }, - { category: 'Electronics', region: 'South', year: 2023, amount: 900 }, - { category: 'Clothing', region: 'North', year: 2022, amount: 600 }, - { category: 'Clothing', region: 'South', year: 2022, amount: 700 }, - { category: 'Clothing', region: 'North', year: 2023, amount: 550 }, - { category: 'Clothing', region: 'South', year: 2023, amount: 650 } - ]); - - // First do a direct MongoDB query to get accurate aggregation results - const directAggregation = await db.collection('sales').aggregate([ - { - $group: { - _id: { - category: "$category", - region: "$region", - year: "$year" - }, - count: { $sum: 1 }, - totalAmount: { $sum: "$amount" } - } - } - ]).toArray(); - - log('Direct MongoDB aggregation:', JSON.stringify(directAggregation, null, 2)); - - // Get direct results into an expected structure - const directCombinations = new Set(); - const directTotalsByCategory = new Map(); - - // Process direct results - for (const result of directAggregation) { - if (result._id && typeof result._id === 'object') { - const category = result._id.category; - const region = result._id.region; - const year = result._id.year; - - if (category && region && year) { - // Add to combinations set - directCombinations.add(`${category}|${region}|${year}`); - - // Add to category totals - if (!directTotalsByCategory.has(category)) { - directTotalsByCategory.set(category, 0); - } - directTotalsByCategory.set( - category, - (directTotalsByCategory.get(category) || 0) + (result.totalAmount || 0) - ); - } - } - } - - log('Direct combinations:', Array.from(directCombinations)); - log('Direct totals by category:', Object.fromEntries(directTotalsByCategory.entries())); - - // Run the SQL through QueryLeaf - const multiQueryLeaf = testSetup.getQueryLeaf(); - const multiSql = ` - SELECT - category, - region, - year, - COUNT(*) as transaction_count, - SUM(amount) as total_sales - FROM sales - GROUP BY category, region, year - `; - - const multiResults = await multiQueryLeaf.execute(multiSql); - log('Multi-column GROUP BY results:', JSON.stringify(multiResults, null, 2)); - - // Create a more resilient verification that's simpler - // Check the basics: we have results - expect(multiResults.length).toBeGreaterThan(0); - - // Very basic check - at minimum we should find one result with Electronics - const hasElectronics = multiResults.some((r: any) => { - const category = - (r.category === 'Electronics') || - (r._id === 'Electronics') || - (r._id && r._id.category === 'Electronics'); - return category; - }); - - const hasClothing = multiResults.some((r: any) => { - const category = - (r.category === 'Clothing') || - (r._id === 'Clothing') || - (r._id && r._id.category === 'Clothing'); - return category; - }); - - // Verify that we have at least Electronics and Clothing categories - expect(hasElectronics).toBe(true); - expect(hasClothing).toBe(true); - - // Verify that the database itself contains the expected data - // using our direct MongoDB query results - - // Check that we have all 8 combinations from our direct query - expect(directCombinations.size).toBe(8); - - // Check that Electronics has more total amount than Clothing - const directElectronicsTotal = directTotalsByCategory.get('Electronics') || 0; - const directClothingTotal = directTotalsByCategory.get('Clothing') || 0; - - expect(directElectronicsTotal).toBeGreaterThan(directClothingTotal); - - // Electronics should have 4 data points (4 combinations of region/year) - expect(Array.from(directCombinations).filter(c => c.startsWith('Electronics')).length).toBe(4); - - // Clothing should have 4 data points (4 combinations of region/year) - expect(Array.from(directCombinations).filter(c => c.startsWith('Clothing')).length).toBe(4); - - // Check for specific groups we previously tested - just verify they exist - // instead of checking exact counts and totals which are harder to verify - expect(directCombinations.has('Electronics|North|2022')).toBe(true); - expect(directCombinations.has('Clothing|South|2023')).toBe(true); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/integration.integration.test.ts b/tests.old/integration/integration.integration.test.ts deleted file mode 100644 index 8741219..0000000 --- a/tests.old/integration/integration.integration.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders } from '../utils/mongo-container'; -import { ObjectId } from 'mongodb'; -import { QueryLeaf } from '../../src/index'; -import { createLogger } from './test-setup'; - -const log = createLogger('integration'); - -describe('QueryLeaf Integration Tests', () => { - const mongoContainer = new MongoTestContainer(); - const TEST_DB = 'queryleaf_test'; - - // Set up MongoDB container before all tests - beforeAll(async () => { - await mongoContainer.start(); - const db = mongoContainer.getDatabase(TEST_DB); - await loadFixtures(db); - }, 30000); // 30 second timeout for container startup - - // Stop MongoDB container after all tests - afterAll(async () => { - await mongoContainer.stop(); - }); - - // Create a new QueryLeaf instance for each test - const getQueryLeaf = () => { - const client = mongoContainer.getClient(); - return new QueryLeaf(client, TEST_DB); - }; - - describe('SELECT queries', () => { - test('should execute a simple SELECT *', async () => { - // First run a command to check what's in the collection - const db = mongoContainer.getDatabase(TEST_DB); - const usersInDb = await db.collection('users').find().toArray(); - log('Users in DB:', JSON.stringify(usersInDb, null, 2)); - - const queryLeaf = getQueryLeaf(); - const sql = 'SELECT * FROM users'; - - log('Executing SQL:', sql); - const results = await queryLeaf.execute(sql); - log('Results:', JSON.stringify(results, null, 2)); - - expect(results).toHaveLength(testUsers.length); - expect(results[0]).toHaveProperty('name'); - expect(results[0]).toHaveProperty('age'); - }); - - // Add a user with nested fields for testing - test('should handle nested fields in queries', async () => { - // First add a user with address info and verify insertion - const db = mongoContainer.getDatabase(TEST_DB); - const insertResult = await db.collection('users').insertOne({ - name: 'Nested User', - age: 40, - email: 'nested@example.com', - address: { - street: '123 Main St', - city: 'Boston', - state: 'MA', - zip: '02108' - } - }); - log('Inserted nested user with ID:', insertResult.insertedId); - - // Verify the insertion directly - const insertedUser = await db.collection('users').findOne({ name: 'Nested User' }); - log('Inserted user found directly:', JSON.stringify(insertedUser, null, 2)); - - const queryLeaf = getQueryLeaf(); - // We need to first test if we can find the user at all - const findSql = "SELECT * FROM users WHERE name = 'Nested User'"; - const findResults = await queryLeaf.execute(findSql); - log('Find by name results:', JSON.stringify(findResults, null, 2)); - - // Now test the nested fields - use a format that works better with the MongoDB projection - const sql = "SELECT name, address FROM users WHERE name = 'Nested User'"; - const results = await queryLeaf.execute(sql); - log('Nested field results:', JSON.stringify(results, null, 2)); - - expect(results.length).toBeGreaterThan(0); - expect(results[0]).toHaveProperty('name', 'Nested User'); - expect(results[0]).toHaveProperty('address'); - // The address might be returned differently depending on MongoDB's projection handling - const address = results[0].address; - expect(address.zip || address.zip || (address && typeof address === 'object' && 'zip' in address ? address.zip : null)).toBe('02108'); - }); - - test('should execute a SELECT with WHERE condition', async () => { - const queryLeaf = getQueryLeaf(); - const sql = 'SELECT * FROM users WHERE age > 20'; - - const results = await queryLeaf.execute(sql); - - expect(results.length).toBeGreaterThan(0); - expect(results.length).toBeLessThan(testUsers.length); - expect(results.every((user: any) => user.age > 20)).toBe(true); - }); - - test('should execute a SELECT with column projection', async () => { - const queryLeaf = getQueryLeaf(); - const sql = 'SELECT name, email FROM users'; - - const results = await queryLeaf.execute(sql); - - // We have added a 'Nested User' in a previous test, so we'll have more than the original test users - expect(results.length).toBeGreaterThanOrEqual(testUsers.length); - results.forEach((user: any) => { - expect(user).toHaveProperty('name'); - expect(user).toHaveProperty('email'); - expect(user).not.toHaveProperty('age'); - }); - }); - - test('should execute a SELECT with filtering by equality', async () => { - const queryLeaf = getQueryLeaf(); - const sql = "SELECT * FROM products WHERE category = 'Electronics'"; - - const results = await queryLeaf.execute(sql); - - expect(results.length).toBeGreaterThan(0); - expect(results.every((product: any) => product.category === 'Electronics')).toBe(true); - }); - - test('should execute a SELECT with complex WHERE conditions', async () => { - const queryLeaf = getQueryLeaf(); - const sql = "SELECT * FROM users WHERE age >= 25 AND active = true"; - - const results = await queryLeaf.execute(sql); - - expect(results.length).toBeGreaterThan(0); - expect(results.every((user: any) => user.age >= 25 && user.active === true)).toBe(true); - }); - - test('should handle array element access', async () => { - // Insert an order with item array - const db = mongoContainer.getDatabase(TEST_DB); - await db.collection('orders').insertOne({ - userId: new ObjectId(), - totalAmount: 150, - items: [ - { id: 'item1', name: 'First Item', price: 50 }, - { id: 'item2', name: 'Second Item', price: 100 } - ], - status: 'Pending' - }); - - const queryLeaf = getQueryLeaf(); - - // Just fetch the entire document - const sql = "SELECT * FROM orders"; - - const results = await queryLeaf.execute(sql); - - // Log the results to see the structure - log('Array access results:', JSON.stringify(results, null, 2)); - - // Check we have a valid result - expect(results.length).toBeGreaterThan(0); - expect(results[0]).toHaveProperty('userId'); - - // Check if items is present - MongoDB might have different projection behavior - const hasItems = results[0].hasOwnProperty('items') || - (results[0]._doc && results[0]._doc.hasOwnProperty('items')); - - // Instead of strict array testing, just verify we can access the data - expect(hasItems || results.some((r: any) => r.hasOwnProperty('items'))).toBeTruthy(); - - // We only need to check that the items are accessible - }); - - // Test deep nested fields and array indexing - test('should handle deep nested fields and array indexing', async () => { - // Insert test data with deep nested fields and arrays - const db = mongoContainer.getDatabase(TEST_DB); - await db.collection('complex_data').insertOne({ - name: 'Complex Object', - metadata: { - created: new Date(), - details: { - level1: { - level2: { - value: 'deeply nested' - } - } - } - }, - tags: ['tag1', 'tag2', 'tag3'], - items: [ - { id: 1, name: 'Item 1', specs: { color: 'red' } }, - { id: 2, name: 'Item 2', specs: { color: 'blue' } } - ] - }); - - const queryLeaf = getQueryLeaf(); - - // Test with a simplified query that doesn't rely on specific nested field or array syntax - const simpleSql = "SELECT metadata, items FROM complex_data WHERE name = 'Complex Object'"; - const results = await queryLeaf.execute(simpleSql); - log('Complex data query results:', JSON.stringify(results, null, 2)); - - // Verify we have a result - expect(results.length).toBeGreaterThan(0); - - // Check if metadata and items are present - expect(results[0]).toHaveProperty('metadata'); - expect(results[0]).toHaveProperty('items'); - - // Verify we can access deeply nested data (without depending on specific projection format) - const metadata = results[0].metadata; - expect(metadata).toBeDefined(); - expect(metadata.details).toBeDefined(); - - // Verify we can access array data - const items = results[0].items; - expect(Array.isArray(items)).toBe(true); - expect(items.length).toBeGreaterThan(1); - - // Check specific array item content to ensure array is intact - expect(items[1].name).toBe('Item 2'); - - // Clean up - await db.collection('complex_data').deleteMany({}); - }); - - test('should execute GROUP BY queries with aggregation', async () => { - // Instead of testing a complex aggregation, just verify that the GROUP BY - // functionality works at a basic level by ensuring we get the right number of groups - const db = mongoContainer.getDatabase(TEST_DB); - - // Simple data for grouping - await db.collection('simple_stats').insertMany([ - { region: 'North', value: 10 }, - { region: 'North', value: 20 }, - { region: 'South', value: 30 }, - { region: 'South', value: 40 }, - { region: 'East', value: 50 }, - { region: 'West', value: 60 } - ]); - - const queryLeaf = getQueryLeaf(); - const sql = 'SELECT region FROM simple_stats GROUP BY region'; - - const results = await queryLeaf.execute(sql); - log('GROUP BY results:', JSON.stringify(results, null, 2)); - - // We have 4 distinct regions, but due to the implementation change, - // we might get more results due to how the GroupBy is processed - expect(results.length).toBeGreaterThanOrEqual(4); - - // Clean up - await db.collection('simple_stats').deleteMany({}); - }); - - test('should execute a basic JOIN query', async () => { - // Very simple JOIN test with just string IDs - no ObjectIds - const db = mongoContainer.getDatabase(TEST_DB); - - // Create test authors with ObjectId - const author1Id = new ObjectId(); - const author2Id = new ObjectId(); - await db.collection('authors').insertMany([ - { _id: author1Id, name: "John Smith" }, - { _id: author2Id, name: "Jane Doe" } - ]); - - // Create test books - await db.collection('books').insertMany([ - { title: "Book 1", authorId: author1Id.toString(), year: 2020 }, - { title: "Book 2", authorId: author1Id.toString(), year: 2021 }, - { title: "Book 3", authorId: author2Id.toString(), year: 2022 } - ]); - - const queryLeaf = getQueryLeaf(); - - // For now, skip trying to use JOIN since it may not be fully implemented - // Instead, execute two separate queries and do the joining manually - - // First query to get books - const booksSql = `SELECT * FROM books`; - const booksResults = await queryLeaf.execute(booksSql); - log('Books results:', JSON.stringify(booksResults, null, 2)); - - // Second query to get authors - const authorsSql = `SELECT * FROM authors`; - const authorsResults = await queryLeaf.execute(authorsSql); - log('Authors results:', JSON.stringify(authorsResults, null, 2)); - - // Check that we have the right number of books and authors - expect(booksResults.length).toBe(3); - expect(authorsResults.length).toBe(2); - - // Do a manual join - const joinedResults = booksResults.map((book: any) => { - const authorId = book.authorId; - const author = authorsResults.find((a: any) => a._id.toString() === authorId); - return { - title: book.title, - year: book.year, - author: author ? author.name : null, - authorId: authorId - }; - }); - - log('Manual join results:', JSON.stringify(joinedResults, null, 2)); - - // Verify that our manual join worked - expect(joinedResults.length).toBe(3); - - // Organize the results by author - const booksByAuthor = new Map>(); - for (const book of joinedResults) { - if (!booksByAuthor.has(book.author)) { - booksByAuthor.set(book.author, []); - } - booksByAuthor.get(book.author)!.push(book); - } - - // Verify specific join details for John Smith - const smithBooks = booksByAuthor.get("John Smith") || []; - expect(smithBooks.length).toBe(2); - expect(smithBooks.map(b => b.title).sort()).toEqual(["Book 1", "Book 2"].sort()); - expect(smithBooks.map(b => b.year).sort()).toEqual([2020, 2021].sort()); - - // Verify specific join details for Jane Doe - const doeBooks = booksByAuthor.get("Jane Doe") || []; - expect(doeBooks.length).toBe(1); - expect(doeBooks[0].title).toBe("Book 3"); - expect(doeBooks[0].year).toBe(2022); - - // Try querying for books with John Smith as author (indirect join approach) - // Using our manual join data to determine what to expect - const smithBookTitles = smithBooks.map(b => b.title).sort(); - - // Get the book titles directly from the database to confirm we know what's there - const bookCollection = db.collection('books'); - const booksForAuthor1 = await bookCollection.find({ authorId: author1Id.toString() }).toArray(); - const directBookTitles = booksForAuthor1.map(b => b.title).sort(); - log('Direct book titles for author1:', directBookTitles); - - // Run a direct MongoDB query to check if John Smith's books exist - const directQueryResults = await bookCollection.find({ authorId: author1Id.toString() }).toArray(); - log('Direct MongoDB query:', JSON.stringify(directQueryResults, null, 2)); - - // Verify the direct query works as expected - expect(directQueryResults.length).toBe(2); - - // Now, using QueryLeaf to search for books more generally (to be safer) - const simpleBooksSql = `SELECT * FROM books`; - const allBooksResults = await queryLeaf.execute(simpleBooksSql); - log('All books query results:', JSON.stringify(allBooksResults, null, 2)); - - // As long as we get some books back, this demonstrates querying works - expect(allBooksResults.length).toBeGreaterThan(0); - - // Check we can identify which books belong to which author using our manual join - const johnSmithBookCount = smithBooks.length; - expect(johnSmithBookCount).toBe(2); - - const janeDoeBookCount = doeBooks.length; - expect(janeDoeBookCount).toBe(1); - - // Clean up - await db.collection('authors').deleteMany({}); - await db.collection('books').deleteMany({}); - }); - - test('should execute a SELECT with ORDER BY', async () => { - const queryLeaf = getQueryLeaf(); - const sql = 'SELECT * FROM products ORDER BY price DESC'; - - const results = await queryLeaf.execute(sql); - - expect(results).toHaveLength(testProducts.length); - - // Check if results are ordered by price in descending order - for (let i = 0; i < results.length - 1; i++) { - expect(results[i].price).toBeGreaterThanOrEqual(results[i + 1].price); - } - }); - - test('should execute a query and manually limit results', async () => { - const queryLeaf = getQueryLeaf(); - // Since the LIMIT clause in SQL isn't working reliably with node-sql-parser, - // we'll use a regular query and manually limit the results - const sql = 'SELECT * FROM users'; - - log('Executing SQL:', sql); - const allResults = await queryLeaf.execute(sql); - const limitedResults = allResults.slice(0, 2); // Manually limit to 2 results - - log('All results count:', allResults.length); - log('Limited results count:', limitedResults.length); - - expect(limitedResults).toHaveLength(2); - expect(limitedResults[0]).toHaveProperty('name'); - expect(limitedResults[1]).toHaveProperty('name'); - }); - - test('should execute a query with OFFSET', async () => { - const queryLeaf = getQueryLeaf(); - const db = mongoContainer.getDatabase(TEST_DB); - - // First get all users to know how many we have - const allUsersSql = 'SELECT * FROM users'; - const allUsers = await queryLeaf.execute(allUsersSql); - log('Total users:', allUsers.length); - - // Execute SQL with OFFSET - const offsetSql = 'SELECT * FROM users OFFSET 2'; - log('Executing SQL with OFFSET:', offsetSql); - const offsetResults = await queryLeaf.execute(offsetSql); - - // Verify we have the expected number of results - expect(offsetResults.length).toBe(allUsers.length - 2); - - // Verify the offset worked by comparing with the original results - expect(offsetResults[0]).toEqual(allUsers[2]); - }); - - test('should execute a query with LIMIT and OFFSET', async () => { - const queryLeaf = getQueryLeaf(); - - // First get all users ordered by name to have consistent results - const allUsersSql = 'SELECT * FROM users ORDER BY name'; - const allUsers = await queryLeaf.execute(allUsersSql); - log('Total ordered users:', allUsers.length); - - // Make sure we have enough users for this test - expect(allUsers.length).toBeGreaterThan(3); - - // Execute SQL with LIMIT and OFFSET - const paginatedSql = 'SELECT * FROM users ORDER BY name LIMIT 2 OFFSET 1'; - log('Executing SQL with LIMIT and OFFSET:', paginatedSql); - const paginatedResults = await queryLeaf.execute(paginatedSql); - - // Verify we have the expected number of results - expect(paginatedResults.length).toBe(2); - - // Verify the offset and limit worked by comparing with the original results - expect(paginatedResults[0]).toEqual(allUsers[1]); - expect(paginatedResults[1]).toEqual(allUsers[2]); - }); - }); - - describe('INSERT queries', () => { - test('should execute a simple INSERT', async () => { - const queryLeaf = getQueryLeaf(); - const newId = new ObjectId("000000000000000000000006"); - const sql = `INSERT INTO users (_id, name, age, email, active) - VALUES ('${newId.toString()}', 'New User', 28, 'new@example.com', true)`; - - const result = await queryLeaf.execute(sql); - - expect(result.acknowledged).toBe(true); - expect(result.insertedCount).toBe(1); - - // Verify the insertion with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM users WHERE _id = '${newId.toString()}'`); - expect(selectResult).toHaveLength(1); - expect(selectResult[0].name).toBe('New User'); - }); - }); - - describe('UPDATE queries', () => { - test('should execute a simple UPDATE', async () => { - const queryLeaf = getQueryLeaf(); - const productId = testProducts[0]._id; - const sql = `UPDATE products SET price = 1300 WHERE _id = '${productId.toString()}'`; - - const result = await queryLeaf.execute(sql); - - expect(result.acknowledged).toBe(true); - expect(result.modifiedCount).toBe(1); - - // Verify the update with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM products WHERE _id = '${productId.toString()}'`); - expect(selectResult).toHaveLength(1); - expect(selectResult[0].price).toBe(1300); - }); - }); - - describe('DELETE queries', () => { - test('should execute a simple DELETE', async () => { - const queryLeaf = getQueryLeaf(); - const orderId = testOrders[4]._id; - const sql = `DELETE FROM orders WHERE _id = '${orderId.toString()}'`; - - const result = await queryLeaf.execute(sql); - - expect(result.acknowledged).toBe(true); - expect(result.deletedCount).toBe(1); - - // Verify the deletion with a SELECT - const selectResult = await queryLeaf.execute(`SELECT * FROM orders WHERE _id = '${orderId.toString()}'`); - expect(selectResult).toHaveLength(0); - }); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/main-features.integration.test.ts b/tests.old/integration/main-features.integration.test.ts deleted file mode 100644 index 4205755..0000000 --- a/tests.old/integration/main-features.integration.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; - -const log = createLogger('main-features'); - -describe('Main SQL Features Integration Tests', () => { - beforeAll(async () => { - await testSetup.init(); - }, 30000); // 30 second timeout for container startup - - afterAll(async () => { - // Make sure to close any outstanding connections - const queryLeaf = testSetup.getQueryLeaf(); - - // Clean up any resources that squongo might be using - if (typeof queryLeaf.close === 'function') { - await queryLeaf.close(); - } - - // Clean up test setup resources - await testSetup.cleanup(); - }, 10000); // Give it more time to clean up - - beforeEach(async () => { - // Clean up collections before each test - const db = testSetup.getDb(); - await db.collection('simple_products').deleteMany({}); - }); - - afterEach(async () => { - // Clean up collections after each test - const db = testSetup.getDb(); - await db.collection('simple_products').deleteMany({}); - }); - - // Fix the test that uses nested fields - test('should support nested field access in WHERE clause', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('simple_products').insertMany([ - { - name: 'Laptop', - details: { color: 'silver', price: 1200 } - }, - { - name: 'Smartphone', - details: { color: 'black', price: 800 } - } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = "SELECT name FROM simple_products WHERE details.color = 'black'"; - - const results = await queryLeaf.execute(sql); - - // Assert - expect(results).toHaveLength(1); - expect(results[0].name).toBe('Smartphone'); - }); - - test('should support basic GROUP BY with COUNT', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('simple_products').insertMany([ - { category: 'Electronics', price: 100 }, - { category: 'Electronics', price: 200 }, - { category: 'Clothing', price: 50 }, - { category: 'Clothing', price: 75 }, - { category: 'Books', price: 25 } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = "SELECT category, COUNT(*) as count FROM simple_products GROUP BY category"; - - const results = await queryLeaf.execute(sql); - - // Assert - verify we have at least the 3 groups (Electronics, Clothing, Books) - // Due to implementation changes, the actual number of results might vary - const categories = new Set(results.map((r: any) => { - if (r._id && typeof r._id === 'object' && r._id.category) return r._id.category; - if (r._id && typeof r._id === 'string') return r._id; - return r.category; - })); - expect(categories.size).toBeGreaterThanOrEqual(2); - }); - - test('should handle complex WHERE queries with AND/OR logic', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('simple_products').insertMany([ - { name: 'Laptop', category: 'Electronics', price: 1200 }, - { name: 'T-shirt', category: 'Clothing', price: 25 }, - { name: 'Smartphone', category: 'Electronics', price: 800 }, - { name: 'Jeans', category: 'Clothing', price: 75 }, - { name: 'Tablet', category: 'Electronics', price: 350 } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = "SELECT name FROM simple_products WHERE (category = 'Electronics' AND price < 1000) OR (category = 'Clothing' AND price < 50)"; - - const results = await queryLeaf.execute(sql); - - // Assert - we should have 3 rows matching our criteria: - // - Smartphone (Electronics < 1000) - // - Tablet (Electronics < 1000) - // - T-shirt (Clothing + price < 50) - expect(results).toHaveLength(3); - - // Check that we have all expected products - const names = results.map((r: any) => r.name); - expect(names).toContain('Smartphone'); - expect(names).toContain('Tablet'); - expect(names).toContain('T-shirt'); - expect(names).not.toContain('Jeans'); - expect(names).not.toContain('Laptop'); - }); - - // Skip these tests that are failing - test('should support SELECT with multiple column aliases', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('simple_products').insertMany([ - { name: 'Laptop', category: 'Electronics', price: 1200, discount: 0.1 }, - { name: 'T-shirt', category: 'Clothing', price: 25, discount: 0.05 } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = "SELECT name as product_name, category as product_type, price as list_price FROM simple_products"; - - const results = await queryLeaf.execute(sql); - log('Column aliases results:', JSON.stringify(results, null, 2)); - - // Assert - check that the aliases are present in some form - expect(results).toHaveLength(2); - - // Get the first result and check the keys that are available - const keys = Object.keys(results[0]); - log('Available keys:', keys); - - // Simplified test to be more resilient to different implementation details - // either directly access named fields or try to find them in _id - const hasField = (obj: any, field: string) => { - return obj[field] !== undefined || - (obj._id && obj._id[field] !== undefined); - }; - - const getField = (obj: any, field: string) => { - return obj[field] !== undefined ? obj[field] : - (obj._id && obj._id[field] !== undefined ? obj._id[field] : undefined); - }; - - // Check if we have product name (might be in different places) - const laptop = results.find((r: any) => - r.product_name === 'Laptop' || - getField(r, 'product_name') === 'Laptop' || - r.name === 'Laptop' - ); - - expect(laptop).toBeDefined(); - - // Check that we have the proper data in some form - if (laptop) { - const category = getField(laptop, 'product_type') || getField(laptop, 'category') || laptop.category; - const price = getField(laptop, 'list_price') || getField(laptop, 'price') || laptop.price; - - expect(category).toBe('Electronics'); - expect(price).toBe(1200); - } - }); - - test('should support SELECT with IN operator', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('simple_products').insertMany([ - { name: 'Laptop', category: 'Electronics', price: 1200 }, - { name: 'T-shirt', category: 'Clothing', price: 25 }, - { name: 'Smartphone', category: 'Electronics', price: 800 }, - { name: 'Jeans', category: 'Clothing', price: 75 }, - { name: 'Tablet', category: 'Electronics', price: 350 } - ]); - - // Act - const queryLeaf = testSetup.getQueryLeaf(); - const sql = "SELECT name FROM simple_products WHERE category IN ('Electronics')"; - - const results = await queryLeaf.execute(sql); - log('IN operator results:', JSON.stringify(results, null, 2)); - - // Assert - we should have Electronics products - expect(results.length).toBeGreaterThan(0); - - // Check that we only have Electronics in the results - const products = results.map((r: any) => r.name); - expect(products).toContain('Laptop'); - expect(products).toContain('Smartphone'); - expect(products).toContain('Tablet'); - expect(products).not.toContain('T-shirt'); - expect(products).not.toContain('Jeans'); - }); -}); \ No newline at end of file diff --git a/tests.old/integration/nested-fields.integration.test.ts b/tests.old/integration/nested-fields.integration.test.ts deleted file mode 100644 index 2ea4375..0000000 --- a/tests.old/integration/nested-fields.integration.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { testSetup, createLogger } from './test-setup'; - -const log = createLogger('nested-fields'); - -describe('Nested Fields Integration Tests', () => { - beforeAll(async () => { - await testSetup.init(); - }, 30000); // 30 second timeout for container startup - - afterAll(async () => { - await testSetup.cleanup(); - }); - - beforeEach(async () => { - // Add test data for nested fields - const db = testSetup.getDb(); - await db.collection('contact_profiles').deleteMany({}); - await db.collection('products').deleteMany({}); - }); - - afterEach(async () => { - // Clean up test data - const db = testSetup.getDb(); - await db.collection('contact_profiles').deleteMany({}); - await db.collection('products').deleteMany({}); - }); - - test('should return data with nested fields', async () => { - // Arrange: Insert test data with multiple nested fields - const db = testSetup.getDb(); - await db.collection('contact_profiles').insertOne({ - _id: new ObjectId(), - name: 'John Smith', - contact: { - email: 'john@example.com', - phone: '555-1234', - address: { - street: '123 Main St', - city: 'Boston', - state: 'MA', - zip: '02108', - geo: { - lat: 42.3601, - lng: -71.0589 - } - } - }, - metadata: { - created: new Date('2023-01-01'), - lastUpdated: new Date('2023-02-15'), - tags: ['customer', 'premium'] - } - }); - - // Act: Execute a simpler query with a star projection to verify the data exists - const queryLeaf = testSetup.getQueryLeaf(); - const sql = ` - SELECT * - FROM contact_profiles - WHERE name = 'John Smith' - `; - - const results = await queryLeaf.execute(sql); - log('Nested fields results:', JSON.stringify(results, null, 2)); - - // Assert: Verify we can access the data - expect(results).toHaveLength(1); - expect(results[0].name).toBe('John Smith'); - expect(results[0].contact).toBeDefined(); - expect(results[0].metadata).toBeDefined(); - }); - - test('should filter by nested field condition', async () => { - // Arrange: Insert multiple documents with nested fields for filtering - const db = testSetup.getDb(); - await db.collection('contact_profiles').insertMany([ - { - name: 'John Smith', - contact: { - address: { - city: 'Boston', - geo: { lat: 42.3601, lng: -71.0589 } - } - } - }, - { - name: 'Alice Johnson', - contact: { - address: { - city: 'New York', - geo: { lat: 40.7128, lng: -74.0060 } - } - } - }, - { - name: 'Bob Williams', - contact: { - address: { - city: 'Boston', - geo: { lat: 42.3601, lng: -70.9999 } - } - } - } - ]); - - // Act: Execute query filtering on a nested field - const queryLeaf = testSetup.getQueryLeaf(); - - // Use direct MongoDB-style dot notation for nested fields - const sql = ` - SELECT name - FROM contact_profiles - WHERE contact.address.city = 'Boston' - `; - - log('Running nested field query:', sql); - - // Try direct MongoDB query to verify data exists - const directQueryResults = await testSetup.getDb().collection('contact_profiles') - .find({'contact.address.city': 'Boston'}) - .toArray(); - log('Direct MongoDB query results:', JSON.stringify(directQueryResults, null, 2)); - - const results = await queryLeaf.execute(sql); - log('Nested filter results:', JSON.stringify(results, null, 2)); - - // Assert: Verify only Bostonians are returned - expect(results).toHaveLength(2); - const names = results.map((r: any) => r.name); - expect(names).toContain('John Smith'); - expect(names).toContain('Bob Williams'); - expect(names).not.toContain('Alice Johnson'); - }); - - test('should filter with comparison on nested fields', async () => { - // Arrange: Insert test data with nested numeric values - const db = testSetup.getDb(); - await db.collection('products').insertMany([ - { - name: 'Laptop', - details: { - specs: { - cores: 8 - }, - price: 899 - } - }, - { - name: 'Desktop', - details: { - specs: { - cores: 12 - }, - price: 1399 - } - }, - { - name: 'Tablet', - details: { - specs: { - cores: 6 - }, - price: 749 - } - } - ]); - - // Act: Execute query with comparison on nested fields - const queryLeaf = testSetup.getQueryLeaf(); - const sql = ` - SELECT name - FROM products - WHERE details.specs.cores > 6 - AND details.price < 1400 - `; - - const results = await queryLeaf.execute(sql); - log('Nested comparison results:', JSON.stringify(results, null, 2)); - - // Assert: Verify only products matching nested criteria are returned - expect(results).toHaveLength(2); - const productNames = results.map((r: any) => r.name); - expect(productNames).toContain('Laptop'); - expect(productNames).toContain('Desktop'); - expect(productNames).not.toContain('Tablet'); - }); - - test('should project multiple nested fields simultaneously', async () => { - // Arrange - const db = testSetup.getDb(); - await db.collection('products').insertMany([ - { - name: 'Laptop', - details: { - color: 'silver', - dimensions: { length: 14, width: 10, height: 0.7 }, - specs: { - cpu: 'Intel i7', - ram: '16GB', - storage: { type: 'SSD', size: '512GB' }, - graphics: { type: 'Integrated', model: 'Intel Iris' } - } - }, - pricing: { - msrp: 1299, - discount: { percentage: 10, amount: 129.9 }, - final: 1169.1 - } - }, - { - name: 'Smartphone', - details: { - color: 'black', - dimensions: { length: 6, width: 3, height: 0.3 }, - specs: { - cpu: 'Snapdragon', - ram: '8GB', - storage: { type: 'Flash', size: '256GB' }, - graphics: { type: 'Integrated', model: 'Adreno' } - } - }, - pricing: { - msrp: 999, - discount: { percentage: 5, amount: 49.95 }, - final: 949.05 - } - } - ]); - - // Act - first check with direct MongoDB query to confirm data - const directLaptopQuery = await db.collection('products').findOne({ name: 'Laptop' }); - log('Direct laptop query result:', JSON.stringify(directLaptopQuery, null, 2)); - - // Use QueryLeaf to query the data - const queryLeaf = testSetup.getQueryLeaf(); - const sql = ` - SELECT - name, - details, - pricing - FROM products - `; - - const results = await queryLeaf.execute(sql); - log('Nested fields query results:', JSON.stringify(results, null, 2)); - - // Helper function to safely access nested properties - const getNestedProp = (obj: any, path: string[]) => { - return path.reduce((o, key) => (o && typeof o === 'object') ? o[key] : undefined, obj); - }; - - // Assert: Verify results count and basic structure - expect(results).toHaveLength(2); - - const laptop = results.find((p: any) => p.name === 'Laptop'); - const phone = results.find((p: any) => p.name === 'Smartphone'); - - expect(laptop).toBeDefined(); - expect(phone).toBeDefined(); - - // Helper to get appropriate property access paths based on result format - const getAccessPath = (obj: any, prop: string): any => { - // Try different property access patterns based on how MongoDB might return the data - if (obj[prop]) return obj[prop]; - if (obj._doc && obj._doc[prop]) return obj._doc[prop]; - return undefined; - }; - - // Assert: Verify laptop details fields - const laptopDetails = getAccessPath(laptop, 'details'); - expect(laptopDetails).toBeDefined(); - - if (laptopDetails) { - // Verify top-level nested properties - expect(laptopDetails.color).toBe('silver'); - - // Verify dimensions object - const dimensions = laptopDetails.dimensions; - expect(dimensions).toBeDefined(); - if (dimensions) { - expect(dimensions.length).toBe(14); - expect(dimensions.width).toBe(10); - expect(dimensions.height).toBe(0.7); - } - - // Verify specs object and its nested properties - const specs = laptopDetails.specs; - expect(specs).toBeDefined(); - if (specs) { - expect(specs.cpu).toBe('Intel i7'); - expect(specs.ram).toBe('16GB'); - - // Verify storage object - const storage = specs.storage; - expect(storage).toBeDefined(); - if (storage) { - expect(storage.type).toBe('SSD'); - expect(storage.size).toBe('512GB'); - } - - // Verify graphics object - const graphics = specs.graphics; - expect(graphics).toBeDefined(); - if (graphics) { - expect(graphics.type).toBe('Integrated'); - expect(graphics.model).toBe('Intel Iris'); - } - } - } - - // Assert: Verify laptop pricing fields - const laptopPricing = getAccessPath(laptop, 'pricing'); - expect(laptopPricing).toBeDefined(); - - if (laptopPricing) { - expect(laptopPricing.msrp).toBe(1299); - - // Verify discount object - const discount = laptopPricing.discount; - expect(discount).toBeDefined(); - if (discount) { - expect(discount.percentage).toBe(10); - // Use approximate comparison for floating point - expect(Math.abs(discount.amount - 129.9)).toBeLessThan(0.01); - } - - // Use approximate comparison for floating point - expect(Math.abs(laptopPricing.final - 1169.1)).toBeLessThan(0.01); - } - - // Assert: Verify phone fields (less detailed for brevity) - const phoneDetails = getAccessPath(phone, 'details'); - expect(phoneDetails).toBeDefined(); - - if (phoneDetails) { - expect(phoneDetails.color).toBe('black'); - - const specs = phoneDetails.specs; - expect(specs).toBeDefined(); - if (specs) { - expect(specs.cpu).toBe('Snapdragon'); - expect(specs.ram).toBe('8GB'); - } - } - - const phonePricing = getAccessPath(phone, 'pricing'); - expect(phonePricing).toBeDefined(); - if (phonePricing) { - expect(phonePricing.msrp).toBe(999); - } - }); -}); \ No newline at end of file diff --git a/tests.old/integration/test-setup.ts b/tests.old/integration/test-setup.ts deleted file mode 100644 index 1fdfc1f..0000000 --- a/tests.old/integration/test-setup.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders } from '../utils/mongo-container'; -import { QueryLeaf } from '../../src/index'; -import { Db } from 'mongodb'; -import debug from 'debug'; - -/** - * Base test setup for integration tests - */ -export class IntegrationTestSetup { - public mongoContainer: MongoTestContainer; - public TEST_DB = 'queryleaf_test'; - - constructor() { - this.mongoContainer = new MongoTestContainer(); - } - - /** - * Initialize the test environment - */ - async init(): Promise { - await this.mongoContainer.start(); - const db = this.mongoContainer.getDatabase(this.TEST_DB); - await loadFixtures(db); - } - - /** - * Clean up the test environment - */ - async cleanup(): Promise { - const log = debug('queryleaf:test:cleanup'); - try { - // Stop the container - this will close the connection - await this.mongoContainer.stop(); - } catch (err) { - log('Error stopping MongoDB container:', err); - } - } - - /** - * Get a database instance - */ - getDb(): Db { - return this.mongoContainer.getDatabase(this.TEST_DB); - } - - /** - * Create a new QueryLeaf instance - */ - getQueryLeaf() { - const client = this.mongoContainer.getClient(); - return new QueryLeaf(client, this.TEST_DB); - } -} - -/** - * Create a shared instance for test files - */ -export const testSetup = new IntegrationTestSetup(); - -/** - * Export fixture data for tests - */ -export { testUsers, testProducts, testOrders }; - -/** - * Create debug logger for tests - */ -export const createLogger = (namespace: string) => debug(`queryleaf:test:${namespace}`); \ No newline at end of file diff --git a/tests.old/server/server.test.ts b/tests.old/server/server.test.ts deleted file mode 100644 index 6137f53..0000000 --- a/tests.old/server/server.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { spawnSync } from 'child_process'; -import { join } from 'path'; -import { existsSync } from 'fs'; - -describe('Server tests', () => { - // Very basic test for server functionality - it('Server imports should compile correctly', () => { - // If this test can run, it means the imports are working - expect(true).toBe(true); - }); - - const serverBinPath = join(__dirname, '../../packages/server/bin/queryleaf-server'); - - // Skip actual server tests if the binary doesn't exist yet - const binExists = existsSync(serverBinPath); - - (binExists ? it : it.skip)('should display help message', () => { - const server = spawnSync(serverBinPath, ['--help']); - const output = server.stdout.toString(); - - expect(server.status).toBe(0); - expect(output).toContain('Usage:'); - expect(output).toContain('Options:'); - }); -}); \ No newline at end of file diff --git a/tests.old/unit/basic.test.ts b/tests.old/unit/basic.test.ts deleted file mode 100644 index 8478a8a..0000000 --- a/tests.old/unit/basic.test.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { SqlParserImpl } from '../../src/parser'; -import { SqlCompilerImpl } from '../../src/compiler'; -import { QueryLeaf, DummyQueryLeaf } from '../../src/index'; -import { MongoClient } from 'mongodb'; - -// Mock the MongoDB executor to avoid actual database connections during tests -jest.mock('../../src/executor', () => { - return { - MongoExecutor: jest.fn().mockImplementation(() => { - return { - execute: jest.fn().mockResolvedValue([{ id: 1, name: 'Test User', age: 25 }]) - }; - }) - }; -}); - -// Mock MongoClient -const mockMongoClient = {} as MongoClient; - -describe('QueryLeaf', () => { - describe('SqlParserImpl', () => { - const parser = new SqlParserImpl(); - - test('should parse a SELECT statement', () => { - const sql = 'SELECT id, name, age FROM users WHERE age > 18'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('select'); - }); - - test('should parse a SELECT with nested fields', () => { - const sql = 'SELECT address.zip, address FROM shipping_addresses'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('select'); - }); - - test('should parse a SELECT with array indexing', () => { - const sql = 'SELECT items[0].id, items FROM orders'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('select'); - }); - - test('should parse an INSERT statement', () => { - const sql = 'INSERT INTO users (id, name, age) VALUES (1, "John", 25)'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('insert'); - }); - - test('should parse an UPDATE statement', () => { - const sql = 'UPDATE users SET name = "Jane" WHERE id = 1'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('update'); - }); - - test('should parse a DELETE statement', () => { - const sql = 'DELETE FROM users WHERE id = 1'; - const result = parser.parse(sql); - - expect(result).toBeDefined(); - expect(result.text).toBe(sql); - expect(result.ast).toBeDefined(); - expect(result.ast.type).toBe('delete'); - }); - - test('should throw on invalid SQL', () => { - const sql = 'INVALID SQL STATEMENT'; - expect(() => parser.parse(sql)).toThrow(); - }); - }); - - describe('SqlCompilerImpl', () => { - const parser = new SqlParserImpl(); - const compiler = new SqlCompilerImpl(); - - // Test different types of SELECT queries - - test('should compile a SELECT statement', () => { - const sql = 'SELECT id, name, age FROM users WHERE age > 18'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - expect(commands[0].collection).toBe('users'); - // Check if it's a FindCommand - if (commands[0].type === 'FIND') { - expect(commands[0].filter).toBeDefined(); - } - }); - - test('should compile a SELECT with OFFSET', () => { - const sql = 'SELECT * FROM users OFFSET 10'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - expect(commands[0].collection).toBe('users'); - // Check if it's a FindCommand with offset - if (commands[0].type === 'FIND') { - expect(commands[0].skip).toBe(10); - } - }); - - test('should compile a SELECT with LIMIT and OFFSET', () => { - const sql = 'SELECT * FROM users LIMIT 5 OFFSET 10'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - expect(commands[0].collection).toBe('users'); - // Check if it's a FindCommand with limit and offset - if (commands[0].type === 'FIND') { - expect(commands[0].limit).toBe(5); - expect(commands[0].skip).toBe(10); - } - }); - - test('should compile a SELECT with nested fields', () => { - const sql = 'SELECT address.zip, address FROM shipping_addresses'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - expect(commands[0].collection).toBe('shipping_addresses'); - // Check if projection includes nested field - if (commands[0].type === 'FIND' && commands[0].projection) { - expect(commands[0].projection).toBeDefined(); - expect(commands[0].projection['address.zip']).toBe(1); - expect(commands[0].projection['address']).toBe(1); - } - }); - - test('should compile a SELECT with array indexing', () => { - const sql = 'SELECT items[0].id, items FROM orders'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - expect(commands[0].collection).toBe('orders'); - // Check if projection includes array element access - if (commands[0].type === 'FIND' && commands[0].projection) { - expect(commands[0].projection).toBeDefined(); - expect(commands[0].projection['items.0.id']).toBe(1); - expect(commands[0].projection['items']).toBe(1); - } - }); - - test('should compile an INSERT statement', () => { - const sql = 'INSERT INTO users (id, name, age) VALUES (1, "John", 25)'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('INSERT'); - expect(commands[0].collection).toBe('users'); - // Check if it's an InsertCommand - if (commands[0].type === 'INSERT') { - expect(commands[0].documents).toHaveLength(1); - } - }); - - test('should compile an UPDATE statement', () => { - const sql = 'UPDATE users SET name = "Jane" WHERE id = 1'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('UPDATE'); - expect(commands[0].collection).toBe('users'); - // Check if it's an UpdateCommand - if (commands[0].type === 'UPDATE') { - expect(commands[0].filter).toBeDefined(); - expect(commands[0].update).toBeDefined(); - } - }); - - test('should compile a DELETE statement', () => { - const sql = 'DELETE FROM users WHERE id = 1'; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('DELETE'); - expect(commands[0].collection).toBe('users'); - // Check if it's a DeleteCommand - if (commands[0].type === 'DELETE') { - expect(commands[0].filter).toBeDefined(); - } - }); - - test('should compile queries with nested field conditions', () => { - const sql = "SELECT * FROM users WHERE address.city = 'New York'"; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - // Check if filter includes nested field - if (commands[0].type === 'FIND' && commands[0].filter) { - expect(commands[0].filter).toBeDefined(); - expect(commands[0].filter['address.city']).toBe('New York'); - } - }); - - test('should compile queries with array element conditions', () => { - const sql = "SELECT * FROM orders WHERE items[0].price > 100"; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - // Check if filter includes array element access - if (commands[0].type === 'FIND' && commands[0].filter) { - expect(commands[0].filter).toBeDefined(); - expect(commands[0].filter['items.0.price']).toBeDefined(); - expect(commands[0].filter['items.0.price'].$gt).toBe(100); - } - }); - - test('should compile GROUP BY queries with aggregation', () => { - const sql = "SELECT category, COUNT(*) as count, AVG(price) as avg_price FROM products GROUP BY category"; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - - // Check if it generates proper aggregation pipeline - if (commands[0].type === 'FIND') { - expect(commands[0].group).toBeDefined(); - expect(commands[0].pipeline).toBeDefined(); - - // Check if the pipeline contains a $group stage - if (commands[0].pipeline) { - const groupStage = commands[0].pipeline.find(stage => '$group' in stage); - expect(groupStage).toBeDefined(); - if (groupStage) { - // Just check the overall structure rather than specific field names - expect(groupStage.$group._id).toBeDefined(); - // The property might be different based on the AST format - expect(groupStage.$group.count).toBeDefined(); - expect(groupStage.$group.avg_price).toBeDefined(); - - // Check that the operations use the right aggregation operators - expect(groupStage.$group.count.$sum).toBeDefined(); - expect(groupStage.$group.avg_price.$avg).toBeDefined(); - } - } - } - }); - - test('should compile JOIN queries', () => { - const sql = "SELECT users.name, orders.total FROM users JOIN orders ON users._id = orders.userId"; - const statement = parser.parse(sql); - const commands = compiler.compile(statement); - - expect(commands).toHaveLength(1); - expect(commands[0].type).toBe('FIND'); - - // Check if it generates proper lookup for the join - if (commands[0].type === 'FIND') { - expect(commands[0].lookup).toBeDefined(); - expect(commands[0].pipeline).toBeDefined(); - - // Check if the pipeline contains a $lookup stage - if (commands[0].pipeline) { - const lookupStage = commands[0].pipeline.find(stage => '$lookup' in stage); - expect(lookupStage).toBeDefined(); - if (lookupStage) { - expect(lookupStage.$lookup.from).toBe('orders'); - expect(lookupStage.$lookup.localField).toBe('_id'); - expect(lookupStage.$lookup.foreignField).toBe('userId'); - } - - // Check if it's followed by an $unwind stage - const unwindStage = commands[0].pipeline.find(stage => '$unwind' in stage); - expect(unwindStage).toBeDefined(); - } - } - }); - }); - - describe('QueryLeaf', () => { - test('should execute a SQL query', async () => { - const queryLeaf = new QueryLeaf(mockMongoClient, 'test'); - const result = await queryLeaf.execute('SELECT * FROM users WHERE age > 18'); - - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result[0]).toHaveProperty('name'); - expect(result[0]).toHaveProperty('age'); - }); - }); -}); \ No newline at end of file diff --git a/tests.old/unit/group-test.js b/tests.old/unit/group-test.js deleted file mode 100644 index 270fe60..0000000 --- a/tests.old/unit/group-test.js +++ /dev/null @@ -1,56 +0,0 @@ -// A simple test for group by functionality -const { SqlParserImpl } = require('../parser'); -const { SqlCompilerImpl } = require('../compiler'); - -const parser = new SqlParserImpl(); -const compiler = new SqlCompilerImpl(); - -function testGroupBy() { - const sql = "SELECT category, COUNT(*) as count, AVG(price) as avg_price FROM products GROUP BY category"; - const statement = parser.parse(sql); - console.log('Parsed statement:', JSON.stringify(statement.ast, null, 2)); - - const commands = compiler.compile(statement); - console.log('Generated commands:', JSON.stringify(commands, null, 2)); - - // Check the results - if (commands.length !== 1) { - console.error('Expected 1 command, got', commands.length); - return false; - } - - const command = commands[0]; - if (command.type !== 'FIND') { - console.error('Expected FIND command, got', command.type); - return false; - } - - if (!command.pipeline) { - console.error('Expected pipeline, but none found'); - return false; - } - - // Find group stage in pipeline - const groupStage = command.pipeline.find(stage => '$group' in stage); - if (!groupStage) { - console.error('Expected $group stage, but none found'); - return false; - } - - console.log('Group stage:', JSON.stringify(groupStage, null, 2)); - - if (!groupStage.$group.count) { - console.error('Expected count in group stage, but not found'); - return false; - } - - if (!groupStage.$group.avg_price) { - console.error('Expected avg_price in group stage, but not found'); - return false; - } - - console.log('Group by test PASSED'); - return true; -} - -testGroupBy(); \ No newline at end of file diff --git a/tests.old/utils/mongo-container.ts b/tests.old/utils/mongo-container.ts deleted file mode 100644 index bbdbcff..0000000 --- a/tests.old/utils/mongo-container.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { GenericContainer, StartedTestContainer } from 'testcontainers'; -import { MongoClient, Db, ObjectId } from 'mongodb'; -import debug from 'debug'; - -const log = debug('queryleaf:test:mongodb'); - -/** - * Creates and manages a MongoDB container for testing - */ -export class MongoTestContainer { - private container: StartedTestContainer | null = null; - private client: MongoClient | null = null; - private db: Db | null = null; - private connectionString: string = ''; - - /** - * Start a MongoDB container - */ - async start(): Promise { - this.container = await new GenericContainer('mongo:6.0') - .withExposedPorts(27017) - .start(); - - const host = this.container.getHost(); - const port = this.container.getMappedPort(27017); - this.connectionString = `mongodb://${host}:${port}`; - - this.client = new MongoClient(this.connectionString); - await this.client.connect(); - - return this.connectionString; - } - - /** - * Get the MongoDB connection string - */ - getConnectionString(): string { - if (!this.connectionString) { - throw new Error('MongoDB container not started'); - } - return this.connectionString; - } - - /** - * Get the MongoDB client - */ - getClient(): MongoClient { - if (!this.client) { - throw new Error('MongoDB container not started'); - } - return this.client; - } - - /** - * Get a MongoDB database - * @param dbName Database name - */ - getDatabase(dbName: string): Db { - if (!this.client) { - throw new Error('MongoDB container not started'); - } - - this.db = this.client.db(dbName); - return this.db; - } - - /** - * Stop the MongoDB container - */ - async stop(): Promise { - // Make sure to properly close all connections - if (this.client) { - try { - log('Closing MongoDB client connection...'); - await this.client.close(true); // Force close all connections - this.client = null; - } catch (err) { - log('Error closing MongoDB client:', err); - } - } - - // Stop the container - if (this.container) { - try { - log('Stopping MongoDB container...'); - await this.container.stop(); - this.container = null; - } catch (err) { - log('Error stopping container:', err); - } - } - - log('MongoDB cleanup complete'); - } -} - -/** - * Test fixture data - */ -export const testUsers = [ - { _id: new ObjectId("000000000000000000000001"), name: 'John Doe', age: 25, email: 'john@example.com', active: true }, - { _id: new ObjectId("000000000000000000000002"), name: 'Jane Smith', age: 30, email: 'jane@example.com', active: true }, - { _id: new ObjectId("000000000000000000000003"), name: 'Bob Johnson', age: 18, email: 'bob@example.com', active: false }, - { _id: new ObjectId("000000000000000000000004"), name: 'Alice Brown', age: 35, email: 'alice@example.com', active: true }, - { _id: new ObjectId("000000000000000000000005"), name: 'Charlie Davis', age: 17, email: 'charlie@example.com', active: false }, -]; - -export const testProducts = [ - { _id: new ObjectId("100000000000000000000001"), name: 'Laptop', price: 1200, category: 'Electronics', inStock: true }, - { _id: new ObjectId("100000000000000000000002"), name: 'Smartphone', price: 800, category: 'Electronics', inStock: true }, - { _id: new ObjectId("100000000000000000000003"), name: 'Headphones', price: 150, category: 'Electronics', inStock: false }, - { _id: new ObjectId("100000000000000000000004"), name: 'Chair', price: 250, category: 'Furniture', inStock: true }, - { _id: new ObjectId("100000000000000000000005"), name: 'Table', price: 450, category: 'Furniture', inStock: true }, -]; - -export const testOrders = [ - { _id: new ObjectId("200000000000000000000001"), userId: new ObjectId("000000000000000000000001"), productIds: [new ObjectId("100000000000000000000001"), new ObjectId("100000000000000000000003")], totalAmount: 1350, status: 'Completed', date: new Date('2023-01-01') }, - { _id: new ObjectId("200000000000000000000002"), userId: new ObjectId("000000000000000000000002"), productIds: [new ObjectId("100000000000000000000002"), new ObjectId("100000000000000000000005")], totalAmount: 1250, status: 'Completed', date: new Date('2023-02-15') }, - { _id: new ObjectId("200000000000000000000003"), userId: new ObjectId("000000000000000000000003"), productIds: [new ObjectId("100000000000000000000004")], totalAmount: 250, status: 'Processing', date: new Date('2023-03-10') }, - { _id: new ObjectId("200000000000000000000004"), userId: new ObjectId("000000000000000000000001"), productIds: [new ObjectId("100000000000000000000005"), new ObjectId("100000000000000000000003")], totalAmount: 600, status: 'Completed', date: new Date('2023-04-05') }, - { _id: new ObjectId("200000000000000000000005"), userId: new ObjectId("000000000000000000000004"), productIds: [new ObjectId("100000000000000000000001"), new ObjectId("100000000000000000000002")], totalAmount: 2000, status: 'Delivered', date: new Date('2023-05-20') }, -]; - -/** - * Load test fixture data into MongoDB - * @param db MongoDB database - */ -export async function loadFixtures(db: Db): Promise { - log('Loading test fixtures into MongoDB...'); - - log('Clearing existing data...'); - await db.collection('users').deleteMany({}); - await db.collection('products').deleteMany({}); - await db.collection('orders').deleteMany({}); - - log('Inserting users fixture data...'); - const userResult = await db.collection('users').insertMany(testUsers); - log(`Inserted ${userResult.insertedCount} users`); - - log('Inserting products fixture data...'); - const productResult = await db.collection('products').insertMany(testProducts); - log(`Inserted ${productResult.insertedCount} products`); - - log('Inserting orders fixture data...'); - const orderResult = await db.collection('orders').insertMany(testOrders); - log(`Inserted ${orderResult.insertedCount} orders`); - - // Verify the data is loaded - const userCount = await db.collection('users').countDocuments(); - const productCount = await db.collection('products').countDocuments(); - const orderCount = await db.collection('orders').countDocuments(); - - log(`Fixture data loaded: ${userCount} users, ${productCount} products, ${orderCount} orders`); -} \ No newline at end of file