├── .eslintrc.js ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── CLAUDE.md ├── COMMERCIAL_LICENSE.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README-monorepo.md ├── README.md ├── SECURITY.md ├── TODO.md ├── bin ├── queryleaf ├── queryleaf-server └── run-all ├── code_of_conduct.md ├── docs ├── README.md ├── assets │ ├── favicon.ico │ ├── logo-transparent-bg-green-shape.png │ └── logo-white.svg ├── debugging │ ├── limitations.md │ └── troubleshooting.md ├── getting-started │ ├── installation.md │ └── quickstart.md ├── index.md ├── overrides │ ├── home.html │ ├── main.html │ └── partials │ │ ├── footer.html │ │ └── integrations │ │ └── analytics │ │ └── custom.html ├── sql-syntax │ ├── array-access.md │ ├── delete.md │ ├── group-by.md │ ├── index.md │ ├── insert.md │ ├── joins.md │ ├── nested-fields.md │ ├── select.md │ └── update.md ├── stylesheets │ ├── extra.css │ └── home.css ├── support │ ├── license-faq.md │ └── pricing.md └── usage │ ├── cli.md │ ├── core-concepts.md │ ├── dummy-client.md │ ├── examples.md │ ├── mongodb-client.md │ ├── postgres-server.md │ └── server.md ├── jest.config.js ├── logo-green-bg-white-shape.png ├── logo-green.svg ├── logo-transparent-bg-black-shape.png ├── logo-transparent-bg-green-shape.png ├── logo-transparent-bg-white-shape.png ├── logo-white.svg ├── logo.svg ├── mkdocs.yml ├── package.json ├── packages ├── cli │ ├── README.md │ ├── bin │ │ └── queryleaf │ ├── jest.config.js │ ├── package.json │ ├── src │ │ └── cli.ts │ ├── tests │ │ ├── integration │ │ │ └── cli.test.ts │ │ └── unit │ │ │ └── cli.test.ts │ └── tsconfig.json ├── lib │ ├── README.md │ ├── build-log.txt │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── compiler.ts │ │ ├── examples │ │ │ ├── basic-usage.ts │ │ │ ├── dummy-client-demo.ts │ │ │ └── existing-client-demo.ts │ │ ├── executor │ │ │ ├── dummy-client.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── interfaces │ │ │ └── index.ts │ │ └── parser.ts │ ├── tests │ │ ├── integration │ │ │ ├── alias.integration.test.ts │ │ │ ├── array-access.integration.test.ts │ │ │ ├── cursor.integration.test.ts │ │ │ ├── edge-cases.integration.test.ts │ │ │ ├── group-by.integration.test.ts │ │ │ ├── integration.integration.test.ts │ │ │ ├── main-features.integration.test.ts │ │ │ ├── nested-fields.integration.test.ts │ │ │ ├── nested-update-issue.test.ts │ │ │ ├── projection.integration.test.ts │ │ │ └── test-setup.ts │ │ ├── unit │ │ │ ├── basic.test.ts │ │ │ └── group-test.js │ │ └── utils │ │ │ └── mongo-container.ts │ └── tsconfig.json ├── postgres-server │ ├── README.md │ ├── bin │ │ └── queryleaf-pg-server │ ├── examples │ │ └── protocol-example.ts │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── pg-server.ts │ │ └── protocol-handler.ts │ ├── tests │ │ ├── integration │ │ │ ├── auth-passthrough.integration.test.ts │ │ │ ├── integration.test.ts │ │ │ ├── minimal-integration.test.ts │ │ │ ├── minimal.integration.test.ts │ │ │ └── protocol.test.ts │ │ ├── unit │ │ │ ├── auth-passthrough.test.ts │ │ │ ├── basic.test.ts │ │ │ ├── server.basic.test.ts │ │ │ └── server.test.ts │ │ └── utils │ │ │ ├── mongo-container.ts │ │ │ └── test-setup.ts │ └── tsconfig.json └── server │ ├── README.md │ ├── bin │ └── queryleaf-server │ ├── jest.config.js │ ├── package.json │ ├── src │ └── server.ts │ ├── tests │ ├── integration │ │ └── server.test.ts │ └── unit │ │ └── server.test.ts │ └── tsconfig.json ├── requirements.txt ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | project: ['./tsconfig.json', './packages/*/tsconfig.json'], 8 | }, 9 | plugins: ['@typescript-eslint'], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | ], 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'warn', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/no-unused-vars': ['error', { 22 | argsIgnorePattern: '^_', 23 | varsIgnorePattern: '^_' 24 | }], 25 | }, 26 | ignorePatterns: ['dist', 'node_modules', '*.js', '*.d.ts'] 27 | }; -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Packages to npm 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Run on any tag that starts with v (e.g., v1.0.0) 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '18' 19 | registry-url: 'https://registry.npmjs.org/' 20 | scope: '@queryleaf' 21 | 22 | - name: Get tag version 23 | id: get_version 24 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | # Update versions in package.json files before publishing 30 | - name: Update lib package version 31 | run: | 32 | bin/run-all version --new-version $VERSION --no-git-tag-version 33 | bin/run-all lockversion 34 | 35 | - name: Build packages 36 | run: yarn build 37 | 38 | # Publish packages in the correct order 39 | - name: Publish lib package 40 | run: bin/run-all publish --access public 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | 44 | # Create GitHub release for this version 45 | - name: Create GitHub Release 46 | uses: softprops/action-gh-release@v1 47 | with: 48 | name: Release ${{ env.VERSION }} 49 | body: | 50 | # QueryLeaf v${{ env.VERSION }} 51 | 52 | Published packages: 53 | - @queryleaf/lib@${{ env.VERSION }} 54 | - @queryleaf/cli@${{ env.VERSION }} 55 | - @queryleaf/server@${{ env.VERSION }} 56 | - @queryleaf/postgres-server@${{ env.VERSION }} 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: QueryLeaf Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | # See supported Node.js versions at https://nodejs.org/en/about/releases/ 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'yarn' 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Gotta build the lib first for downstream stuff 30 | run: yarn build 31 | - name: Run unit tests 32 | run: yarn test:unit 33 | 34 | - name: Run integration tests 35 | run: yarn test:integration 36 | env: 37 | # This ensures testcontainers can find Docker 38 | TESTCONTAINERS_HOST_OVERRIDE: "localhost" 39 | 40 | 41 | - name: Format check 42 | run: yarn format:check 43 | 44 | - name: Typecheck 45 | run: yarn typecheck -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled output 2 | /dist 3 | /build 4 | /site 5 | packages/*/dist 6 | 7 | # Dependencies 8 | /node_modules 9 | packages/*/node_modules 10 | 11 | # IDE and OS files 12 | .idea/ 13 | .vscode/ 14 | *.swp 15 | *.swo 16 | .DS_Store 17 | 18 | # Logs and temp files 19 | *.log 20 | tmp/ 21 | temp/ 22 | 23 | # Optional: if you use a package manager other than npm 24 | # yarn.lock 25 | # pnpm-lock.yaml 26 | # or if you use specific test result files 27 | coverage/ 28 | # test-results/ 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # QueryLeaf Development Guide 2 | 3 | QueryLeaf is a SQL to MongoDB compiler / translator. 4 | 5 | ## Build & Test Commands 6 | - Full build: `yarn build` 7 | - Typecheck: `yarn typecheck` 8 | - Lint: `yarn lint` (fix: `yarn lint:fix`) 9 | - Format: `yarn format` (check: `yarn format:check`) 10 | - Run all tests: `yarn test` 11 | - Run individual package tests: `yarn test:lib`, `yarn test:cli`, `yarn test:server`, `yarn test:pg-server` 12 | - Run single test: `cd packages/[package] && yarn yarn -t "test name"` or `yarn jest path/to/test.test.ts -t "test name"` 13 | - Integration tests: `yarn test:lib:integration` (requires Docker) 14 | - Documentation: `yarn docs:serve` (dev), `yarn docs:build` (build) 15 | 16 | ## Code Style Guidelines 17 | - We use YARN, not NPM 18 | - GitHub actions is used for builds, see workflows in the .github folder. 19 | - TypeScript with strict typing; avoid `any` when possible 20 | - Single quotes, trailing commas, 2-space indentation, 100 char line limit 21 | - Prefix unused variables with underscore (e.g., `_unused`) 22 | - Monorepo structure with packages: lib, cli, server, postgres-server 23 | - Descriptive variable/function names in camelCase 24 | - Error handling with proper try/catch blocks and meaningful error messages 25 | - Use async/await for asynchronous code 26 | - Follow existing patterns for similar functionality 27 | - Tests should cover both unit and integration cases 28 | -------------------------------------------------------------------------------- /COMMERCIAL_LICENSE.md: -------------------------------------------------------------------------------- 1 | # QueryLeaf Commercial License Agreement 2 | 3 | ## Overview 4 | 5 | This Commercial License Agreement (the "Agreement") is entered into by and between QueryLeaf ("Licensor") and the individual or entity that has purchased a commercial license ("Licensee"). 6 | 7 | This license permits Licensee to use, modify, and distribute the QueryLeaf software (the "Software") in both non-commercial and commercial applications, subject to the terms and limitations set forth in this Agreement. 8 | 9 | ## License Grant 10 | 11 | Subject to the terms and conditions of this Agreement, Licensor grants to Licensee a non-exclusive, non-transferable license to: 12 | 13 | 1. Use the Software for any purpose, including commercial use 14 | 2. Modify the Software to suit Licensee's needs 15 | 3. Integrate the Software into Licensee's applications, services, or systems 16 | 4. Distribute the Software as part of Licensee's applications, provided that the Software is not the primary value of Licensee's product 17 | 18 | ## Restrictions 19 | 20 | Licensee may not: 21 | 22 | 1. Use the Software in a way that competes directly with QueryLeaf or Beekeeper Studio 23 | 2. Transfer, sell, rent, lease, sublicense, or distribute the Software as a standalone product 24 | 3. Remove or alter any proprietary notices or marks on the Software 25 | 4. Use the Software in any way that violates applicable laws or regulations 26 | 27 | ## Term and Termination 28 | 29 | This license is effective from the date of purchase and remains in effect unless terminated. Licensor may terminate this license if Licensee breaches any of its terms. Upon termination, Licensee must cease all use of the Software and destroy all copies. 30 | 31 | ## Support and Updates 32 | 33 | Details regarding support and updates are specified in the service agreement corresponding to the tier of license purchased. For more information on available pricing plans and their features, please visit [queryleaf.com](https://queryleaf.com). 34 | 35 | ## Warranty Disclaimer 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | 39 | ## Limitation of Liability 40 | 41 | IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 42 | 43 | ## Governing Law 44 | 45 | This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which the Licensor is established, without giving effect to any principles of conflicts of law. 46 | 47 | ## Entire Agreement 48 | 49 | This Agreement constitutes the entire agreement between the parties concerning the subject matter hereof and supersedes all prior and contemporaneous agreements and understandings, whether oral or written. 50 | 51 | --- 52 | 53 | For pricing plans and purchasing information, please visit [queryleaf.com](https://queryleaf.com) 54 | 55 | For any questions regarding this license, please contact support@queryleaf.com -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guidelines 2 | 3 | Last updated: Feb 27 2022 4 | 5 | We welcome community contributions! If you're thinking of contributing, thank you! 6 | 7 | We ask that all contributors abide by our [code of conduct](https://github.com/beekeeper-studio/beekeeper-studio/blob/master/code_of_conduct.md) 8 | 9 | 10 | ### Opening Issues 11 | 12 | We have templates for questions, features, or bug reports, please follow the directions in these templates, but generally: 13 | 14 | - Please give as much detail as possible about the feature or issue. 15 | - Include OS, database type, database version, and app version 16 | - Include steps to replicate (eg the SQL to run) 17 | 18 | ### Pull Requests 19 | 20 | It's usually best to open an issue before spending a lot of time working on the code, just in case someone else is working on the same problem. We're always happy to discuss how to implement or design things to help you before you begin work. 21 | 22 | Sometimes we don't merge pull requests if they don't meet our design goals, but we really never want this to happen, so please talk to us! 23 | 24 | ### Legal 25 | 26 | All contributions to this repository are made under the [MIT License](https://opensource.org/licenses/MIT). 27 | 28 | #### What this means practically 29 | 30 | If you make a PR, the PR code is licensed as MIT. As soon as I copy (merge) the code into this repository it is then made available under the GPLv3 as part of Beekeeper Studio. The code is still copyright to you, and your original MIT license still applies to the code in your PR. The MIT license requires that I maintain the copyright notice, which is made available below. 31 | 32 | #### Why do it this way 33 | 34 | Practically speaking, we need to have the ability to change the Beekeeper Studio license in the future if we need to, by providing your contributions under the MIT license, we can do so without requiring that all contributors sign a [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement) or hand over their copyright to us. This is a pretty common way to manage contributions for a large-ish open source project, so it should be totally fine for 99% of contributors. 35 | 36 | #### MIT License for Contributions 37 | 38 | Copyright 2022 Code Contributor (whoever you are) 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 45 | -------------------------------------------------------------------------------- /README-monorepo.md: -------------------------------------------------------------------------------- 1 | # QueryLeaf Monorepo Guidelines 2 | 3 | This repository uses a monorepo structure with Yarn workspaces to manage multiple packages. 4 | 5 | ## Repository Structure 6 | 7 | - `packages/lib`: Core library for SQL to MongoDB translation 8 | - `packages/cli`: Command-line interface 9 | - `packages/server`: REST API server 10 | 11 | ## Development Workflow 12 | 13 | ### Installation 14 | 15 | ```bash 16 | yarn install 17 | ``` 18 | 19 | ### Building 20 | 21 | ```bash 22 | # Build all packages 23 | yarn build 24 | 25 | # Build individual packages 26 | yarn build:lib 27 | yarn build:cli 28 | yarn build:server 29 | ``` 30 | 31 | ### Testing 32 | 33 | ```bash 34 | # Run all tests 35 | yarn test 36 | 37 | # Run tests for individual packages 38 | yarn test:lib 39 | yarn test:cli 40 | yarn test:server 41 | 42 | # Run specific test types for the lib package 43 | yarn test:lib:unit 44 | yarn test:lib:integration 45 | ``` 46 | 47 | ### Code Quality 48 | 49 | ```bash 50 | # Run TypeScript type checking 51 | yarn typecheck 52 | 53 | # Run linting 54 | yarn lint 55 | yarn lint:fix 56 | 57 | # Run code formatting 58 | yarn format 59 | yarn format:check 60 | 61 | # Run all validations (types, linting, tests, formatting) 62 | yarn validate 63 | ``` 64 | 65 | ## Adding new features 66 | 67 | 1. Determine which package(s) need to be modified 68 | 2. Make changes in the appropriate package(s) 69 | 3. Add tests in the package's `tests` directory 70 | 4. Run `yarn validate` to ensure everything passes 71 | 5. Submit a pull request 72 | 73 | ## Dependency Management 74 | 75 | - Shared dev dependencies (TypeScript, ESLint, etc.) are in the root `package.json` 76 | - Package-specific dependencies are in each package's `package.json` 77 | - Use `yarn add -W` to add a dependency to the root 78 | - Use `yarn workspace @queryleaf/[package] add ` to add a dependency to a specific package 79 | 80 | ## Release Process 81 | 82 | Each package is versioned independently but released together. 83 | 84 | 1. Update versions in each package's `package.json` 85 | 2. Build all packages: `yarn build` 86 | 3. Publish packages: 87 | ``` 88 | cd packages/lib && npm publish 89 | cd ../cli && npm publish 90 | cd ../server && npm publish 91 | ``` 92 | 93 | ## Best Practices 94 | 95 | 1. Keep packages focused on a single responsibility 96 | 2. Share code through dependencies, not copy-paste 97 | 3. Ensure all code is properly tested 98 | 4. Maintain consistent coding style across packages using ESLint and Prettier 99 | 5. Document all public APIs -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We support the latest version of Beekeeper Studio only. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a vulnerability we prefer that you email us instead of filing a public ticket so we can remediate the issue quickly. We'll verify and fix vulnerabilities as soon as possible and give you updates too! 10 | 11 | support@beekeeperstudio.io 12 | 13 | 14 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # QueryLeaf Project TODO List 2 | 3 | ## Integration Tests 4 | We've implemented the following tests: 5 | 6 | ### Simple Queries 7 | - [x] Test SELECT with multiple column aliases (implemented and working) 8 | - [ ] Test SELECT with arithmetic operations in projections 9 | - [x] Test SELECT with multiple WHERE conditions connected by OR (implemented and working) 10 | - [x] Test SELECT with IN operator (implemented and working) 11 | - [ ] Test SELECT with NOT IN operator 12 | - [ ] Test SELECT with NULL/NOT NULL checks 13 | - [ ] Test SELECT with LIMIT and OFFSET 14 | - [ ] Test SELECT with ORDER BY multiple fields 15 | - [ ] Test SELECT with ORDER BY ASC/DESC combinations 16 | 17 | ### Nested Field Access 18 | - [ ] Test querying on deeply nested fields (3+ levels deep) 19 | - [x] Test projecting multiple nested fields simultaneously (implemented, working with fixes) 20 | - [x] Test filtering with comparisons on nested fields (implemented, working with fixes) 21 | - [ ] Test updating nested fields 22 | - [x] Test nested field access with complex WHERE conditions (implemented, working with fixes) 23 | 24 | ### Array Access 25 | - [x] Test querying arrays with multiple indices (implemented, working with fixes) 26 | - [x] Test filtering by array element properties at different indices (implemented, working with fixes) 27 | - [ ] Test filtering by multiple array elements simultaneously 28 | - [ ] Test projecting multiple array elements in one query 29 | - [ ] Test array access with nested arrays 30 | - [ ] Test updating array elements 31 | 32 | ### GROUP BY 33 | - [x] Test GROUP BY with multiple columns (implemented, working with fixes) 34 | - [ ] Test GROUP BY with HAVING clause 35 | - [ ] Test GROUP BY with multiple aggregation functions 36 | - [x] Test aggregation functions: AVG, MIN, MAX, COUNT (implemented, working with fixes) 37 | - [ ] Test GROUP BY with ORDER BY on aggregation results 38 | - [ ] Test GROUP BY with complex expressions 39 | - [ ] Test GROUP BY with filtering before aggregation 40 | - [ ] Test GROUP BY on nested fields 41 | - [ ] Test performance with large dataset aggregation 42 | 43 | ### JOINs 44 | - [x] Test INNER JOIN with multiple conditions 45 | - [ ] Test LEFT OUTER JOIN implementation 46 | - [ ] Test RIGHT OUTER JOIN implementation 47 | - [ ] Test FULL OUTER JOIN implementation 48 | - [ ] Test JOIN with WHERE conditions 49 | - [ ] Test multiple JOINs in one query (3+ tables) 50 | - [ ] Test JOINs with aggregation 51 | - [ ] Test JOINs with nested field access 52 | - [ ] Test JOINs with array field access 53 | - [ ] Test performance with large dataset JOINs 54 | 55 | ### Advanced Features 56 | - [ ] Test CASE statements in SELECT list 57 | - [ ] Test subqueries in WHERE clause 58 | - [ ] Test subqueries in FROM clause 59 | - [ ] Test window functions if supported 60 | - [ ] Test date/time functions 61 | - [ ] Test string functions 62 | 63 | ### Edge Cases 64 | - [x] Test handling of special characters in field names 65 | - [x] Test handling of extremely large result sets 66 | - [x] Test behavior with invalid SQL syntax 67 | - [x] Test behavior with valid SQL but unsupported features 68 | - [x] Test behavior with missing collections 69 | - [x] Test behavior with invalid data types 70 | - [x] Test handling of MongoDB ObjectId conversions 71 | 72 | ## Performance Testing 73 | - [ ] Benchmark simple queries vs native MongoDB queries 74 | - [ ] Benchmark complex queries vs native MongoDB queries 75 | - [ ] Benchmark with increasing dataset sizes (10K, 100K, 1M documents) 76 | - [ ] Identify bottlenecks in the translation process 77 | - [ ] Optimize query execution for common patterns 78 | 79 | ## Documentation 80 | - [ ] Document SQL syntax support and limitations 81 | - [ ] Create examples of each supported SQL feature 82 | - [ ] Document MongoDB query translation for each SQL feature 83 | - [ ] Create a troubleshooting guide 84 | - [ ] Add inline code documentation 85 | 86 | ## Feature Enhancements 87 | - [ ] Add support for SQL DISTINCT 88 | - [ ] Implement SQL subquery support 89 | - [ ] Support for data types (DATE, TIMESTAMP, BOOLEAN) 90 | - [ ] Add basic transaction support 91 | - [ ] Support for SQL UNION, INTERSECT, EXCEPT 92 | - [ ] Add index creation/management via SQL 93 | - [ ] Implement execution plan visualization 94 | - [ ] Add query caching 95 | - [ ] Support for conditional expressions (CASE) 96 | - [ ] Add execution metrics and query profiling -------------------------------------------------------------------------------- /bin/queryleaf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cli.js'); -------------------------------------------------------------------------------- /bin/queryleaf-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/server.js'); -------------------------------------------------------------------------------- /bin/run-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run a command across all packages in a specific order 4 | # Usage: bin/run-all [additional args] 5 | 6 | set -e 7 | 8 | if [ -z "$1" ]; then 9 | echo "Usage: bin/run-all [additional args]" 10 | echo "Example: bin/run-all test:unit" 11 | exit 1 12 | fi 13 | 14 | COMMAND=$1 15 | shift 16 | ARGS=$@ 17 | 18 | # Define package order: lib first, then others 19 | PACKAGES=("lib" "cli" "server" "postgres-server") 20 | 21 | for pkg in "${PACKAGES[@]}"; do 22 | echo "========== Running command in @queryleaf/$pkg ==========" 23 | yarn workspace "@queryleaf/$pkg" $COMMAND $ARGS 24 | done 25 | 26 | echo "========== All commands completed successfully ==========" -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Beekeeper Studio Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | 130 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # QueryLeaf Documentation 2 | 3 | This directory contains the source files for the QueryLeaf documentation site. The documentation is built using [MkDocs](https://www.mkdocs.org/) with the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme. 4 | 5 | ## Viewing the Documentation 6 | 7 | You can view the documentation in several ways: 8 | 9 | 1. **Online**: Visit the published documentation at [queryleaf.com/docs](https://queryleaf.com/docs) 10 | 11 | 2. **Locally**: Run the documentation server locally with: 12 | ```bash 13 | npm run docs:serve 14 | ``` 15 | Then open your browser to [http://localhost:8000](http://localhost:8000) 16 | 17 | ## Documentation Structure 18 | 19 | The documentation is organized as follows: 20 | 21 | - `index.md`: Home page 22 | - `getting-started/`: Installation and quickstart guides 23 | - `usage/`: Core concepts and usage examples 24 | - `sql-syntax/`: Detailed SQL syntax reference 25 | - `debugging/`: Troubleshooting and limitations 26 | - `licenses/`: License information 27 | - `assets/`: Images and other static assets 28 | - `stylesheets/`: Custom CSS styles 29 | 30 | ## Contributing to the Documentation 31 | 32 | We welcome contributions to improve the documentation. Follow these steps: 33 | 34 | 1. Make your changes to the Markdown files in this directory 35 | 2. Run `npm run docs:serve` to preview your changes locally 36 | 3. Once you're satisfied, submit a pull request with your changes 37 | 38 | ## Building the Documentation 39 | 40 | To build a static version of the documentation: 41 | 42 | ```bash 43 | npm run docs:build 44 | ``` 45 | 46 | This will generate a `site` directory containing the static HTML, CSS, and JavaScript files. 47 | 48 | ## Deploying the Documentation 49 | 50 | To deploy the documentation to GitHub Pages: 51 | 52 | ```bash 53 | npm run docs:deploy 54 | ``` 55 | 56 | This will build the documentation and deploy it to the `gh-pages` branch of the repository. 57 | 58 | ## Documentation Technology 59 | 60 | - [MkDocs](https://www.mkdocs.org/): The documentation site generator 61 | - [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/): The theme for the documentation 62 | - [Python-Markdown](https://python-markdown.github.io/): The Markdown parser 63 | - [PyMdown Extensions](https://facelessuser.github.io/pymdown-extensions/): Extensions for Python-Markdown 64 | 65 | ## Local Development Setup 66 | 67 | To set up the documentation development environment: 68 | 69 | 1. Install Python 3.x 70 | 2. Install MkDocs and all required packages using the requirements.txt file: 71 | ```bash 72 | pip install -r ../requirements.txt 73 | ``` 74 | 75 | ## Documentation Guidelines 76 | 77 | When contributing to the documentation, please follow these guidelines: 78 | 79 | 1. Use clear, concise language 80 | 2. Include code examples where appropriate 81 | 3. Follow the existing structure and formatting 82 | 4. Test your changes locally before submitting 83 | 5. Use proper Markdown syntax and formatting 84 | 6. Include screenshots or diagrams for complex concepts when helpful 85 | 86 | ## License 87 | 88 | The documentation is licensed under the same terms as the QueryLeaf project. -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/logo-transparent-bg-green-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/docs/assets/logo-transparent-bg-green-shape.png -------------------------------------------------------------------------------- /docs/assets/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Getting started with QueryLeaf is straightforward. This guide will walk you through the installation process and prerequisites. 4 | 5 | ## Prerequisites 6 | 7 | Before installing QueryLeaf, make sure you have: 8 | 9 | - Node.js 16.x or higher 10 | - npm or yarn package manager 11 | - MongoDB (for running actual queries) 12 | - Docker (optional, for running integration tests) 13 | 14 | ## Installing QueryLeaf 15 | 16 | QueryLeaf is divided into three separate packages to minimize dependencies: 17 | 18 | ### Core Library 19 | 20 | Install the core library as a dependency in your project: 21 | 22 | === "npm" 23 | ```bash 24 | npm install @queryleaf/lib 25 | ``` 26 | 27 | === "yarn" 28 | ```bash 29 | yarn add @queryleaf/lib 30 | ``` 31 | 32 | ### Command Line Interface 33 | 34 | To use the command-line interface, install the CLI package: 35 | 36 | === "npm" 37 | ```bash 38 | # As a project dependency 39 | npm install @queryleaf/cli 40 | 41 | # Or globally 42 | npm install -g @queryleaf/cli 43 | ``` 44 | 45 | === "yarn" 46 | ```bash 47 | # As a project dependency 48 | yarn add @queryleaf/cli 49 | 50 | # Or globally 51 | yarn global add @queryleaf/cli 52 | ``` 53 | 54 | After installation, you'll have access to the `queryleaf` command. 55 | 56 | ### Web Server 57 | 58 | To use the web server for a MongoDB SQL proxy, install the server package: 59 | 60 | === "npm" 61 | ```bash 62 | # As a project dependency 63 | npm install @queryleaf/server 64 | 65 | # Or globally 66 | npm install -g @queryleaf/server 67 | ``` 68 | 69 | === "yarn" 70 | ```bash 71 | # As a project dependency 72 | yarn add @queryleaf/server 73 | 74 | # Or globally 75 | yarn global add @queryleaf/server 76 | ``` 77 | 78 | After installation, you'll have access to the `queryleaf-server` command. 79 | 80 | ## TypeScript Support 81 | 82 | QueryLeaf is written in TypeScript and includes type definitions out of the box. You don't need to install any additional packages for TypeScript support. 83 | 84 | ## Peer Dependencies 85 | 86 | QueryLeaf has the following peer dependencies: 87 | 88 | - `mongodb`: The official MongoDB driver for Node.js 89 | - `node-sql-parser`: Used to parse SQL statements 90 | 91 | These dependencies will be installed automatically when you install QueryLeaf. 92 | 93 | ## Setting Up Your Project 94 | 95 | Here's a basic project setup with QueryLeaf: 96 | 97 | 1. Create a new directory for your project: 98 | ```bash 99 | mkdir my-queryleaf-project 100 | cd my-queryleaf-project 101 | ``` 102 | 103 | 2. Initialize a new npm project: 104 | ```bash 105 | npm init -y 106 | ``` 107 | 108 | 3. Install QueryLeaf and MongoDB client: 109 | ```bash 110 | npm install @queryleaf/lib mongodb 111 | ``` 112 | 113 | 4. Create a basic file structure: 114 | ``` 115 | my-queryleaf-project/ 116 | ├── node_modules/ 117 | ├── src/ 118 | │ └── index.js 119 | ├── package.json 120 | └── package-lock.json 121 | ``` 122 | 123 | 5. Add a basic usage example in `src/index.js`: 124 | ```javascript 125 | const { MongoClient } = require('mongodb'); 126 | const { QueryLeaf } = require('@queryleaf/lib'); 127 | 128 | async function main() { 129 | // Connect to MongoDB 130 | const client = new MongoClient('mongodb://localhost:27017'); 131 | await client.connect(); 132 | console.log('Connected to MongoDB'); 133 | 134 | // Create QueryLeaf instance 135 | const queryLeaf = new QueryLeaf(client, 'mydatabase'); 136 | 137 | try { 138 | // Execute a query 139 | const results = await queryLeaf.execute('SELECT * FROM mycollection LIMIT 10'); 140 | console.log('Query results:', results); 141 | } catch (error) { 142 | console.error('Error executing query:', error); 143 | } finally { 144 | // Close the connection 145 | await client.close(); 146 | console.log('MongoDB connection closed'); 147 | } 148 | } 149 | 150 | main().catch(console.error); 151 | ``` 152 | 153 | ## Verifying Installation 154 | 155 | To verify that QueryLeaf is installed correctly and working: 156 | 157 | 1. Make sure you have MongoDB running locally 158 | 2. Create a simple test script 159 | 3. Run the script and check for any errors 160 | 161 | If everything is set up correctly, you should be able to execute SQL queries against your MongoDB database. 162 | 163 | ## Next Steps 164 | 165 | Now that you have installed QueryLeaf, you can proceed to: 166 | 167 | - [Quick Start Guide](quickstart.md): Learn the basics of using QueryLeaf 168 | - [Core Concepts](../usage/core-concepts.md): Understand the architecture and principles 169 | - [CLI Documentation](../usage/cli.md): Learn how to use the command-line interface 170 | - [Server Documentation](../usage/server.md): Learn how to run and use the web server 171 | - [Examples](../usage/examples.md): See practical examples of using QueryLeaf -------------------------------------------------------------------------------- /docs/getting-started/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | This guide will help you get up and running with QueryLeaf quickly, demonstrating basic functionality and common use cases. 4 | 5 | ## Basic Usage 6 | 7 | Here's a simple example showing how to use QueryLeaf with an existing MongoDB client: 8 | 9 | ```typescript 10 | import { MongoClient } from 'mongodb'; 11 | import { QueryLeaf } from '@queryleaf/lib'; 12 | 13 | async function runQueries() { 14 | // Connect to MongoDB 15 | const client = new MongoClient('mongodb://localhost:27017'); 16 | await client.connect(); 17 | 18 | try { 19 | // Create a QueryLeaf instance with your MongoDB client 20 | const queryLeaf = new QueryLeaf(client, 'mydatabase'); 21 | 22 | // Execute a simple SELECT query 23 | const users = await queryLeaf.execute( 24 | 'SELECT name, email FROM users WHERE age > 21 ORDER BY name ASC' 25 | ); 26 | console.log('Users over 21:', users); 27 | 28 | // Execute an INSERT query 29 | await queryLeaf.execute( 30 | `INSERT INTO products (name, price, category) 31 | VALUES ('Laptop', 999.99, 'Electronics')` 32 | ); 33 | console.log('Product inserted'); 34 | 35 | // Execute an UPDATE query 36 | await queryLeaf.execute( 37 | "UPDATE users SET status = 'active' WHERE lastLogin > '2023-01-01'" 38 | ); 39 | console.log('Users updated'); 40 | 41 | // Execute a DELETE query 42 | await queryLeaf.execute( 43 | "DELETE FROM sessions WHERE expiry < '2023-06-01'" 44 | ); 45 | console.log('Old sessions deleted'); 46 | } catch (error) { 47 | console.error('Error:', error); 48 | } finally { 49 | // Close your MongoDB client when done 50 | await client.close(); 51 | } 52 | } 53 | 54 | runQueries().catch(console.error); 55 | ``` 56 | 57 | ## Using with TypeScript 58 | 59 | QueryLeaf is written in TypeScript and provides full type definitions. Here's how to use it with TypeScript: 60 | 61 | ```typescript 62 | import { MongoClient } from 'mongodb'; 63 | import { QueryLeaf } from '@queryleaf/lib'; 64 | 65 | interface User { 66 | _id: string; 67 | name: string; 68 | email: string; 69 | age: number; 70 | } 71 | 72 | async function getUsers(): Promise { 73 | const client = new MongoClient('mongodb://localhost:27017'); 74 | await client.connect(); 75 | 76 | try { 77 | const queryLeaf = new QueryLeaf(client, 'mydatabase'); 78 | 79 | // Results will be typed as User[] 80 | const users = await queryLeaf.execute( 81 | 'SELECT _id, name, email, age FROM users WHERE age > 21' 82 | ); 83 | 84 | return users; 85 | } finally { 86 | await client.close(); 87 | } 88 | } 89 | 90 | getUsers() 91 | .then(users => console.log(`Found ${users.length} users`)) 92 | .catch(console.error); 93 | ``` 94 | 95 | ## Using with a Dummy Client for Testing 96 | 97 | For testing or development without a real MongoDB instance, you can use the `DummyQueryLeaf` class: 98 | 99 | ```typescript 100 | import { DummyQueryLeaf } from '@queryleaf/lib'; 101 | 102 | async function testQueries() { 103 | // Create a dummy client that logs operations without executing them 104 | const dummyLeaf = new DummyQueryLeaf('testdb'); 105 | 106 | // Execute queries (will be logged but not executed) 107 | await dummyLeaf.execute('SELECT * FROM users LIMIT 10'); 108 | // [DUMMY MongoDB] FIND in testdb.users with filter: {} 109 | // [DUMMY MongoDB] Executing find on users with limit: 10 110 | 111 | await dummyLeaf.execute('UPDATE products SET price = price * 1.1 WHERE category = "Electronics"'); 112 | // [DUMMY MongoDB] UPDATE in testdb.products with filter: {"category":"Electronics"} 113 | // [DUMMY MongoDB] Executing update on products with update: {"price":{"$multiply":["$price",1.1]}} 114 | } 115 | 116 | testQueries().catch(console.error); 117 | ``` 118 | 119 | ## Common SQL Patterns 120 | 121 | Here are some common SQL patterns you can use with QueryLeaf: 122 | 123 | ### Querying Nested Fields 124 | 125 | ```typescript 126 | // Query for users in a specific city 127 | const nyUsers = await queryLeaf.execute( 128 | 'SELECT name, email, address.city FROM users WHERE address.city = "New York"' 129 | ); 130 | ``` 131 | 132 | ### Working with Array Fields 133 | 134 | ```typescript 135 | // Query for orders with a specific item at index 0 136 | const laptopOrders = await queryLeaf.execute( 137 | 'SELECT _id, customer, total FROM orders WHERE items[0].name = "Laptop"' 138 | ); 139 | 140 | // Query for high-value items in any position 141 | const expensiveOrders = await queryLeaf.execute( 142 | 'SELECT _id, customer, total FROM orders WHERE items[0].price > 1000' 143 | ); 144 | ``` 145 | 146 | ### Using JOINs 147 | 148 | ```typescript 149 | // Join users and orders collections 150 | const userOrders = await queryLeaf.execute(` 151 | SELECT u.name, u.email, o._id as order_id, o.total 152 | FROM users u 153 | JOIN orders o ON u._id = o.userId 154 | WHERE o.status = 'processing' 155 | `); 156 | ``` 157 | 158 | ### Aggregation with GROUP BY 159 | 160 | ```typescript 161 | // Group by status and count orders 162 | const orderStats = await queryLeaf.execute(` 163 | SELECT status, COUNT(*) as count, SUM(total) as total_value 164 | FROM orders 165 | GROUP BY status 166 | `); 167 | ``` 168 | 169 | ## Next Steps 170 | 171 | Now that you've seen the basic usage of QueryLeaf, you can: 172 | 173 | - Explore the [Core Concepts](../usage/core-concepts.md) to understand the architecture 174 | - Check out the [SQL Syntax Reference](../sql-syntax/index.md) for detailed information on supported SQL features 175 | - See the [Examples](../usage/examples.md) for more complex use cases 176 | - Learn about the MongoDB client integration in [MongoDB Client](../usage/mongodb-client.md) -------------------------------------------------------------------------------- /docs/overrides/home.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | {% block tabs %} 3 | {{ super() }} 4 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |

SQL for MongoDB that just works

30 |

QueryLeaf is available for Node.js, as a cli, and as a standalone webserver

31 | 39 |
40 |
41 |
42 |
43 |
SQL
44 |
MongoDB
45 |
46 |
47 |
48 |
SELECT u.name, COUNT(o._id) as order_count
49 | FROM users u
50 | LEFT JOIN orders o ON u._id = o.userId
51 | WHERE u.status = 'active'
52 |   AND o.items[0].price > 100
53 | GROUP BY u.name
54 | ORDER BY order_count DESC
55 | LIMIT 5
56 |
57 |
58 | The SQL you write 59 | 60 | MongoDB executes 61 |
62 |
63 |
64 |
65 |
66 | 67 | {{ page.content }} 68 |
69 |
70 | {% endblock %} 71 | {% block content %}{% endblock %} 72 | {% block footer %} 73 |
74 | 79 |
80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block extrahead %} 3 | {% if page.meta and page.meta.homepage %} 4 | 5 | {% endif %} 6 | 7 | {% endblock %} 8 | {% block scripts %} 9 | {{ super() }} 10 | 91 | {% endblock %} 92 | -------------------------------------------------------------------------------- /docs/overrides/partials/footer.html: -------------------------------------------------------------------------------- 1 | {% if config.copyright %} 2 | 7 | {% endif %} -------------------------------------------------------------------------------- /docs/overrides/partials/integrations/analytics/custom.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/sql-syntax/array-access.md: -------------------------------------------------------------------------------- 1 | # Working with Array Access 2 | 3 | MongoDB documents can contain arrays, and QueryLeaf provides special syntax for accessing array elements. This page explains how to query and manipulate arrays in your SQL queries. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | SELECT array elements | ✅ Full | Project specific array indices | 10 | | WHERE with array elements | ✅ Full | Filter based on specific array indices | 11 | | UPDATE array elements | ✅ Full | Modify specific array elements | 12 | | INSERT with arrays | ✅ Full | Create documents with array structures | 13 | | Bracket notation | ✅ Full | Access array elements by index [0], [1], etc. | 14 | | Nested arrays | ✅ Full | Access arrays within arrays | 15 | | Arrays with objects | ✅ Full | Access object fields within arrays | 16 | | Array operators | ❌ None | No $elemMatch, $all, etc. | 17 | | Array modifiers | ❌ None | No $push, $pull, $addToSet support | 18 | | Array wildcard | ❌ None | Can't match any element with "items[].name" | 19 | 20 | ## Array Access Syntax 21 | 22 | In QueryLeaf, you can access array elements using square bracket notation with zero-based indices: 23 | 24 | ```sql 25 | field[index].subfield 26 | ``` 27 | 28 | ## Querying Arrays 29 | 30 | ### Basic Array Element Access 31 | 32 | ```sql 33 | -- Select array elements 34 | SELECT _id, customer, items[0].name, items[0].price 35 | FROM orders 36 | 37 | -- Filter by array element value 38 | SELECT _id, customer, total 39 | FROM orders 40 | WHERE items[0].name = 'Laptop' 41 | 42 | -- Multiple array indices 43 | SELECT _id, items[0].name AS first_item, items[1].name AS second_item 44 | FROM orders 45 | ``` 46 | 47 | ### Array Elements with Nested Fields 48 | 49 | ```sql 50 | -- Nested fields within array elements 51 | SELECT _id, items[0].product.name, items[0].product.category 52 | FROM orders 53 | WHERE items[0].product.manufacturer = 'Apple' 54 | ``` 55 | 56 | ### Using WHERE with Array Elements 57 | 58 | ```sql 59 | -- Comparison operations on array elements 60 | SELECT _id, customer, total 61 | FROM orders 62 | WHERE items[0].price > 500 63 | 64 | -- Multiple conditions on array elements 65 | SELECT _id, customer 66 | FROM orders 67 | WHERE items[0].price > 100 AND items[0].quantity >= 2 68 | 69 | -- Conditions on different array elements 70 | SELECT _id, customer 71 | FROM orders 72 | WHERE items[0].category = 'Electronics' AND items[1].category = 'Accessories' 73 | ``` 74 | 75 | ## Updating Array Elements 76 | 77 | ```sql 78 | -- Update a specific array element 79 | UPDATE orders 80 | SET items[0].status = 'shipped' 81 | WHERE _id = '60a6c5ef837f3d2d54c965f3' 82 | 83 | -- Update nested field within array element 84 | UPDATE orders 85 | SET items[1].product.stock = 150 86 | WHERE _id = '60a6c5ef837f3d2d54c965f3' 87 | ``` 88 | 89 | ## Inserting Documents with Arrays 90 | 91 | ```sql 92 | -- Insert with array of simple values 93 | INSERT INTO users (name, email, tags) 94 | VALUES ( 95 | 'Bob Johnson', 96 | 'bob@example.com', 97 | ['developer', 'nodejs', 'mongodb'] 98 | ) 99 | 100 | -- Insert with array of objects 101 | INSERT INTO orders (customer, items) 102 | VALUES ( 103 | 'Alice Brown', 104 | [ 105 | { "product": "Laptop", "price": 999.99, "quantity": 1 }, 106 | { "product": "Mouse", "price": 24.99, "quantity": 2 } 107 | ] 108 | ) 109 | ``` 110 | 111 | ## Translation to MongoDB 112 | 113 | When working with arrays, QueryLeaf translates SQL to MongoDB using dot notation for array indices: 114 | 115 | | SQL | MongoDB | 116 | |-----|--------| 117 | | `items[0].name` in SELECT | Projection with `'items.0.name': 1` | 118 | | `items[0].price > 500` in WHERE | Filter with `'items.0.price': { $gt: 500 }` | 119 | | `SET items[0].status = 'shipped'` | Update with `$set: {'items.0.status': 'shipped'}` | 120 | | Array in INSERT | Array in insertOne/insertMany | 121 | 122 | ### Example Translation 123 | 124 | SQL: 125 | ```sql 126 | SELECT _id, customer, items[0].name, items[0].price 127 | FROM orders 128 | WHERE items[0].price > 500 129 | ``` 130 | 131 | MongoDB: 132 | ```javascript 133 | db.collection('orders').find( 134 | { 'items.0.price': { $gt: 500 } }, 135 | { _id: 1, customer: 1, 'items.0.name': 1, 'items.0.price': 1 } 136 | ) 137 | ``` 138 | 139 | ## Limitations 140 | 141 | - QueryLeaf does not currently support MongoDB's array operators like `$elemMatch` or `$all` 142 | - Limited support for querying any element in an array (vs. a specific indexed element) 143 | - No direct support for array update operators like `$push`, `$pull`, or `$addToSet` 144 | 145 | ## Performance Considerations 146 | 147 | - Create indexes on frequently queried array fields: `db.orders.createIndex({'items.product': 1})` 148 | - Be aware that querying and updating arrays with many elements can be less efficient 149 | - Consider using specific array indices when possible for faster performance 150 | 151 | ## Best Practices 152 | 153 | - Keep arrays at a reasonable size for optimal performance 154 | - Consider how array data will be queried when designing your schema 155 | - For complex array operations, consider using the MongoDB driver directly 156 | - Use array access when you need to work with specific positions in arrays 157 | 158 | ## Related Pages 159 | 160 | - [SELECT Queries](select.md) 161 | - [UPDATE Operations](update.md) 162 | - [INSERT Operations](insert.md) 163 | - [Working with Nested Fields](nested-fields.md) -------------------------------------------------------------------------------- /docs/sql-syntax/delete.md: -------------------------------------------------------------------------------- 1 | # DELETE Operations 2 | 3 | The DELETE statement is used to remove documents from MongoDB collections. This page describes the syntax and features of DELETE operations in QueryLeaf. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | DELETE basic | ✅ Full | Remove documents from collections | 10 | | WHERE clause | ✅ Full | Standard filtering conditions | 11 | | Complex conditions | ✅ Full | Supports AND, OR, and nested conditions | 12 | | Nested field filters | ✅ Full | Filter using dot notation | 13 | | Array element filters | ✅ Full | Filter using array indexing | 14 | | Bulk delete | ✅ Full | Delete multiple documents at once | 15 | | RETURNING clause | ❌ None | Cannot return deleted documents | 16 | | LIMIT in DELETE | ❌ None | Cannot limit number of deleted documents | 17 | 18 | ## Basic Syntax 19 | 20 | ```sql 21 | DELETE FROM collection 22 | [WHERE conditions] 23 | ``` 24 | 25 | ## Examples 26 | 27 | ### Basic DELETE 28 | 29 | ```sql 30 | -- Delete all documents in a collection (use with caution!) 31 | DELETE FROM users 32 | 33 | -- Delete documents matching a condition 34 | DELETE FROM products 35 | WHERE category = 'Discontinued' 36 | 37 | -- Delete a specific document by _id 38 | DELETE FROM users 39 | WHERE _id = '507f1f77bcf86cd799439011' 40 | ``` 41 | 42 | ### Complex Conditions 43 | 44 | ```sql 45 | -- Delete with multiple conditions 46 | DELETE FROM orders 47 | WHERE status = 'cancelled' AND createdAt < '2023-01-01' 48 | 49 | -- Delete with OR condition 50 | DELETE FROM sessions 51 | WHERE expiry < '2023-06-01' OR status = 'invalid' 52 | ``` 53 | 54 | ### Working with Nested Fields 55 | 56 | ```sql 57 | -- Delete based on nested field condition 58 | DELETE FROM users 59 | WHERE address.country = 'Deprecated Country Name' 60 | 61 | -- Delete based on nested array element 62 | DELETE FROM orders 63 | WHERE items[0].productId = 'DISC-001' 64 | ``` 65 | 66 | ## Translation to MongoDB 67 | 68 | When you run a DELETE query, QueryLeaf translates it to MongoDB operations: 69 | 70 | | SQL Feature | MongoDB Equivalent | 71 | |-------------|-------------------| 72 | | DELETE FROM collection | db.collection() | 73 | | No WHERE clause | deleteMany({}) | 74 | | WHERE with _id | deleteOne({ _id: ... }) | 75 | | WHERE with conditions | deleteMany({ conditions }) | 76 | | Nested fields in WHERE | Dot notation in filter | 77 | 78 | ### Example Translation 79 | 80 | SQL: 81 | ```sql 82 | DELETE FROM orders 83 | WHERE status = 'cancelled' AND createdAt < '2023-01-01' 84 | ``` 85 | 86 | MongoDB: 87 | ```javascript 88 | db.collection('orders').deleteMany({ 89 | status: 'cancelled', 90 | createdAt: { $lt: '2023-01-01' } 91 | }) 92 | ``` 93 | 94 | ## Performance Considerations 95 | 96 | - Add appropriate indexes for fields used in WHERE conditions 97 | - Deleting a specific document by `_id` is the most efficient operation 98 | - For large-scale deletions, consider using MongoDB's bulk operations or aggregation pipeline 99 | - Be careful with DELETE operations without WHERE clauses, as they will remove all documents 100 | 101 | ## Best Practices 102 | 103 | - Always use WHERE conditions unless you explicitly want to delete all documents 104 | - Consider using soft deletes (setting an 'isDeleted' flag) for important data 105 | - Back up data before performing large DELETE operations 106 | - For very large collections, consider a batched approach to deleting documents 107 | 108 | ## Related Pages 109 | 110 | - [INSERT Operations](insert.md) 111 | - [UPDATE Operations](update.md) 112 | - [Working with Nested Fields](nested-fields.md) 113 | - [SELECT Queries](select.md) -------------------------------------------------------------------------------- /docs/sql-syntax/group-by.md: -------------------------------------------------------------------------------- 1 | # GROUP BY and Aggregation 2 | 3 | The GROUP BY clause is used to group documents based on specified fields and perform aggregation operations. This page explains how to use GROUP BY and aggregation functions in QueryLeaf. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | Basic GROUP BY | ✅ Full | Group by single or multiple fields | 10 | | COUNT(*) | ✅ Full | Count documents in each group | 11 | | COUNT(field) | ✅ Full | Count non-null values in each group | 12 | | SUM | ✅ Full | Sum of values in each group | 13 | | AVG | ✅ Full | Average of values in each group | 14 | | MIN | ✅ Full | Minimum value in each group | 15 | | MAX | ✅ Full | Maximum value in each group | 16 | | Nested field grouping | ✅ Full | Group by embedded document fields | 17 | | ORDER BY with GROUP BY | ✅ Full | Sort grouped results | 18 | | HAVING clause | ⚠️ Limited | Limited support for filtering groups | 19 | | Complex expressions | ⚠️ Limited | Limited support in grouping expressions | 20 | | Window functions | ❌ None | No support for window functions | 21 | 22 | ## GROUP BY Syntax 23 | 24 | ```sql 25 | SELECT 26 | columns, 27 | AGG_FUNCTION(column) AS alias 28 | FROM collection 29 | [WHERE conditions] 30 | GROUP BY columns 31 | [ORDER BY columns] 32 | ``` 33 | 34 | ## Supported Aggregation Functions 35 | 36 | QueryLeaf supports the following aggregation functions: 37 | 38 | | Function | Description | 39 | |----------|-------------| 40 | | COUNT(*) | Count the number of documents in each group | 41 | | COUNT(field) | Count non-null values of a field in each group | 42 | | SUM(field) | Sum of field values in each group | 43 | | AVG(field) | Average of field values in each group | 44 | | MIN(field) | Minimum field value in each group | 45 | | MAX(field) | Maximum field value in each group | 46 | 47 | ## Basic GROUP BY Examples 48 | 49 | ### Simple GROUP BY 50 | 51 | ```sql 52 | -- Group by single field 53 | SELECT category, COUNT(*) as count 54 | FROM products 55 | GROUP BY category 56 | 57 | -- Group by single field with conditions 58 | SELECT status, COUNT(*) as count 59 | FROM orders 60 | WHERE createdAt > '2023-01-01' 61 | GROUP BY status 62 | ``` 63 | 64 | ### Multiple Aggregation Functions 65 | 66 | ```sql 67 | -- Multiple aggregation functions 68 | SELECT 69 | category, 70 | COUNT(*) as count, 71 | AVG(price) as avg_price, 72 | MIN(price) as min_price, 73 | MAX(price) as max_price, 74 | SUM(price) as total_value 75 | FROM products 76 | GROUP BY category 77 | ``` 78 | 79 | ### Sorting Grouped Results 80 | 81 | ```sql 82 | -- GROUP BY with ORDER BY 83 | SELECT category, COUNT(*) as count 84 | FROM products 85 | GROUP BY category 86 | ORDER BY count DESC 87 | 88 | -- Multiple sort fields 89 | SELECT status, COUNT(*) as count, SUM(total) as revenue 90 | FROM orders 91 | GROUP BY status 92 | ORDER BY revenue DESC, count ASC 93 | ``` 94 | 95 | ## GROUP BY with Multiple Fields 96 | 97 | ```sql 98 | -- Group by multiple fields 99 | SELECT 100 | category, 101 | manufacturer, 102 | COUNT(*) as count, 103 | AVG(price) as avg_price 104 | FROM products 105 | GROUP BY category, manufacturer 106 | ``` 107 | 108 | ## GROUP BY with Nested Fields 109 | 110 | ```sql 111 | -- Group by nested field 112 | SELECT 113 | address.city, 114 | COUNT(*) as user_count 115 | FROM users 116 | GROUP BY address.city 117 | ``` 118 | 119 | ## Translation to MongoDB 120 | 121 | QueryLeaf translates GROUP BY operations to MongoDB's aggregation framework: 122 | 123 | | SQL Feature | MongoDB Equivalent | 124 | |-------------|-------------------| 125 | | GROUP BY | $group stage | 126 | | COUNT(*) | $sum: 1 | 127 | | SUM(field) | $sum: '$field' | 128 | | AVG(field) | $avg: '$field' | 129 | | MIN(field) | $min: '$field' | 130 | | MAX(field) | $max: '$field' | 131 | | WHERE with GROUP BY | $match stage before $group | 132 | | ORDER BY with GROUP BY | $sort stage after $group | 133 | 134 | ### Example Translation 135 | 136 | SQL: 137 | ```sql 138 | SELECT 139 | category, 140 | COUNT(*) as count, 141 | AVG(price) as avg_price 142 | FROM products 143 | WHERE stock > 0 144 | GROUP BY category 145 | ORDER BY count DESC 146 | ``` 147 | 148 | MongoDB: 149 | ```javascript 150 | db.collection('products').aggregate([ 151 | { $match: { stock: { $gt: 0 } } }, 152 | { 153 | $group: { 154 | _id: '$category', 155 | category: { $first: '$category' }, 156 | count: { $sum: 1 }, 157 | avg_price: { $avg: '$price' } 158 | } 159 | }, 160 | { $sort: { count: -1 } } 161 | ]) 162 | ``` 163 | 164 | ## Performance Considerations 165 | 166 | - GROUP BY operations use MongoDB's aggregation framework, which can be resource-intensive 167 | - Create indexes on fields used in GROUP BY and WHERE clauses 168 | - Be mindful of memory usage when grouping large collections 169 | - Consider using the allowDiskUse option for large datasets (via MongoDB driver) 170 | - Aggregation pipelines with multiple stages can impact performance 171 | 172 | ## Limitations 173 | 174 | - Limited support for HAVING clause 175 | - No support for window functions 176 | - Limited support for complex expressions in GROUP BY 177 | - Non-standard handling of NULL values in grouping 178 | 179 | ## Best Practices 180 | 181 | - Use WHERE conditions to limit the documents before grouping 182 | - Create indexes on frequently grouped fields 183 | - Keep aggregation pipelines simple when possible 184 | - Consider using MongoDB's native aggregation for very complex operations 185 | - For time-based grouping, consider pre-processing dates 186 | 187 | ## Related Pages 188 | 189 | - [SELECT Queries](select.md) 190 | - [Using JOINs](joins.md) 191 | - [Working with Nested Fields](nested-fields.md) -------------------------------------------------------------------------------- /docs/sql-syntax/insert.md: -------------------------------------------------------------------------------- 1 | # INSERT Operations 2 | 3 | The INSERT statement is used to add new documents to MongoDB collections. This page describes the syntax and features of INSERT operations in QueryLeaf. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | INSERT basic | ✅ Full | Standard single-document insertion | 10 | | Multiple rows | ✅ Full | Batch insertions supported | 11 | | Custom _id | ✅ Full | Specify your own _id value | 12 | | Nested objects | ✅ Full | Supports full MongoDB document structure | 13 | | Arrays | ✅ Full | Both simple and complex arrays supported | 14 | | NULL values | ✅ Full | Explicit NULL values supported | 15 | | Returning clause | ❌ None | Cannot return inserted documents | 16 | | Conflict handling | ❌ None | No ON CONFLICT support | 17 | 18 | ## Basic Syntax 19 | 20 | ```sql 21 | INSERT INTO collection (field1, field2, ...) 22 | VALUES (value1, value2, ...), (value1, value2, ...), ... 23 | ``` 24 | 25 | ## Examples 26 | 27 | ### Basic INSERT 28 | 29 | ```sql 30 | -- Insert a single document 31 | INSERT INTO users (name, email, age) 32 | VALUES ('John Doe', 'john@example.com', 30) 33 | 34 | -- Insert multiple documents 35 | INSERT INTO products (name, price, category) 36 | VALUES 37 | ('Laptop', 999.99, 'Electronics'), 38 | ('Desk Chair', 199.99, 'Furniture'), 39 | ('Coffee Mug', 14.99, 'Kitchenware') 40 | ``` 41 | 42 | ### Inserting with MongoDB-Specific Types 43 | 44 | ```sql 45 | -- Insert with nested object 46 | INSERT INTO users (name, email, address) 47 | VALUES ( 48 | 'Jane Smith', 49 | 'jane@example.com', 50 | { 51 | "street": "123 Main St", 52 | "city": "New York", 53 | "state": "NY", 54 | "zip": "10001" 55 | } 56 | ) 57 | 58 | -- Insert with array 59 | INSERT INTO users (name, email, tags) 60 | VALUES ( 61 | 'Bob Johnson', 62 | 'bob@example.com', 63 | ['developer', 'nodejs', 'mongodb'] 64 | ) 65 | 66 | -- Insert with both nested objects and arrays 67 | INSERT INTO orders (customer, items, shipTo) 68 | VALUES ( 69 | 'Alice Brown', 70 | [ 71 | { "product": "Laptop", "price": 999.99, "quantity": 1 }, 72 | { "product": "Mouse", "price": 24.99, "quantity": 2 } 73 | ], 74 | { 75 | "address": "456 Oak St", 76 | "city": "Boston", 77 | "state": "MA", 78 | "zip": "02101" 79 | } 80 | ) 81 | ``` 82 | 83 | ## NULL Values 84 | 85 | ```sql 86 | -- Insert with NULL value 87 | INSERT INTO users (name, email, phone) 88 | VALUES ('Chris Wilson', 'chris@example.com', NULL) 89 | ``` 90 | 91 | ## Translation to MongoDB 92 | 93 | When you run an INSERT query, QueryLeaf translates it to MongoDB operations: 94 | 95 | | SQL Feature | MongoDB Equivalent | 96 | |-------------|-------------------| 97 | | INSERT INTO collection | db.collection() | 98 | | VALUES | Document objects | 99 | | Single row | insertOne() | 100 | | Multiple rows | insertMany() | 101 | | Nested objects | Embedded documents | 102 | | Arrays | MongoDB arrays | 103 | 104 | ### Example Translation 105 | 106 | SQL: 107 | ```sql 108 | INSERT INTO users (name, email, address) 109 | VALUES ( 110 | 'Jane Smith', 111 | 'jane@example.com', 112 | { 113 | "street": "123 Main St", 114 | "city": "New York", 115 | "state": "NY", 116 | "zip": "10001" 117 | } 118 | ) 119 | ``` 120 | 121 | MongoDB: 122 | ```javascript 123 | db.collection('users').insertOne({ 124 | name: 'Jane Smith', 125 | email: 'jane@example.com', 126 | address: { 127 | street: '123 Main St', 128 | city: 'New York', 129 | state: 'NY', 130 | zip: '10001' 131 | } 132 | }) 133 | ``` 134 | 135 | ## Performance Considerations 136 | 137 | - Use bulk inserts (multiple rows in a single INSERT statement) for better performance 138 | - Consider adding appropriate indexes before bulk inserts 139 | - Be aware of document size limits in MongoDB (16MB per document) 140 | - For very large imports, consider using MongoDB's native tools or bulk operations 141 | 142 | ## Working with MongoDB ObjectIDs 143 | 144 | MongoDB automatically generates an `_id` field for new documents if you don't specify one. You can also provide your own ObjectID value: 145 | 146 | ```sql 147 | -- Let MongoDB generate an _id 148 | INSERT INTO users (name, email) 149 | VALUES ('Jane Smith', 'jane@example.com') 150 | 151 | -- Specify a string that will be converted to ObjectID (must be valid 24-character hex) 152 | INSERT INTO users (_id, name, email) 153 | VALUES ('507f1f77bcf86cd799439011', 'Mark Davis', 'mark@example.com') 154 | 155 | -- Use a custom string ID (will not be converted to ObjectID) 156 | INSERT INTO users (_id, name, email) 157 | VALUES ('custom_id_1', 'Sarah Jones', 'sarah@example.com') 158 | ``` 159 | 160 | ### How ObjectID Conversion Works 161 | 162 | QueryLeaf automatically converts string values to MongoDB ObjectID objects when: 163 | 164 | 1. The field name is `_id` 165 | 2. The field name ends with `Id` (e.g., `userId`) 166 | 3. The field name ends with `Ids` (for arrays of IDs) 167 | 4. The string follows the MongoDB ObjectID format (24 hex characters) 168 | 169 | For example, in this INSERT statement: 170 | 171 | ```sql 172 | INSERT INTO orders (customerId, products) 173 | VALUES ( 174 | '507f1f77bcf86cd799439011', 175 | [ 176 | { productId: '609f1f77bcf86cd799439a22', quantity: 2 } 177 | ] 178 | ) 179 | ``` 180 | 181 | Both `customerId` and the `productId` inside the array will be automatically converted to MongoDB ObjectID objects. 182 | 183 | ## Related Pages 184 | 185 | - [UPDATE Operations](update.md) 186 | - [DELETE Operations](delete.md) 187 | - [Working with Nested Fields](nested-fields.md) 188 | - [Working with Array Access](array-access.md) -------------------------------------------------------------------------------- /docs/sql-syntax/joins.md: -------------------------------------------------------------------------------- 1 | # Using JOINs 2 | 3 | QueryLeaf supports JOIN operations, which allow you to combine data from multiple MongoDB collections. This page explains how to use JOINs in your SQL queries. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | INNER JOIN | ✅ Full | Standard JOIN keyword | 10 | | LEFT JOIN | ❌ None | Not currently supported | 11 | | RIGHT JOIN | ❌ None | Not currently supported | 12 | | FULL OUTER JOIN | ❌ None | Not currently supported | 13 | | JOIN conditions | ⚠️ Limited | Equal conditions only (ON a.id = b.id) | 14 | | Multiple JOINs | ✅ Full | Chain multiple tables together | 15 | | JOIN with WHERE | ✅ Full | Filter joined results | 16 | | JOIN with GROUP BY | ✅ Full | Aggregate joined data | 17 | | JOIN with ORDER BY | ✅ Full | Sort joined results | 18 | | JOIN with LIMIT | ✅ Full | Paginate joined results | 19 | | Complex JOIN conditions | ❌ None | No support for AND/OR in JOIN conditions | 20 | 21 | ## JOIN Syntax 22 | 23 | ```sql 24 | SELECT columns 25 | FROM collection1 [alias1] 26 | JOIN collection2 [alias2] ON alias1.field = alias2.field 27 | [WHERE conditions] 28 | [GROUP BY columns] 29 | [ORDER BY columns] 30 | [LIMIT count] 31 | ``` 32 | 33 | ## Supported JOIN Types 34 | 35 | Currently, QueryLeaf supports: 36 | 37 | - INNER JOIN (default JOIN keyword) 38 | 39 | Other JOIN types (LEFT, RIGHT, FULL OUTER) are not currently supported. 40 | 41 | ## Basic JOIN Examples 42 | 43 | ### Simple JOIN between Collections 44 | 45 | ```sql 46 | -- Join users and orders collections 47 | SELECT u.name, o.total, o.createdAt 48 | FROM users u 49 | JOIN orders o ON u._id = o.userId 50 | ``` 51 | 52 | ### JOIN with Conditions 53 | 54 | ```sql 55 | -- Join with WHERE conditions 56 | SELECT u.name, o.total, o.status 57 | FROM users u 58 | JOIN orders o ON u._id = o.userId 59 | WHERE o.status = 'completed' AND u.accountType = 'premium' 60 | ``` 61 | 62 | ### JOIN with Sorting and Limits 63 | 64 | ```sql 65 | -- Join with ORDER BY and LIMIT 66 | SELECT u.name, o.total 67 | FROM users u 68 | JOIN orders o ON u._id = o.userId 69 | ORDER BY o.total DESC 70 | LIMIT 10 71 | ``` 72 | 73 | ## Working with Multiple JOINs 74 | 75 | ```sql 76 | -- Multiple JOINs 77 | SELECT u.name, o._id as order_id, p.name as product_name 78 | FROM users u 79 | JOIN orders o ON u._id = o.userId 80 | JOIN order_items oi ON o._id = oi.orderId 81 | JOIN products p ON oi.productId = p._id 82 | WHERE o.status = 'completed' 83 | ``` 84 | 85 | ## JOINs with Aggregation 86 | 87 | ```sql 88 | -- JOIN with GROUP BY 89 | SELECT 90 | u.accountType, 91 | COUNT(*) as order_count, 92 | SUM(o.total) as total_spent 93 | FROM users u 94 | JOIN orders o ON u._id = o.userId 95 | GROUP BY u.accountType 96 | ``` 97 | 98 | ## Using Aliases 99 | 100 | ```sql 101 | -- Using table aliases for clarity 102 | SELECT 103 | customer.name as customer_name, 104 | customer.email, 105 | purchase.order_id, 106 | purchase.amount 107 | FROM users customer 108 | JOIN orders purchase ON customer._id = purchase.userId 109 | ``` 110 | 111 | ## Translation to MongoDB 112 | 113 | QueryLeaf translates JOINs to MongoDB's aggregation framework using `$lookup` and other stages: 114 | 115 | | SQL Feature | MongoDB Equivalent | 116 | |-------------|-------------------| 117 | | JOIN | $lookup stage | 118 | | ON condition | localField and foreignField in $lookup | 119 | | WHERE | $match stage | 120 | | ORDER BY | $sort stage | 121 | | LIMIT | $limit stage | 122 | | GROUP BY with JOINs | $lookup + $group stages | 123 | 124 | ### Example Translation 125 | 126 | SQL: 127 | ```sql 128 | SELECT u.name, o.total, o.status 129 | FROM users u 130 | JOIN orders o ON u._id = o.userId 131 | WHERE o.status = 'completed' 132 | ``` 133 | 134 | MongoDB: 135 | ```javascript 136 | db.collection('users').aggregate([ 137 | { 138 | $lookup: { 139 | from: 'orders', 140 | localField: '_id', 141 | foreignField: 'userId', 142 | as: 'o' 143 | } 144 | }, 145 | { $unwind: '$o' }, 146 | { $match: { 'o.status': 'completed' } }, 147 | { 148 | $project: { 149 | 'name': 1, 150 | 'o.total': 1, 151 | 'o.status': 1 152 | } 153 | } 154 | ]) 155 | ``` 156 | 157 | ## Performance Considerations 158 | 159 | - JOINs in MongoDB are implemented using the aggregation framework, which can be resource-intensive 160 | - Create indexes on the JOIN fields (both the local and foreign fields) 161 | - Consider denormalizing data for frequently joined collections 162 | - Use WHERE conditions to limit the documents before the JOIN when possible 163 | - Be mindful of memory usage when JOINing large collections 164 | 165 | ## Limitations 166 | 167 | - Only INNER JOIN is currently supported 168 | - JOINs can be significantly slower than in traditional SQL databases 169 | - Complex join conditions (beyond equality) are not supported 170 | - Performance degrades quickly with multiple JOINs 171 | 172 | ## Best Practices 173 | 174 | - Use JOINs sparingly in MongoDB - consider alternative schema designs when possible 175 | - Always create indexes on JOIN fields 176 | - Keep JOIN chains short (prefer 1-2 JOINs rather than 3+) 177 | - Use aliases for better readability 178 | - Consider denormalizing data for frequently accessed relationships 179 | 180 | ## Related Pages 181 | 182 | - [SELECT Queries](select.md) 183 | - [GROUP BY and Aggregation](group-by.md) -------------------------------------------------------------------------------- /docs/sql-syntax/nested-fields.md: -------------------------------------------------------------------------------- 1 | # Working with Nested Fields 2 | 3 | MongoDB documents can contain nested fields (embedded documents), and QueryLeaf provides SQL syntax for working with these structures. This page explains how to query, update, and insert data with nested fields. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | SELECT nested fields | ✅ Full | Project specific embedded fields | 10 | | WHERE with nested fields | ✅ Full | Filter on embedded document fields | 11 | | UPDATE nested fields | ✅ Full | Modify specific embedded fields | 12 | | INSERT with nested objects | ✅ Full | Create documents with embedded structures | 13 | | Multilevel nesting | ✅ Full | Support for deeply nested paths (a.b.c.d) | 14 | | Nested field indexing | ⚠️ N/A | MongoDB-side feature, not SQL syntax | 15 | | Nested array operations | ⚠️ Limited | See [Array Access](array-access.md) page | 16 | | Dot notation in expressions | ⚠️ Limited | Limited support in complex expressions | 17 | 18 | ## Nested Field Syntax 19 | 20 | In QueryLeaf, you can access nested fields using dot notation: 21 | 22 | ```sql 23 | collection.field.subfield 24 | ``` 25 | 26 | ## Querying Nested Fields 27 | 28 | ### SELECT with Nested Fields 29 | 30 | ```sql 31 | -- Select nested fields 32 | SELECT name, address.city, address.state 33 | FROM users 34 | 35 | -- Filter based on nested fields 36 | SELECT name, email 37 | FROM users 38 | WHERE address.city = 'New York' 39 | 40 | -- Multiple nested levels 41 | SELECT name, shipping.address.street, shipping.address.city 42 | FROM orders 43 | WHERE shipping.address.country = 'USA' 44 | ``` 45 | 46 | ### WHERE Conditions with Nested Fields 47 | 48 | ```sql 49 | -- Simple equality with nested field 50 | SELECT * FROM users WHERE profile.language = 'en' 51 | 52 | -- Comparison operators 53 | SELECT * FROM products WHERE details.weight > 5 54 | 55 | -- Multiple nested field conditions 56 | SELECT * FROM users 57 | WHERE address.city = 'San Francisco' AND profile.verified = true 58 | ``` 59 | 60 | ## Updating Nested Fields 61 | 62 | ```sql 63 | -- Update a single nested field 64 | UPDATE users 65 | SET address.city = 'Chicago' 66 | WHERE _id = '507f1f77bcf86cd799439011' 67 | 68 | -- Update multiple nested fields 69 | UPDATE users 70 | SET 71 | address.street = '123 Oak St', 72 | address.city = 'Boston', 73 | address.state = 'MA', 74 | address.zip = '02101' 75 | WHERE email = 'john@example.com' 76 | 77 | -- Update deeply nested fields 78 | UPDATE orders 79 | SET customer.shipping.address.zipCode = '94105' 80 | WHERE _id = '60a6c5ef837f3d2d54c965f3' 81 | ``` 82 | 83 | ## Inserting Documents with Nested Fields 84 | 85 | ```sql 86 | -- Insert with nested object 87 | INSERT INTO users (name, email, address) 88 | VALUES ( 89 | 'Jane Smith', 90 | 'jane@example.com', 91 | { 92 | "street": "123 Main St", 93 | "city": "New York", 94 | "state": "NY", 95 | "zip": "10001" 96 | } 97 | ) 98 | 99 | -- Insert with multiple nested objects 100 | INSERT INTO orders (customer, shipping, billing) 101 | VALUES ( 102 | 'Bob Johnson', 103 | { 104 | "method": "Express", 105 | "address": { 106 | "street": "456 Pine St", 107 | "city": "Seattle", 108 | "state": "WA", 109 | "zip": "98101" 110 | } 111 | }, 112 | { 113 | "method": "Credit Card", 114 | "address": { 115 | "street": "456 Pine St", 116 | "city": "Seattle", 117 | "state": "WA", 118 | "zip": "98101" 119 | } 120 | } 121 | ) 122 | ``` 123 | 124 | ## Translation to MongoDB 125 | 126 | When working with nested fields, QueryLeaf translates SQL to MongoDB using dot notation: 127 | 128 | | SQL | MongoDB | 129 | |-----|--------| 130 | | `address.city` in SELECT | Projection with `'address.city': 1` | 131 | | `address.city = 'New York'` in WHERE | Filter with `'address.city': 'New York'` | 132 | | `SET address.city = 'Chicago'` | Update with `$set: {'address.city': 'Chicago'}` | 133 | | Nested object in INSERT | Embedded document in insertOne/insertMany | 134 | 135 | ### Example Translation 136 | 137 | SQL: 138 | ```sql 139 | SELECT name, address.city, address.state 140 | FROM users 141 | WHERE address.country = 'USA' 142 | ``` 143 | 144 | MongoDB: 145 | ```javascript 146 | db.collection('users').find( 147 | { 'address.country': 'USA' }, 148 | { name: 1, 'address.city': 1, 'address.state': 1 } 149 | ) 150 | ``` 151 | 152 | ## Performance Considerations 153 | 154 | - Create indexes on frequently queried nested fields: `db.users.createIndex({'address.city': 1})` 155 | - Be aware that updating deeply nested fields can be less efficient than updating top-level fields 156 | - Consider denormalizing data if you frequently query specific nested fields 157 | 158 | ## Best Practices 159 | 160 | - Use nested fields for data that logically belongs together (e.g., address components) 161 | - Keep nesting depth reasonable (2-3 levels) for optimal performance 162 | - Consider MongoDB's document size limits (16MB) when working with deeply nested structures 163 | - For complex nested structures that need independent querying, consider using separate collections with references 164 | 165 | ## Related Pages 166 | 167 | - [SELECT Queries](select.md) 168 | - [UPDATE Operations](update.md) 169 | - [INSERT Operations](insert.md) 170 | - [Working with Array Access](array-access.md) -------------------------------------------------------------------------------- /docs/sql-syntax/update.md: -------------------------------------------------------------------------------- 1 | # UPDATE Operations 2 | 3 | The UPDATE statement is used to modify existing documents in MongoDB collections. This page describes the syntax and features of UPDATE operations in QueryLeaf. 4 | 5 | ## Feature Support 6 | 7 | | Feature | Support | Notes | 8 | |---------|---------|-------| 9 | | UPDATE basic | ✅ Full | Simple field updates | 10 | | WHERE clause | ✅ Full | Standard filtering conditions | 11 | | Multiple fields | ✅ Full | Update multiple fields at once | 12 | | Nested fields | ✅ Full | Update embedded document fields | 13 | | Array elements | ✅ Full | Update specific array indexes | 14 | | NULL values | ✅ Full | Set fields to NULL | 15 | | Expressions | ⚠️ Limited | Limited support for expressions in SET | 16 | | Increments | ❌ None | No direct equivalent to `SET x = x + 1` | 17 | | Array operators | ❌ None | No $push, $pull, $addToSet support | 18 | | RETURNING clause | ❌ None | Cannot return updated documents | 19 | 20 | ## Basic Syntax 21 | 22 | ```sql 23 | UPDATE collection 24 | SET field1 = value1, field2 = value2, ... 25 | [WHERE conditions] 26 | ``` 27 | 28 | ## Examples 29 | 30 | ### Basic UPDATE 31 | 32 | ```sql 33 | -- Update a single field for all documents 34 | UPDATE users 35 | SET status = 'active' 36 | 37 | -- Update with WHERE condition 38 | UPDATE products 39 | SET price = 1299.99 40 | WHERE name = 'Premium Laptop' 41 | 42 | -- Update multiple fields 43 | UPDATE users 44 | SET 45 | status = 'inactive', 46 | lastUpdated = '2023-06-15' 47 | WHERE lastLogin < '2023-01-01' 48 | ``` 49 | 50 | ### Updating Nested Fields 51 | 52 | ```sql 53 | -- Update nested field 54 | UPDATE users 55 | SET address.city = 'San Francisco', address.state = 'CA' 56 | WHERE _id = '507f1f77bcf86cd799439011' 57 | 58 | -- Update deeply nested field 59 | UPDATE orders 60 | SET shipTo.address.zipCode = '94105' 61 | WHERE _id = '60a6c5ef837f3d2d54c965f3' 62 | ``` 63 | 64 | ### Updating Array Elements 65 | 66 | ```sql 67 | -- Update specific array element 68 | UPDATE orders 69 | SET items[0].status = 'shipped' 70 | WHERE _id = '60a6c5ef837f3d2d54c965f3' 71 | 72 | -- Update array element based on condition 73 | UPDATE inventory 74 | SET items[0].quantity = 100 75 | WHERE items[0].sku = 'LAPTOP-001' 76 | ``` 77 | 78 | ## NULL Values 79 | 80 | ```sql 81 | -- Set field to NULL 82 | UPDATE users 83 | SET phone = NULL 84 | WHERE _id = '507f1f77bcf86cd799439011' 85 | ``` 86 | 87 | ## Translation to MongoDB 88 | 89 | When you run an UPDATE query, QueryLeaf translates it to MongoDB operations: 90 | 91 | | SQL Feature | MongoDB Equivalent | 92 | |-------------|-------------------| 93 | | UPDATE collection | db.collection() | 94 | | SET field = value | $set operator | 95 | | WHERE | Query filter object | 96 | | Nested fields | Dot notation in $set | 97 | | Array elements | Dot notation with indices | 98 | 99 | ### Example Translation 100 | 101 | SQL: 102 | ```sql 103 | UPDATE users 104 | SET status = 'inactive', lastUpdated = '2023-06-15' 105 | WHERE lastLogin < '2023-01-01' 106 | ``` 107 | 108 | MongoDB: 109 | ```javascript 110 | db.collection('users').updateMany( 111 | { lastLogin: { $lt: '2023-01-01' } }, 112 | { $set: { status: 'inactive', lastUpdated: '2023-06-15' } } 113 | ) 114 | ``` 115 | 116 | ## Performance Considerations 117 | 118 | - Updates with specific `_id` values are the most efficient 119 | - Add appropriate indexes for fields used in WHERE conditions 120 | - Be mindful of update operations on large collections 121 | - Consider using the MongoDB driver directly for complex update operations 122 | 123 | ## Working with MongoDB ObjectIDs 124 | 125 | When updating documents using ObjectID fields: 126 | 127 | ```sql 128 | -- Update by _id (string will be converted to ObjectID) 129 | UPDATE users 130 | SET status = 'verified' 131 | WHERE _id = '507f1f77bcf86cd799439011' 132 | 133 | -- Update by reference ID field 134 | UPDATE orders 135 | SET status = 'shipped' 136 | WHERE customerId = '507f1f77bcf86cd799439011' 137 | ``` 138 | 139 | QueryLeaf automatically converts the string value in the WHERE clause to a MongoDB ObjectID object when the field name is `_id` or ends with `Id` and the string is a valid 24-character hexadecimal string. 140 | 141 | ## Limitations 142 | 143 | - Limited support for complex update operations (e.g., $inc, $push) 144 | - No direct support for MongoDB's array update operators like $push, $pull 145 | - No direct support for positional updates in arrays 146 | 147 | ## Related Pages 148 | 149 | - [INSERT Operations](insert.md) 150 | - [DELETE Operations](delete.md) 151 | - [Working with Nested Fields](nested-fields.md) 152 | - [Working with Array Access](array-access.md) -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #4caf50; 3 | --md-primary-fg-color--light: #8bc34a; 4 | --md-primary-fg-color--dark: #2e7d32; 5 | --md-accent-fg-color: #8bc34a; 6 | --md-primary-bg-color: #ffffff; 7 | --md-text-color: #333333; 8 | --md-code-bg-color: #f5f7f9; 9 | --md-code-fg-color: #111111; 10 | } 11 | 12 | /* Global styles */ 13 | body, html { 14 | scroll-behavior: smooth; 15 | } 16 | 17 | .md-header { 18 | background-color: var(--md-primary-fg-color); 19 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | .md-tabs { 23 | background-color: var(--md-primary-fg-color--dark); 24 | } 25 | 26 | /* Top Navigation Tabs */ 27 | .md-tabs__link { 28 | margin-top: 0.4rem; 29 | padding: 0.6rem 0.8rem; 30 | font-weight: 500; 31 | font-size: 0.9rem; 32 | opacity: 0.9; 33 | transition: all 0.2s ease; 34 | } 35 | 36 | .md-tabs__link:hover, 37 | .md-tabs__link--active { 38 | opacity: 1; 39 | font-weight: 600; 40 | } 41 | 42 | .md-tabs__item--active { 43 | position: relative; 44 | } 45 | 46 | .md-tabs__item--active::after { 47 | content: ""; 48 | position: absolute; 49 | bottom: 0; 50 | left: 50%; 51 | transform: translateX(-50%); 52 | width: 40%; 53 | height: 3px; 54 | background-color: white; 55 | border-radius: 3px 3px 0 0; 56 | } 57 | 58 | /* Adjust tab heights */ 59 | .md-tabs__list { 60 | height: 3rem; 61 | } 62 | 63 | .md-tabs__item { 64 | height: 3rem; 65 | padding: 0 0.5rem; 66 | } 67 | 68 | /* Typography */ 69 | .md-typeset h1 { 70 | color: var(--md-primary-fg-color--dark); 71 | font-weight: 700; 72 | margin-bottom: 1.5rem; 73 | letter-spacing: -0.02em; 74 | } 75 | 76 | .md-typeset h2 { 77 | color: var(--md-primary-fg-color--dark); 78 | font-weight: 600; 79 | margin-top: 2rem; 80 | margin-bottom: 1rem; 81 | letter-spacing: -0.01em; 82 | } 83 | 84 | .md-typeset h3 { 85 | font-weight: 600; 86 | color: #333; 87 | margin-top: 1.5rem; 88 | margin-bottom: 0.75rem; 89 | } 90 | 91 | .md-typeset p { 92 | line-height: 1.6; 93 | color: var(--md-text-color); 94 | } 95 | 96 | .md-typeset a { 97 | color: var(--md-primary-fg-color); 98 | text-decoration: none; 99 | transition: color 0.2s ease-in-out; 100 | } 101 | 102 | .md-typeset a:hover { 103 | color: var(--md-primary-fg-color--dark); 104 | text-decoration: underline; 105 | } 106 | 107 | /* Code blocks */ 108 | .md-typeset code { 109 | background-color: var(--md-code-bg-color); 110 | color: var(--md-code-fg-color); 111 | padding: 0.2em 0.4em; 112 | border-radius: 3px; 113 | font-size: 0.9em; 114 | } 115 | 116 | .md-typeset pre > code { 117 | font-size: 0.85em; 118 | line-height: 1.6; 119 | padding: 1rem; 120 | } 121 | 122 | /* Buttons */ 123 | .md-button { 124 | transition: all 0.3s ease-in-out; 125 | font-weight: 500; 126 | border-radius: 4px; 127 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 128 | padding: 0.5rem 1.5rem; 129 | } 130 | 131 | .md-button:hover { 132 | transform: translateY(-2px); 133 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 134 | } 135 | 136 | .md-button--primary { 137 | background-color: var(--md-primary-fg-color); 138 | color: var(--md-primary-bg-color) !important; 139 | border: none; 140 | } 141 | 142 | .md-button--primary:hover { 143 | background-color: var(--md-primary-fg-color--dark); 144 | color: white !important; 145 | } 146 | 147 | /* Logo and navigation */ 148 | .md-header-nav__button.md-logo img, 149 | .md-header-nav__button.md-logo svg { 150 | height: 2rem; 151 | } 152 | 153 | .home-logo { 154 | display: block; 155 | margin: 0 auto; 156 | max-width: 150px; 157 | height: auto; 158 | } 159 | 160 | /* Tables */ 161 | .md-typeset table:not([class]) { 162 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); 163 | border-radius: 4px; 164 | overflow: hidden; 165 | } 166 | 167 | .md-typeset table:not([class]) th { 168 | background-color: var(--md-primary-fg-color); 169 | color: white; 170 | font-weight: 600; 171 | padding: 0.9rem; 172 | } 173 | 174 | .md-typeset table:not([class]) td { 175 | padding: 0.9rem; 176 | border-bottom: 1px solid #f0f0f0; 177 | } 178 | 179 | .md-typeset table:not([class]) tr:last-child td { 180 | border-bottom: none; 181 | } 182 | 183 | /* Footer */ 184 | .md-footer-meta { 185 | background-color: var(--md-primary-fg-color--dark); 186 | } 187 | 188 | .md-footer-nav { 189 | background-color: #1c1c1c; 190 | } 191 | 192 | /* Utility classes */ 193 | .center-text { 194 | text-align: center; 195 | } 196 | 197 | /* Card effects */ 198 | .feature-card, .pricing-card, .code-sample { 199 | transition: all 0.3s ease; 200 | will-change: transform, box-shadow; 201 | } 202 | 203 | /* Animations */ 204 | @keyframes fadeIn { 205 | from { opacity: 0; transform: translateY(20px); } 206 | to { opacity: 1; transform: translateY(0); } 207 | } 208 | 209 | .hero-content, .section-title, .feature-card, .code-sample, .pricing-card { 210 | animation: fadeIn 0.8s ease-out forwards; 211 | } 212 | 213 | .feature-card:nth-child(2) { 214 | animation-delay: 0.2s; 215 | } 216 | 217 | .feature-card:nth-child(3) { 218 | animation-delay: 0.4s; 219 | } 220 | 221 | /* Custom scrollbar */ 222 | ::-webkit-scrollbar { 223 | width: 8px; 224 | height: 8px; 225 | } 226 | 227 | ::-webkit-scrollbar-track { 228 | background: #f1f1f1; 229 | } 230 | 231 | ::-webkit-scrollbar-thumb { 232 | background: #c1c1c1; 233 | border-radius: 4px; 234 | } 235 | 236 | ::-webkit-scrollbar-thumb:hover { 237 | background: #a8a8a8; 238 | } -------------------------------------------------------------------------------- /docs/support/license-faq.md: -------------------------------------------------------------------------------- 1 | # License FAQ 2 | 3 | ## Managing existing licenses 4 | 5 | Go to the [QueryLeaf Billing Dashboard](https://billing.stripe.com/p/login/bIYdRKdtcfklgdqaEE) to manage existing subscriptions 6 | 7 | ## General Questions 8 | 9 | ### What license does QueryLeaf use? 10 | QueryLeaf uses a dual licensing model: 11 | - GNU Affero General Public License v3 (AGPL-3.0) for the Community Edition 12 | - Commercial License for businesses using QueryLeaf in proprietary applications 13 | 14 | ### What is the difference between the AGPL and Commercial Licenses? 15 | The AGPL is a strong copyleft license that requires any modifications to be shared under the same license and requires source code disclosure if you run a modified version as a service. The Commercial License removes these requirements, allowing you to use QueryLeaf in proprietary applications without disclosing your source code. 16 | 17 | ### Which license should I choose? 18 | - Choose the **AGPL license** if you are working on open source projects, personal projects, or for evaluation. 19 | - Choose the **Commercial License** if you are using QueryLeaf in a proprietary application, especially if you're offering it as a service or if you need commercial support. 20 | 21 | ## AGPL Questions 22 | 23 | ### Can I use the AGPL version in a commercial project? 24 | Yes, but with important caveats. If your application is offered as a service (including web applications), the AGPL requires you to make the source code of your application available to users, including any modifications you've made to QueryLeaf. 25 | 26 | ### Does the AGPL affect my entire application? 27 | Yes, the AGPL's copyleft provisions generally extend to the entire application that incorporates the AGPL-licensed code, not just the QueryLeaf parts. 28 | 29 | ### What if I'm only using QueryLeaf internally? 30 | Even internal usage may trigger AGPL requirements if users (including employees) interact with the application over a network. For purely internal tools where source code is never shared outside your organization, AGPL may be suitable, but consult with your legal team. 31 | 32 | ## Commercial License Questions 33 | 34 | ### What does the Commercial License cover? 35 | The Commercial License covers all QueryLeaf components: 36 | - Core library 37 | - Command-line interface (CLI) 38 | - Web Server 39 | - PostgreSQL Server 40 | 41 | ### Are there any usage limitations with the Commercial License? 42 | The Commercial License prohibits: 43 | - Using QueryLeaf to create a competing product 44 | - Redistributing QueryLeaf as a standalone product 45 | - Removing copyright notices 46 | 47 | ### Do I need a license for each developer or for each deployment? 48 | Pricing is tiered based on the number of developers at your organization, and the number of MongoDB servers you will be using QueryLeaf with. See the pricing section on the [homepage](/) for more information. 49 | 50 | ### Do I need to purchase a separate license for the PostgreSQL Server? 51 | No, all server components (Web Server and PostgreSQL Server) are included in the same Commercial License. There's no need to purchase separate licenses for different components. 52 | 53 | ## Server-specific Questions 54 | 55 | ### Does the AGPL apply to the PostgreSQL Server and Web Server components? 56 | Yes, if you're using the AGPL version, the license requirements apply to both server components. If you run either server as a service, you must make the source code (including modifications) available to users of that service. 57 | 58 | ### Are there any technical limitations in the Community Edition of the servers? 59 | No, the Community Edition includes the full functionality of both server components. The difference is purely in the licensing terms, not in technical capabilities. 60 | 61 | ### Can I embed the PostgreSQL Server in my application? 62 | - With the **AGPL license**: Yes, but you must make your application's source code available to users. 63 | - With the **Commercial license**: Yes, with no requirement to disclose your source code. 64 | 65 | ## Support Questions 66 | 67 | ### Does the Commercial License include support? 68 | Yes, all commercial licenses include email support. Higher-tier licenses include priority support, quarterly reviews, and dedicated account managers. 69 | 70 | ### Is there any support for the Community Edition? 71 | Community support is available through GitHub issues. Commercial support is only available with a paid license. 72 | 73 | ### How do I upgrade my license? 74 | To upgrade from Developer to Business or from Business to Enterprise, contact [sales@queryleaf.com](mailto:sales@queryleaf.com) with your current license information. 75 | 76 | ## Still Have Questions? 77 | 78 | If you have additional questions about licensing or need help determining which license is right for you, please contact us: 79 | 80 | - Email: [sales@queryleaf.com](mailto:sales@queryleaf.com) 81 | - Subject: "License Question" -------------------------------------------------------------------------------- /docs/support/pricing.md: -------------------------------------------------------------------------------- 1 | # Pricing & Licensing 2 | 3 | QueryLeaf uses a dual-licensing model designed to balance open source availability with sustainable commercial development. 4 | 5 | ## Licensing Options 6 | 7 | QueryLeaf is available under two licensing options: 8 | 9 | 1. **AGPL v3 License (Community Edition)** - Free for open source projects, personal use, and evaluation 10 | 2. **Commercial License** - For commercial use in proprietary applications 11 | 12 | ## Community Edition 13 | 14 | The Community Edition of QueryLeaf is free for personal use, trials under a commercial license, or free forever under AGPL. 15 | 16 | The Community Edition is licensed under the [GNU Affero General Public License v3 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html). This license: 17 | 18 | - Allows free use, modification, and distribution of the software 19 | - Requires that any modifications to the software be shared under the same license 20 | - Requires source code disclosure if you run a modified version of the software as a service over a network 21 | - Has strong copyleft provisions that may affect proprietary applications 22 | 23 | The AGPL license is perfect for: 24 | - Open source projects 25 | - Personal projects 26 | - Educational purposes 27 | - Evaluation before purchase 28 | 29 | ## Commercial License 30 | 31 | Our Commercial License is designed for businesses and organizations that require more flexibility in how they use and distribute QueryLeaf. This license: 32 | 33 | - Removes the AGPL requirements for source code disclosure 34 | - Allows integration into proprietary software without license contamination 35 | - Provides commercial support options 36 | - Includes all components: Library, CLI, Web Server, and PostgreSQL Server 37 | 38 | ## Pricing Plans 39 | 40 | See the [Pricing section of the homepage](/#pricing) for pricing 41 | 42 | ## Server Components and Licensing 43 | 44 | Both the Web Server and PostgreSQL Server components are covered under the same dual-licensing model. This means: 45 | 46 | 1. **Under AGPL**: If you run either server as a service, you must make the source code (including any modifications) available to users of that service. 47 | 2. **Under Commercial License**: You can run and distribute the server components as part of your proprietary applications without AGPL obligations. 48 | 49 | The PostgreSQL Server component is particularly valuable for commercial users as it allows seamless integration with existing PostgreSQL clients and tools, making it ideal for enterprise deployments. 50 | 51 | ## How to Purchase 52 | 53 | For more information or to purchase a commercial license: 54 | 55 | - **Developer License**: [Email sales@queryleaf.com](mailto:sales@queryleaf.com?subject=QueryLeaf Developer License) 56 | - **Business License**: [Email sales@queryleaf.com](mailto:sales@queryleaf.com?subject=QueryLeaf Business License) 57 | - **Enterprise License**: [Email enterprise@queryleaf.com](mailto:enterprise@queryleaf.com?subject=QueryLeaf Enterprise License) 58 | 59 | ## Need Help? 60 | 61 | For questions about licensing or to discuss your specific needs, please [contact our sales team](mailto:sales@queryleaf.com). 62 | 63 | --- 64 | 65 | © 2023-2025 Beekeeper Studio, Inc. All rights reserved. -------------------------------------------------------------------------------- /docs/usage/cli.md: -------------------------------------------------------------------------------- 1 | # Command-Line Interface 2 | 3 | QueryLeaf provides a command-line interface (CLI) that allows you to execute SQL queries against a MongoDB database directly from your terminal. This is useful for quick queries, automation scripts, or when you don't need a full web interface. 4 | 5 | ## Installation 6 | 7 | The CLI is included with the QueryLeaf package. If you've installed QueryLeaf globally, you can use it directly from your terminal: 8 | 9 | ```bash 10 | npm install -g queryleaf 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Basic Usage 16 | 17 | ```bash 18 | queryleaf --db --query "SELECT * FROM users LIMIT 10" 19 | ``` 20 | 21 | ### Options 22 | 23 | | Option | Description | Default | 24 | |--------|-------------|---------| 25 | | `--uri` | MongoDB connection URI | `mongodb://localhost:27017` | 26 | | `--db` | MongoDB database name (required) | - | 27 | | `--file` | SQL file to execute | - | 28 | | `--query`, `-q` | SQL query to execute | - | 29 | | `--json` | Output results as JSON | `false` | 30 | | `--pretty` | Pretty-print JSON output | `true` | 31 | | `--interactive`, `-i` | Run in interactive mode | `false` | 32 | | `--help`, `-h` | Show help | - | 33 | | `--version`, `-v` | Show version | - | 34 | 35 | ### Examples 36 | 37 | #### Execute a Single Query 38 | 39 | ```bash 40 | queryleaf --db mydb --query "SELECT * FROM users WHERE age > 21" 41 | ``` 42 | 43 | #### Execute Multiple Queries from a File 44 | 45 | Create a file named `queries.sql` with multiple SQL statements: 46 | 47 | ```sql 48 | SELECT * FROM users LIMIT 5; 49 | SELECT name, email FROM users WHERE active = true; 50 | ``` 51 | 52 | Then execute it: 53 | 54 | ```bash 55 | queryleaf --db mydb --file queries.sql 56 | ``` 57 | 58 | #### Output Results as JSON 59 | 60 | ```bash 61 | queryleaf --db mydb --query "SELECT * FROM products" --json 62 | ``` 63 | 64 | #### Interactive Mode 65 | 66 | The interactive mode provides a SQL shell experience: 67 | 68 | ```bash 69 | queryleaf --db mydb --interactive 70 | ``` 71 | 72 | In interactive mode, you can: 73 | 74 | - Type SQL queries that end with a semicolon to execute them 75 | - Use multi-line queries (continue typing until you add a semicolon) 76 | - Use special commands: 77 | - `.help` - Show help information 78 | - `.tables` - List all collections in the database 79 | - `.exit` or `.quit` - Exit the shell 80 | - `.clear` - Clear the current query buffer 81 | - `.json` - Toggle JSON output mode 82 | 83 | ## Examples 84 | 85 | ### Basic Query 86 | 87 | ```bash 88 | $ queryleaf --db inventory --query "SELECT * FROM products WHERE price > 100 LIMIT 5" 89 | ``` 90 | 91 | ### Executing Complex Queries 92 | 93 | ```bash 94 | $ queryleaf --db sales --query "SELECT category, SUM(total) as revenue FROM orders GROUP BY category ORDER BY revenue DESC" 95 | ``` 96 | 97 | ### Using in Scripts 98 | 99 | You can use the CLI in shell scripts: 100 | 101 | ```bash 102 | #!/bin/bash 103 | # backup-data.sh 104 | 105 | # Export data as JSON 106 | queryleaf --db analytics --query "SELECT * FROM events WHERE date >= '2023-01-01'" --json > events_backup.json 107 | ``` 108 | 109 | ### Interactive Session Example 110 | 111 | ``` 112 | $ queryleaf --db test --interactive 113 | 114 | Starting interactive SQL shell. Type .help for commands, .exit to quit. 115 | Connected to database: test 116 | sql> .tables 117 | Collections in database: 118 | users 119 | products 120 | orders 121 | 122 | sql> SELECT * FROM users LIMIT 2; 123 | Executing: SELECT * FROM users LIMIT 2; 124 | _id | name | email | age | active 125 | ----------+-----------+----------------------+-----------+----------- 126 | 6079c40f9 | John Doe | john@example.com | 30 | true 127 | 6079c41a5 | Jane Smith| jane@example.com | 25 | true 128 | 129 | 2 record(s) returned 130 | Execution time: 15ms 131 | 132 | sql> SELECT COUNT(*) as count FROM users WHERE active = true; 133 | Executing: SELECT COUNT(*) as count FROM users WHERE active = true; 134 | count 135 | ---------- 136 | 42 137 | 138 | 1 record(s) returned 139 | Execution time: 12ms 140 | 141 | sql> .exit 142 | Goodbye! 143 | ``` 144 | 145 | ## Advanced Usage 146 | 147 | ### Piping Data 148 | 149 | You can pipe query results to other command-line tools: 150 | 151 | ```bash 152 | # Count lines of output 153 | queryleaf --db logs --query "SELECT * FROM events" | wc -l 154 | 155 | # Filter results with grep 156 | queryleaf --db users --query "SELECT * FROM logins" | grep "failed" 157 | 158 | # Process JSON output with jq 159 | queryleaf --db analytics --query "SELECT * FROM pageviews" --json | jq '.[] | select(.duration > 60)' 160 | ``` 161 | 162 | ### Using Environment Variables 163 | 164 | You can use environment variables to avoid typing connection details repeatedly: 165 | 166 | ```bash 167 | # Set environment variables 168 | export MONGO_URI="mongodb://user:password@hostname:27017" 169 | export MONGO_DB="production" 170 | 171 | # Use them in your query 172 | queryleaf --uri "$MONGO_URI" --db "$MONGO_DB" --query "SELECT * FROM users" 173 | ``` -------------------------------------------------------------------------------- /docs/usage/core-concepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts 2 | 3 | This page explains the key concepts and architecture of QueryLeaf to help you understand how it works and how to use it effectively. 4 | 5 | ## Architecture Overview 6 | 7 | QueryLeaf follows a modular architecture with three main components: 8 | 9 | 1. **SqlParser**: Converts SQL text into an abstract syntax tree (AST) 10 | 2. **SqlCompiler**: Transforms the AST into MongoDB commands 11 | 3. **CommandExecutor**: Executes the commands against a MongoDB database 12 | 13 | The flow of a query through the system is: 14 | 15 | ``` 16 | SQL Query → Parser → AST → Compiler → MongoDB Commands → Executor → Results 17 | ``` 18 | 19 | ### 1. SQL Parser 20 | 21 | The SQL Parser component takes raw SQL text and converts it into an abstract syntax tree (AST) using the `node-sql-parser` library. QueryLeaf extends the standard PostgreSQL dialect with additional support for: 22 | 23 | - Nested field access (e.g., `address.city`) 24 | - Array element access (e.g., `items[0].name`) 25 | 26 | The parser uses preprocessing and postprocessing to handle these extensions. 27 | 28 | ### 2. SQL Compiler 29 | 30 | The SQL Compiler takes the AST from the parser and converts it into MongoDB commands. It handles: 31 | 32 | - Mapping SQL operations to MongoDB operations (`SELECT` → `find`, `INSERT` → `insertMany`, etc.) 33 | - Converting SQL WHERE clauses to MongoDB query filters 34 | - Transforming JOINs into MongoDB `$lookup` aggregation stages 35 | - Converting GROUP BY clauses to MongoDB aggregation pipelines 36 | 37 | ### 3. Command Executor 38 | 39 | The Command Executor takes the MongoDB commands generated by the compiler and executes them against a MongoDB database using your provided MongoDB client. It: 40 | 41 | - Executes the appropriate MongoDB methods based on the command type 42 | - Handles complex operations like aggregation pipelines 43 | - Returns the results in a consistent format 44 | 45 | ## Key Objects 46 | 47 | ### QueryLeaf 48 | 49 | The main class that ties everything together. It provides a simple API to execute SQL queries against MongoDB: 50 | 51 | ```typescript 52 | const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase'); 53 | 54 | // Execute a query and get all results as an array 55 | const results = await queryLeaf.execute('SELECT * FROM users'); 56 | 57 | // Or use a cursor for more control and memory efficiency 58 | // You can optionally specify the batch size to control memory usage 59 | const cursor = await queryLeaf.executeCursor('SELECT * FROM users', { batchSize: 50 }); 60 | await cursor.forEach(user => { 61 | console.log(`Processing user: ${user.name}`); 62 | }); 63 | await cursor.close(); 64 | ``` 65 | 66 | ### DummyQueryLeaf 67 | 68 | A special implementation of QueryLeaf that doesn't execute real MongoDB operations but logs them instead. Useful for testing and debugging: 69 | 70 | ```typescript 71 | const dummyLeaf = new DummyQueryLeaf('mydatabase'); 72 | await dummyLeaf.execute('SELECT * FROM users'); 73 | // [DUMMY MongoDB] FIND in mydatabase.users with filter: {} 74 | 75 | // Cursor support works with DummyQueryLeaf too 76 | const cursor = await dummyLeaf.executeCursor('SELECT * FROM users'); 77 | await cursor.forEach(user => { 78 | // Process mock data 79 | }); 80 | await cursor.close(); 81 | ``` 82 | 83 | ## Relationship Between SQL and MongoDB Concepts 84 | 85 | QueryLeaf maps SQL concepts to MongoDB concepts: 86 | 87 | | SQL Concept | MongoDB Equivalent | 88 | |-------------|-------------------| 89 | | Database | Database | 90 | | Table | Collection | 91 | | Row | Document | 92 | | Column | Field | 93 | | JOIN | $lookup | 94 | | WHERE | Query Filter | 95 | | GROUP BY | Aggregation ($group) | 96 | | ORDER BY | Sort | 97 | | LIMIT | Limit | 98 | | SELECT | Projection | 99 | 100 | ## Data Type Handling 101 | 102 | QueryLeaf handles the conversion between SQL and MongoDB data types: 103 | 104 | - SQL strings → MongoDB strings 105 | - SQL numbers → MongoDB numbers 106 | - SQL dates → MongoDB dates 107 | - SQL NULL → MongoDB null 108 | - SQL booleans → MongoDB booleans 109 | 110 | ## Naming Conventions 111 | 112 | QueryLeaf uses specific naming conventions for mapping SQL to MongoDB: 113 | 114 | - SQL table names map directly to MongoDB collection names 115 | - Column names map directly to MongoDB field names 116 | - Nested fields use dot notation (e.g., `address.city`) 117 | - Array access uses dot notation with indices (e.g., `items.0.name` for SQL's `items[0].name`) 118 | 119 | ## Execution Flow 120 | 121 | QueryLeaf supports two main execution methods: 122 | 123 | ### Standard Execution 124 | 125 | When you call `queryLeaf.execute(sqlQuery)`, the following happens: 126 | 127 | 1. The SQL query is parsed into an AST 128 | 2. The AST is compiled into MongoDB commands 129 | 3. The commands are executed against the MongoDB database 130 | 4. All results are loaded into memory and returned as an array 131 | 132 | This is simple to use but can be memory-intensive for large result sets. 133 | 134 | ### Cursor Execution 135 | 136 | When you call `queryLeaf.executeCursor(sqlQuery)`, the following happens: 137 | 138 | 1. The SQL query is parsed into an AST 139 | 2. The AST is compiled into MongoDB commands 140 | 3. For SELECT queries, a MongoDB cursor is returned instead of loading all results 141 | 4. You control how and when results are processed (streaming/batching) 142 | 143 | This approach is more memory-efficient for large datasets and gives you more control. 144 | 145 | If any step fails in either approach, an error is thrown with details about what went wrong. 146 | 147 | ## Extending QueryLeaf 148 | 149 | QueryLeaf is designed to be extensible. You can customize its behavior by: 150 | 151 | - Creating custom implementations of the interfaces 152 | - Extending the parser to support additional SQL syntax 153 | - Customizing the compiler to generate specialized MongoDB commands 154 | - Implementing a custom executor for specialized MongoDB operations 155 | 156 | ## Next Steps 157 | 158 | Now that you understand the core concepts of QueryLeaf, you can: 159 | 160 | - Learn more about [MongoDB Client Integration](mongodb-client.md) 161 | - See how to use the [Dummy Client for Testing](dummy-client.md) 162 | - View practical [Usage Examples](examples.md) 163 | - Explore the [SQL Syntax Reference](../sql-syntax/index.md) for details on supported SQL features -------------------------------------------------------------------------------- /docs/usage/server.md: -------------------------------------------------------------------------------- 1 | # Web Server 2 | 3 | QueryLeaf provides a web server that allows you to run a MongoDB SQL proxy. This server offers both a REST API for programmatic access and a simple web UI for interactive querying. 4 | 5 | ## Installation 6 | 7 | The web server is included with the QueryLeaf package. If you've installed QueryLeaf globally, you can use it directly: 8 | 9 | ```bash 10 | npm install -g queryleaf 11 | ``` 12 | 13 | ## Starting the Server 14 | 15 | ### Basic Usage 16 | 17 | ```bash 18 | queryleaf-server 19 | ``` 20 | 21 | By default, this will: 22 | - Start a server on port 3000 23 | - Try to connect to MongoDB at `mongodb://localhost:27017` 24 | - Expect a database name provided via the `MONGO_DB` environment variable 25 | 26 | ### Environment Variables 27 | 28 | The server can be configured using the following environment variables: 29 | 30 | | Variable | Description | Default | 31 | |----------|-------------|---------| 32 | | `PORT` | Port to run the server on | `3000` | 33 | | `MONGO_URI` | MongoDB connection URI | `mongodb://localhost:27017` | 34 | | `MONGO_DB` | MongoDB database name (required) | - | 35 | | `LOG_FORMAT` | Morgan logging format | `dev` | 36 | | `ENABLE_CORS` | Enable CORS | `true` | 37 | | `CORS_ORIGIN` | CORS origin | `*` | 38 | | `API_RATE_LIMIT` | API rate limit (requests) | `100` | 39 | | `API_RATE_LIMIT_WINDOW` | Rate limit window (minutes) | `15` | 40 | | `SWAGGER_ENABLED` | Enable Swagger documentation | `true` | 41 | 42 | ### Example 43 | 44 | ```bash 45 | PORT=8080 MONGO_URI="mongodb://localhost:27017" MONGO_DB="test" queryleaf-server 46 | ``` 47 | 48 | ## Using the Web UI 49 | 50 | The web UI is available at the root URL (e.g., `http://localhost:3000/`). It provides: 51 | 52 | - A SQL editor with syntax highlighting 53 | - A list of available collections 54 | - Results displayed in a tabular format 55 | - Error messages for failed queries 56 | - Query execution time information 57 | 58 | ### Features 59 | 60 | - **SQL Editor**: Write and edit SQL queries with syntax highlighting 61 | - **Collections List**: Quick access to available collections in your database 62 | - **Table Results**: Results displayed in a clean, tabular format 63 | - **Error Handling**: Clear error messages for troubleshooting 64 | - **Keyboard Shortcuts**: Use Ctrl+Enter (Cmd+Enter on Mac) to execute queries 65 | 66 | ## REST API 67 | 68 | The server provides a REST API for programmatic access to your MongoDB database through SQL queries. 69 | 70 | ### API Endpoints 71 | 72 | #### Execute a SQL Query 73 | 74 | ``` 75 | POST /api/query 76 | ``` 77 | 78 | **Request Body**: 79 | ```json 80 | { 81 | "sql": "SELECT * FROM users LIMIT 10" 82 | } 83 | ``` 84 | 85 | **Response**: 86 | ```json 87 | { 88 | "results": [ 89 | { 90 | "_id": "5f8d41f2e6b5a92c6a328d1a", 91 | "name": "John Doe", 92 | "email": "john@example.com", 93 | "age": 30 94 | }, 95 | // ... more results 96 | ], 97 | "rowCount": 10, 98 | "executionTime": 15 99 | } 100 | ``` 101 | 102 | #### List Available Collections 103 | 104 | ``` 105 | GET /api/tables 106 | ``` 107 | 108 | **Response**: 109 | ```json 110 | { 111 | "collections": [ 112 | "users", 113 | "products", 114 | "orders" 115 | ] 116 | } 117 | ``` 118 | 119 | #### Health Check 120 | 121 | ``` 122 | GET /api/health 123 | ``` 124 | 125 | **Response**: 126 | ```json 127 | { 128 | "status": "ok", 129 | "mongodb": "connected", 130 | "version": "1.0.0", 131 | "database": "mydb" 132 | } 133 | ``` 134 | 135 | ### API Documentation 136 | 137 | The API documentation is available at `/api-docs` when the server is running with Swagger enabled. 138 | 139 | ## Security Considerations 140 | 141 | The server includes several security features: 142 | 143 | - **Helmet**: Sets various HTTP headers for security 144 | - **Rate Limiting**: Prevents abuse through request rate limiting 145 | - **CORS**: Configurable Cross-Origin Resource Sharing 146 | 147 | However, for production use, you should consider: 148 | 149 | 1. Using HTTPS (by putting the server behind a reverse proxy like Nginx) 150 | 2. Adding authentication (via an authentication proxy or custom middleware) 151 | 3. Restricting the CORS origin to specific domains 152 | 4. Running the server in a container or restricted environment 153 | 154 | ## Use Cases 155 | 156 | ### Local Development Database Interface 157 | 158 | Run the server locally to provide a simple SQL interface to your MongoDB development database: 159 | 160 | ```bash 161 | MONGO_DB="development" queryleaf-server 162 | ``` 163 | 164 | ### Microservice for SQL Access 165 | 166 | Deploy the server as a microservice to provide SQL access to MongoDB for applications that expect SQL: 167 | 168 | ```bash 169 | PORT=8080 MONGO_URI="mongodb://user:pass@production-mongodb:27017" MONGO_DB="production" CORS_ORIGIN="https://myapp.example.com" queryleaf-server 170 | ``` 171 | 172 | ### Analytics Access Point 173 | 174 | Provide a SQL interface for analytics tools or data scientists who are more comfortable with SQL: 175 | 176 | ```bash 177 | PORT=3030 MONGO_URI="mongodb://analytics-user:pass@mongodb:27017" MONGO_DB="analytics" queryleaf-server 178 | ``` 179 | 180 | ## Advanced Configuration 181 | 182 | For advanced configuration, you can create a startup script: 183 | 184 | ```javascript 185 | // server.js 186 | const { startServer } = require('queryleaf/dist/server'); 187 | 188 | startServer({ 189 | port: 8080, 190 | mongoUri: 'mongodb://localhost:27017', 191 | databaseName: 'myapp', 192 | logFormat: 'combined', 193 | enableCors: true, 194 | corsOrigin: 'https://myapp.example.com', 195 | apiRateLimit: 200, 196 | apiRateLimitWindow: 30, 197 | swaggerEnabled: true 198 | }).then(({ app, server }) => { 199 | console.log('Server started with custom configuration'); 200 | 201 | // Add custom middleware or routes 202 | app.get('/custom', (req, res) => { 203 | res.json({ message: 'Custom endpoint' }); 204 | }); 205 | }).catch(error => { 206 | console.error('Failed to start server:', error); 207 | }); 208 | ``` 209 | 210 | Then run it with: 211 | 212 | ```bash 213 | node server.js 214 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/packages/lib', 4 | '/packages/cli', 5 | '/packages/server' 6 | ] 7 | }; -------------------------------------------------------------------------------- /logo-green-bg-white-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/logo-green-bg-white-shape.png -------------------------------------------------------------------------------- /logo-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /logo-transparent-bg-black-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/logo-transparent-bg-black-shape.png -------------------------------------------------------------------------------- /logo-transparent-bg-green-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/logo-transparent-bg-green-shape.png -------------------------------------------------------------------------------- /logo-transparent-bg-white-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beekeeper-studio/queryleaf/63b1e27dc6d2db5b3cea4b3c7cef27443cffcbcd/logo-transparent-bg-white-shape.png -------------------------------------------------------------------------------- /logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: QueryLeaf 2 | site_description: SQL to MongoDB query translator for NodeJS 3 | site_url: https://queryleaf.com/ 4 | repo_url: https://github.com/beekeeper-studio/queryleaf 5 | repo_name: beekeeper-studio/queryleaf 6 | 7 | theme: 8 | name: material 9 | palette: 10 | primary: green 11 | accent: light green 12 | logo: assets/logo-white.svg 13 | favicon: assets/favicon.ico 14 | icon: 15 | repo: fontawesome/brands/github 16 | features: 17 | - navigation.tracking 18 | - navigation.sections 19 | - navigation.indexes 20 | - navigation.tabs 21 | - navigation.top 22 | - content.tabs.link 23 | - content.code.copy 24 | - content.code.annotate 25 | custom_dir: docs/overrides 26 | 27 | nav: 28 | - Home: index.md 29 | - Library: 30 | - Getting Started: 31 | - getting-started/installation.md 32 | - getting-started/quickstart.md 33 | - Usage: 34 | - usage/core-concepts.md 35 | - usage/mongodb-client.md 36 | - usage/dummy-client.md 37 | - usage/examples.md 38 | - Debugging: 39 | - debugging/troubleshooting.md 40 | - debugging/limitations.md 41 | - CLI: 42 | - Overview: usage/cli.md 43 | - Server: 44 | - Overview: usage/server.md 45 | - PostgreSQL Server: 46 | - Overview: usage/postgres-server.md 47 | - SQL Syntax: 48 | - sql-syntax/index.md 49 | - sql-syntax/select.md 50 | - sql-syntax/insert.md 51 | - sql-syntax/update.md 52 | - sql-syntax/delete.md 53 | - sql-syntax/nested-fields.md 54 | - sql-syntax/array-access.md 55 | - sql-syntax/joins.md 56 | - sql-syntax/group-by.md 57 | - Support: 58 | - Pricing & Licensing: support/pricing.md 59 | - License FAQ: support/license-faq.md 60 | 61 | markdown_extensions: 62 | - pymdownx.highlight: 63 | anchor_linenums: true 64 | - pymdownx.superfences 65 | - pymdownx.inlinehilite 66 | - pymdownx.tabbed: 67 | alternate_style: true 68 | - admonition 69 | - pymdownx.details 70 | - attr_list 71 | - md_in_html 72 | - tables 73 | - footnotes 74 | 75 | extra: 76 | social: 77 | - icon: fontawesome/brands/github 78 | link: https://github.com/beekeeper-studio/queryleaf 79 | - icon: fontawesome/brands/twitter 80 | link: https://twitter.com/beekeeperstudio 81 | 82 | plugins: 83 | - search 84 | - glightbox 85 | - minify: 86 | minify_html: true 87 | 88 | extra_css: 89 | - stylesheets/extra.css 90 | - stylesheets/home.css 91 | 92 | copyright: Copyright © 2023-2025 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queryleaf-monorepo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "SQL to MongoDB query translator", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "clean:lib": "rm -rf packages/lib/dist", 11 | "clean:cli": "rm -rf packages/cli/dist", 12 | "clean:server": "rm -rf packages/server/dist", 13 | "clean:pg-server": "rm -rf packages/postgres-server/dist", 14 | "clean": "yarn clean:lib && yarn clean:cli && yarn clean:server && yarn clean:pg-server", 15 | "build:lib": "yarn workspace @queryleaf/lib build", 16 | "build:cli": "yarn workspace @queryleaf/cli build", 17 | "build:server": "yarn workspace @queryleaf/server build", 18 | "build:pg-server": "yarn workspace @queryleaf/postgres-server build", 19 | "build": "bin/run-all build", 20 | "build:references": "tsc --build", 21 | "test": "bin/run-all test", 22 | "test:unit": "bin/run-all test:unit", 23 | "test:integration": "bin/run-all test:integration", 24 | "test:lib": "yarn workspace @queryleaf/lib test", 25 | "test:lib:integration": "yarn workspace @queryleaf/lib test:integration", 26 | "test:cli": "yarn workspace @queryleaf/cli test", 27 | "test:server": "yarn workspace @queryleaf/server test", 28 | "test:pg-server": "yarn workspace @queryleaf/postgres-server test", 29 | "typecheck": "bin/run-all typecheck", 30 | "lint": "bin/run-all lint", 31 | "lint:fix": "bin/run-all lint:fix", 32 | "format": "prettier --write \"packages/*/src/**/*.ts\"", 33 | "format:check": "prettier --check \"packages/*/src/**/*.ts\"", 34 | "validate": "yarn typecheck && yarn lint && yarn test && yarn format:check", 35 | "docs:serve": "mkdocs serve", 36 | "docs:build": "pip install -r requirements.txt && mkdocs build" 37 | }, 38 | "keywords": [ 39 | "sql", 40 | "mongodb", 41 | "compiler", 42 | "query", 43 | "cli", 44 | "server" 45 | ], 46 | "author": "", 47 | "license": "AGPL-3.0", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/beekeeper-studio/queryleaf.git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/beekeeper-studio/queryleaf/issues" 54 | }, 55 | "homepage": "https://github.com/beekeeper-studio/queryleaf#readme", 56 | "devDependencies": { 57 | "@types/node": "^22.13.10", 58 | "@typescript-eslint/eslint-plugin": "^6.21.0", 59 | "@typescript-eslint/parser": "^6.21.0", 60 | "eslint": "^8.57.0", 61 | "prettier": "^3.2.5", 62 | "typescript": "^5.8.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 |

2 | QueryLeaf Logo 3 |

4 | 5 |

@queryleaf/cli

6 | 7 |

SQL to MongoDB query translator - Command Line Interface

8 | 9 | ## Overview 10 | 11 | `@queryleaf/cli` provides a command-line interface for QueryLeaf, allowing you to execute SQL queries against MongoDB directly from your terminal. It leverages the core `@queryleaf/lib` package to parse SQL, transform it into MongoDB commands, and execute those commands. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | # Global installation 17 | npm install -g @queryleaf/cli 18 | # or 19 | yarn global add @queryleaf/cli 20 | 21 | # Local installation 22 | npm install @queryleaf/cli 23 | # or 24 | yarn add @queryleaf/cli 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```bash 30 | # Basic usage 31 | queryleaf --uri mongodb://localhost:27017 --db mydb "SELECT * FROM users" 32 | 33 | # With authentication 34 | queryleaf --uri mongodb://user:pass@localhost:27017 --db mydb "SELECT * FROM users" 35 | 36 | # Output formatting 37 | queryleaf --uri mongodb://localhost:27017 --db mydb --format table "SELECT * FROM users" 38 | 39 | # Help 40 | queryleaf --help 41 | ``` 42 | 43 | ## Example Queries 44 | 45 | ```bash 46 | # Basic SELECT with WHERE 47 | queryleaf "SELECT name, email FROM users WHERE age > 21" 48 | 49 | # Nested field access 50 | queryleaf "SELECT name, address.city FROM users WHERE address.zip = '10001'" 51 | 52 | # Array access 53 | queryleaf "SELECT items[0].name FROM orders WHERE items[0].price > 100" 54 | 55 | # GROUP BY with aggregation 56 | queryleaf "SELECT status, COUNT(*) as count FROM orders GROUP BY status" 57 | 58 | # JOIN between collections 59 | queryleaf "SELECT u.name, o.total FROM users u JOIN orders o ON u._id = o.userId" 60 | ``` 61 | 62 | ## Configuration 63 | 64 | You can configure the CLI using command-line arguments or environment variables: 65 | 66 | | Argument | Environment Variable | Description | 67 | |----------------|----------------------|----------------------------------| 68 | | `--uri` | `MONGODB_URI` | MongoDB connection URI | 69 | | `--db` | `MONGODB_DB` | MongoDB database name | 70 | | `--format` | `QUERYLEAF_FORMAT` | Output format (json, table, csv) | 71 | | `--debug` | `QUERYLEAF_DEBUG` | Enable debug output | 72 | 73 | ## Links 74 | 75 | - [Website](https://queryleaf.com) 76 | - [Documentation](https://queryleaf.com/docs) 77 | - [GitHub Repository](https://github.com/beekeeper-studio/queryleaf) 78 | 79 | ## License 80 | 81 | QueryLeaf is dual-licensed: 82 | 83 | - [AGPL-3.0](https://github.com/beekeeper-studio/queryleaf/blob/main/LICENSE.md) for open source use 84 | - [Commercial license](https://github.com/beekeeper-studio/queryleaf/blob/main/COMMERCIAL_LICENSE.md) for commercial use 85 | 86 | For commercial licensing options, visit [queryleaf.com](https://queryleaf.com). -------------------------------------------------------------------------------- /packages/cli/bin/queryleaf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cli.js'); 4 | -------------------------------------------------------------------------------- /packages/cli/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/tests'], 5 | transform: { 6 | '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }] 7 | }, 8 | testMatch: ['**/tests/**/*.test.ts', '**/tests/**/*.test.js'], 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | openHandlesTimeout: 30000, 11 | detectOpenHandles: true, 12 | }; -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@queryleaf/cli", 3 | "version": "0.1.0", 4 | "description": "SQL to MongoDB query translator - Command Line Interface", 5 | "main": "dist/cli.js", 6 | "bin": { 7 | "queryleaf": "./bin/queryleaf" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf dist", 11 | "build": "yarn clean && tsc", 12 | "typecheck": "tsc --noEmit", 13 | "start": "ts-node src/cli.ts", 14 | "test": "yarn test:unit && yarn test:integration", 15 | "test:unit": "jest tests/unit", 16 | "test:integration": "jest tests/integration --runInBand", 17 | "lint": "eslint --ext .ts src", 18 | "lint:fix": "eslint --ext .ts --fix src", 19 | "lockversion": "sed -i -E \"s|(\\\"@queryleaf/lib\\\"\\s*:\\s*\\\")[^\\\"]+(\\\")|\\1$VERSION\\2|\" package.json" 20 | }, 21 | "keywords": [ 22 | "sql", 23 | "mongodb", 24 | "compiler", 25 | "query", 26 | "cli" 27 | ], 28 | "author": "", 29 | "license": "AGPL-3.0", 30 | "dependencies": { 31 | "@queryleaf/lib": "0.1.0", 32 | "chalk": "^4.1.2", 33 | "mongodb": "^6.14.2", 34 | "yargs": "^17.7.2" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^29.5.14", 38 | "@types/mongodb": "^4.0.7", 39 | "@types/yargs": "^17.0.32", 40 | "jest": "^29.7.0", 41 | "node-fetch": "2", 42 | "ts-jest": "^29.2.6", 43 | "ts-node": "^10.9.2" 44 | }, 45 | "publishConfig": { 46 | "access": "public" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/tests/integration/cli.test.ts: -------------------------------------------------------------------------------- 1 | 2 | describe('CLI tests', () => { 3 | // Very basic test for CLI functionality 4 | it('CLI imports should compile correctly', () => { 5 | // no tests right now 6 | expect(true).toBe(true); 7 | }); 8 | }); -------------------------------------------------------------------------------- /packages/cli/tests/unit/cli.test.ts: -------------------------------------------------------------------------------- 1 | 2 | describe('CLI tests', () => { 3 | // Very basic test for CLI functionality 4 | it('CLI imports should compile correctly', () => { 5 | // no tests right now 6 | expect(true).toBe(true); 7 | }); 8 | }); -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 9 | } -------------------------------------------------------------------------------- /packages/lib/README.md: -------------------------------------------------------------------------------- 1 |

2 | QueryLeaf Logo 3 |

4 | 5 |

@queryleaf/lib

6 | 7 |

SQL to MongoDB query translator - Core library

8 | 9 | ## Overview 10 | 11 | `@queryleaf/lib` is the core library for QueryLeaf, a tool that translates SQL queries into MongoDB commands. It provides the foundation for all QueryLeaf packages by parsing SQL using node-sql-parser, transforming it into an abstract command set, and executing those commands against the MongoDB Node.js driver. 12 | 13 | ## Features 14 | 15 | - Parse SQL statements into an abstract syntax tree 16 | - Compile SQL AST into MongoDB commands 17 | - Execute MongoDB commands using the official driver 18 | - Support for basic SQL operations (SELECT, INSERT, UPDATE, DELETE) 19 | - Advanced querying features: 20 | - Nested field access (e.g., `address.zip`) 21 | - Array element access (e.g., `items[0].name`) 22 | - GROUP BY with aggregation functions (COUNT, SUM, AVG, MIN, MAX) 23 | - JOINs between collections 24 | - Direct MongoDB cursor access for fine-grained result processing and memory efficiency 25 | 26 | ## Installation 27 | 28 | ```bash 29 | npm install @queryleaf/lib 30 | # or 31 | yarn add @queryleaf/lib 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```typescript 37 | import { QueryLeaf } from '@queryleaf/lib'; 38 | import { MongoClient } from 'mongodb'; 39 | 40 | // Your existing MongoDB client 41 | const mongoClient = new MongoClient('mongodb://localhost:27017'); 42 | await mongoClient.connect(); 43 | 44 | // Create QueryLeaf with your MongoDB client 45 | const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase'); 46 | 47 | // Execute SQL queries against your MongoDB database 48 | const results = await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); 49 | console.log(results); 50 | 51 | // Get a MongoDB cursor for more control over result processing and memory efficiency 52 | // You can optionally specify a batch size to control how many documents are fetched at once 53 | const cursor = await queryLeaf.executeCursor('SELECT * FROM users WHERE age > 30', { batchSize: 50 }); 54 | await cursor.forEach((doc) => { 55 | console.log(`User: ${doc.name}`); 56 | }); 57 | await cursor.close(); 58 | 59 | // When you're done, close your MongoDB client 60 | await mongoClient.close(); 61 | ``` 62 | 63 | ### Testing with DummyQueryLeaf 64 | 65 | For testing or debugging without a real database, use DummyQueryLeaf: 66 | 67 | ```typescript 68 | import { DummyQueryLeaf } from '@queryleaf/lib'; 69 | 70 | // Create a DummyQueryLeaf instance for testing 71 | const queryLeaf = new DummyQueryLeaf('mydatabase'); 72 | 73 | // Operations will be logged to console but not executed 74 | await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); 75 | // [DUMMY MongoDB] FIND in mydatabase.users with filter: { "age": { "$gt": 21 } } 76 | 77 | // You can also use cursor functionality with DummyQueryLeaf 78 | const cursor = await queryLeaf.executeCursor('SELECT * FROM users LIMIT 10'); 79 | await cursor.forEach((doc) => { 80 | // Process each document 81 | }); 82 | await cursor.close(); 83 | ``` 84 | 85 | ## SQL Query Examples 86 | 87 | ```sql 88 | -- Basic SELECT with WHERE 89 | SELECT name, email FROM users WHERE age > 21 90 | 91 | -- Nested field access 92 | SELECT name, address.city FROM users WHERE address.zip = '10001' 93 | 94 | -- Array access 95 | SELECT items[0].name FROM orders WHERE items[0].price > 100 96 | 97 | -- GROUP BY with aggregation 98 | SELECT status, COUNT(*) as count FROM orders GROUP BY status 99 | 100 | -- JOIN between collections 101 | SELECT u.name, o.total FROM users u JOIN orders o ON u._id = o.userId 102 | ``` 103 | 104 | ## Working with Cursors 105 | 106 | When working with large result sets, using MongoDB cursors directly can be more memory-efficient and gives you more control over result processing: 107 | 108 | ```typescript 109 | // Get a cursor for a SELECT query 110 | // You can specify a batch size to control memory usage and network behavior 111 | const cursor = await queryLeaf.executeCursor('SELECT * FROM products WHERE price > 100', { batchSize: 100 }); 112 | 113 | // Option 1: Convert to array (loads all results into memory) 114 | const results = await cursor.toArray(); 115 | console.log(`Found ${results.length} products`); 116 | 117 | // Option 2: Iterate with forEach (memory efficient) 118 | await cursor.forEach(product => { 119 | console.log(`Processing ${product.name}...`); 120 | }); 121 | 122 | // Option 3: Manual iteration with next/hasNext (most control) 123 | while (await cursor.hasNext()) { 124 | const product = await cursor.next(); 125 | // Process each product individually 126 | console.log(`Product: ${product.name}, $${product.price}`); 127 | } 128 | 129 | // Always close the cursor when done 130 | await cursor.close(); 131 | ``` 132 | 133 | Features: 134 | - Returns MongoDB `FindCursor` for normal queries and `AggregationCursor` for aggregations 135 | - Supports all cursor methods like `forEach()`, `toArray()`, `next()`, `hasNext()` 136 | - Efficiently handles large result sets with MongoDB's batching system (configurable batch size) 137 | - Works with all advanced QueryLeaf features (filtering, sorting, aggregations, etc.) 138 | - Only available for read operations (SELECT queries) 139 | ``` 140 | 141 | ## Links 142 | 143 | - [Website](https://queryleaf.com) 144 | - [Documentation](https://queryleaf.com/docs) 145 | - [GitHub Repository](https://github.com/beekeeper-studio/queryleaf) 146 | 147 | ## License 148 | 149 | QueryLeaf is dual-licensed: 150 | 151 | - [AGPL-3.0](https://github.com/beekeeper-studio/queryleaf/blob/main/LICENSE.md) for open source use 152 | - [Commercial license](https://github.com/beekeeper-studio/queryleaf/blob/main/COMMERCIAL_LICENSE.md) for commercial use 153 | 154 | For commercial licensing options, visit [queryleaf.com](https://queryleaf.com). -------------------------------------------------------------------------------- /packages/lib/build-log.txt: -------------------------------------------------------------------------------- 1 | Files: 249 2 | Lines: 100788 3 | Identifiers: 102182 4 | Symbols: 63308 5 | Types: 89 6 | Instantiations: 0 7 | Memory used: 109932K 8 | I/O read: 0.01s 9 | I/O write: 0.00s 10 | Parse time: 0.42s 11 | Bind time: 0.18s 12 | Check time: 0.00s 13 | Emit time: 0.00s 14 | Total time: 0.60s 15 | -------------------------------------------------------------------------------- /packages/lib/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/tests'], 5 | transform: { 6 | '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }] 7 | }, 8 | testMatch: ['**/tests/**/*.test.ts', '**/tests/**/*.test.js'], 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | openHandlesTimeout: 30000, // 30 seconds timeout for tests involving containers 11 | detectOpenHandles: true // Helps identify what's keeping Jest open 12 | }; -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@queryleaf/lib", 3 | "version": "0.1.0", 4 | "description": "SQL to MongoDB query translator - Core library", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "clean": "rm -rf dist", 9 | "build": "yarn clean && tsc", 10 | "typecheck": "npx tsc --noEmit", 11 | "dev": "ts-node src/index.ts", 12 | "example": "ts-node src/examples/basic-usage.ts", 13 | "test": "yarn test:unit && yarn test:integration", 14 | "test:unit": "jest tests/unit", 15 | "test:integration": "jest tests/integration --runInBand", 16 | "lint": "eslint --ext .ts src", 17 | "lint:fix": "eslint --ext .ts --fix src", 18 | "lockversion": "echo \"Skipping for lib\"" 19 | }, 20 | "keywords": [ 21 | "sql", 22 | "mongodb", 23 | "compiler", 24 | "query" 25 | ], 26 | "author": "", 27 | "license": "AGPL-3.0", 28 | "dependencies": { 29 | "debug": "^4.4.0", 30 | "node-sql-parser": "^4.11.0" 31 | }, 32 | "peerDependencies": { 33 | "mongodb": "^6.14.2" 34 | }, 35 | "devDependencies": { 36 | "@types/debug": "^4.1.12", 37 | "@types/jest": "^29.5.14", 38 | "@types/mongodb": "^4.0.7", 39 | "jest": "^29.7.0", 40 | "mongodb": "^6.14.2", 41 | "testcontainers": "^10.20.0", 42 | "ts-jest": "^29.2.6", 43 | "ts-node": "^10.9.2" 44 | }, 45 | "publishConfig": { 46 | "access": "public" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/lib/src/examples/basic-usage.ts: -------------------------------------------------------------------------------- 1 | import { QueryLeaf, CursorResult } from '../index'; 2 | import { Document, FindCursor, AggregationCursor, MongoClient } from 'mongodb'; 3 | 4 | /** 5 | * Example showing how to use QueryLeaf with an existing MongoDB client 6 | */ 7 | async function main() { 8 | // Your existing MongoDB connection 9 | const connectionString = 'mongodb://localhost:27017'; 10 | const dbName = 'example'; 11 | 12 | // In a real application, you would already have a MongoDB client 13 | const mongoClient = new MongoClient(connectionString); 14 | await mongoClient.connect(); 15 | 16 | // Create a QueryLeaf instance with your MongoDB client 17 | const queryLeaf = new QueryLeaf(mongoClient, dbName); 18 | 19 | try { 20 | // Setup sample data with nested structures and arrays 21 | console.log('\nSetting up sample data with nested structures and arrays...'); 22 | 23 | await queryLeaf.execute(` 24 | INSERT INTO users (_id, name, age, email, active, address) VALUES 25 | ('101', 'Nested User', 30, 'nested@example.com', true, { 26 | "street": "123 Main St", 27 | "city": "New York", 28 | "state": "NY", 29 | "zip": "10001" 30 | }) 31 | `); 32 | 33 | await queryLeaf.execute(` 34 | INSERT INTO orders (_id, userId, items, total) VALUES 35 | ('201', '101', [ 36 | { "id": "item1", "name": "Laptop", "price": 1200 }, 37 | { "id": "item2", "name": "Mouse", "price": 25 } 38 | ], 1225) 39 | `); 40 | 41 | // Example SQL queries 42 | const queries = [ 43 | // Basic SELECT 44 | 'SELECT * FROM users LIMIT 5', 45 | 46 | // SELECT with WHERE condition 47 | 'SELECT name, email FROM users WHERE age > 21 AND active = true', 48 | 49 | // SELECT with ORDER BY 50 | 'SELECT * FROM products ORDER BY price DESC LIMIT 3', 51 | 52 | // Nested field queries - show accessing address fields 53 | "SELECT name, address.city, address.zip FROM users WHERE _id = '101'", 54 | 55 | // Query with nested field condition 56 | "SELECT * FROM users WHERE address.city = 'New York'", 57 | 58 | // Array element access - query showing array indices 59 | "SELECT _id, items[0].name, items[0].price FROM orders WHERE _id = '201'", 60 | 61 | // Array element condition 62 | 'SELECT _id, userId FROM orders WHERE items[0].price > 1000', 63 | 64 | // GROUP BY with aggregation functions 65 | 'SELECT status, COUNT(*) as count, SUM(total) as total_amount FROM orders GROUP BY status', 66 | 67 | // JOIN between collections 68 | 'SELECT u.name, o._id as order_id, o.total FROM users u JOIN orders o ON u._id = o.userId', 69 | 70 | // INSERT example 71 | "INSERT INTO users (_id, name, age, email, active) VALUES ('100', 'Example User', 25, 'example@example.com', true)", 72 | 73 | // SELECT to verify the insertion 74 | "SELECT * FROM users WHERE _id = '100'", 75 | 76 | // UPDATE example 77 | "UPDATE users SET age = 26 WHERE _id = '100'", 78 | 79 | // SELECT to verify the update 80 | "SELECT * FROM users WHERE _id = '100'", 81 | 82 | // DELETE example 83 | "DELETE FROM users WHERE _id = '100'", 84 | 85 | // SELECT to verify the deletion 86 | "SELECT * FROM users WHERE _id = '100'", 87 | 88 | // Clean up sample data 89 | "DELETE FROM users WHERE _id = '101'", 90 | "DELETE FROM orders WHERE _id = '201'", 91 | ]; 92 | 93 | // Execute each query and display results 94 | for (const sql of queries) { 95 | console.log(`\nExecuting SQL: ${sql}`); 96 | try { 97 | const result = await queryLeaf.execute(sql); 98 | console.log('Result:', JSON.stringify(result, null, 2)); 99 | } catch (error) { 100 | console.error('Error:', error instanceof Error ? error.message : String(error)); 101 | } 102 | } 103 | // Example showing how to use the executeCursor method 104 | console.log('\nUsing the executeCursor method:'); 105 | 106 | let cursor: CursorResult = null; 107 | try { 108 | // Using the executeCursor method to get a MongoDB cursor 109 | // You can optionally specify a batch size to control memory usage 110 | cursor = await queryLeaf.executeCursor('SELECT * FROM users WHERE active = true', { 111 | batchSize: 20, 112 | }); 113 | 114 | // Check if we got a cursor back 115 | if (cursor) { 116 | // Now we can use the cursor methods directly 117 | console.log('Iterating through cursor results with forEach:'); 118 | await cursor.forEach((doc: any) => { 119 | console.log(`- User: ${doc.name}, Email: ${doc.email}`); 120 | }); 121 | } else { 122 | console.log('Expected a cursor but got null (not a SELECT/FIND query)'); 123 | } 124 | } catch (error) { 125 | console.error('Cursor error:', error instanceof Error ? error.message : String(error)); 126 | } finally { 127 | // Always close the cursor when done 128 | if (cursor) { 129 | await cursor.close(); 130 | } 131 | } 132 | } finally { 133 | // Close the MongoDB client that we created 134 | // QueryLeaf does not manage MongoDB connections 135 | await mongoClient.close(); 136 | } 137 | } 138 | 139 | // Run the example if this file is executed directly 140 | if (require.main === module) { 141 | main().catch((error) => { 142 | console.error('Unhandled error:', error); 143 | process.exit(1); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /packages/lib/src/examples/dummy-client-demo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example demonstrating the use of the DummyQueryLeaf for testing 3 | */ 4 | 5 | import { DummyQueryLeaf } from '../index'; 6 | 7 | async function main() { 8 | console.log('Creating DummyQueryLeaf for testing'); 9 | const queryLeaf = new DummyQueryLeaf('test_database'); 10 | 11 | console.log('Executing SELECT statement'); 12 | await queryLeaf.execute('SELECT * FROM users WHERE age > 21'); 13 | 14 | console.log('Executing INSERT statement'); 15 | await queryLeaf.execute(` 16 | INSERT INTO products (name, price, category) 17 | VALUES ('Laptop', 1299.99, 'Electronics') 18 | `); 19 | 20 | console.log('Executing UPDATE statement'); 21 | await queryLeaf.execute(` 22 | UPDATE users 23 | SET status = 'active', last_login = NOW() 24 | WHERE user_id = '507f1f77bcf86cd799439011' 25 | `); 26 | 27 | console.log('Executing DELETE statement'); 28 | await queryLeaf.execute(` 29 | DELETE FROM orders 30 | WHERE status = 'cancelled' AND created_at < '2023-01-01' 31 | `); 32 | 33 | console.log('Executing GROUP BY with aggregation'); 34 | await queryLeaf.execute(` 35 | SELECT category, AVG(price) as avg_price, COUNT(*) as product_count 36 | FROM products 37 | GROUP BY category 38 | HAVING AVG(price) > 100 39 | `); 40 | 41 | console.log('Executing JOIN'); 42 | await queryLeaf.execute(` 43 | SELECT o.order_id, u.name, o.total 44 | FROM orders o 45 | JOIN users u ON o.user_id = u.user_id 46 | WHERE o.status = 'shipped' 47 | `); 48 | 49 | console.log('Done with testing'); 50 | } 51 | 52 | // Run the example 53 | main().catch((error) => { 54 | console.error('Error in example:', error); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/lib/src/examples/existing-client-demo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example showing how to use QueryLeaf with an existing MongoDB client 3 | * This is the recommended way to use QueryLeaf in a real application 4 | */ 5 | 6 | import { MongoClient } from 'mongodb'; 7 | import { QueryLeaf } from '../index'; 8 | 9 | // This would be your application's existing MongoDB client setup 10 | class MyApplication { 11 | private mongoClient: MongoClient; 12 | 13 | constructor() { 14 | // Your application's MongoDB client configuration 15 | this.mongoClient = new MongoClient('mongodb://localhost:27017', { 16 | // Your custom options here 17 | connectTimeoutMS: 5000, 18 | // etc. 19 | }); 20 | } 21 | 22 | async initialize() { 23 | console.log('Initializing application...'); 24 | // Connect your MongoDB client 25 | await this.mongoClient.connect(); 26 | console.log('MongoDB client connected'); 27 | } 28 | 29 | async shutdown() { 30 | console.log('Shutting down application...'); 31 | await this.mongoClient.close(); 32 | console.log('MongoDB client disconnected'); 33 | } 34 | 35 | // Your application would use this MongoDB client directly for some operations 36 | getMongoClient() { 37 | return this.mongoClient; 38 | } 39 | } 40 | 41 | async function main() { 42 | // Create your application 43 | const app = new MyApplication(); 44 | 45 | try { 46 | // Initialize your application (connects to MongoDB) 47 | await app.initialize(); 48 | 49 | // Create QueryLeaf using your application's MongoDB client directly 50 | const queryLeaf = new QueryLeaf(app.getMongoClient(), 'example_db'); 51 | 52 | console.log('\nExecuting SQL query using your existing MongoDB client:'); 53 | 54 | // Example: Create a test collection 55 | const createQuery = ` 56 | INSERT INTO test_collection (name, value) VALUES 57 | ('Example', 42) 58 | `; 59 | 60 | console.log(`\nExecuting SQL: ${createQuery}`); 61 | const createResult = await queryLeaf.execute(createQuery); 62 | console.log('Result:', JSON.stringify(createResult, null, 2)); 63 | 64 | // Example: Query the collection 65 | const selectQuery = 'SELECT * FROM test_collection'; 66 | console.log(`\nExecuting SQL: ${selectQuery}`); 67 | const selectResult = await queryLeaf.execute(selectQuery); 68 | console.log('Result:', JSON.stringify(selectResult, null, 2)); 69 | 70 | // Clean up 71 | const cleanupQuery = 'DELETE FROM test_collection'; 72 | console.log(`\nExecuting SQL: ${cleanupQuery}`); 73 | const cleanupResult = await queryLeaf.execute(cleanupQuery); 74 | console.log('Result:', JSON.stringify(cleanupResult, null, 2)); 75 | 76 | // You can close QueryLeaf, but it won't close your MongoDB client 77 | await queryLeaf.close(); 78 | } finally { 79 | // Your application manages the MongoDB client lifecycle 80 | await app.shutdown(); 81 | } 82 | } 83 | 84 | // Run the example 85 | if (require.main === module) { 86 | main().catch((error) => { 87 | console.error('Error:', error); 88 | process.exit(1); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /packages/lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SqlStatement, 3 | Command, 4 | SqlParser, 5 | SqlCompiler, 6 | CommandExecutor, 7 | ExecutionResult, 8 | CursorResult, 9 | CursorOptions, 10 | } from './interfaces'; 11 | import { Document, MongoClient } from 'mongodb'; 12 | import { SqlParserImpl } from './parser'; 13 | import { SqlCompilerImpl } from './compiler'; 14 | import { MongoExecutor } from './executor'; 15 | import { DummyMongoClient } from './executor/dummy-client'; 16 | 17 | /** 18 | * QueryLeaf: SQL to MongoDB query translator 19 | */ 20 | export class QueryLeaf { 21 | private parser: SqlParser; 22 | private compiler: SqlCompiler; 23 | private executor: CommandExecutor; 24 | 25 | /** 26 | * Create a new QueryLeaf instance with your MongoDB client 27 | * @param client Your MongoDB client 28 | * @param dbName Database name 29 | */ 30 | constructor(client: MongoClient, dbName: string) { 31 | this.parser = new SqlParserImpl(); 32 | this.compiler = new SqlCompilerImpl(); 33 | this.executor = new MongoExecutor(client, dbName); 34 | } 35 | 36 | /** 37 | * Execute a SQL query on MongoDB and return documents 38 | * @param sql SQL query string 39 | * @returns Document results (no cursors) 40 | * @typeParam T - The type of documents that will be returned (defaults to Document) 41 | */ 42 | async execute(sql: string): Promise> { 43 | const statement = this.parse(sql); 44 | const commands = this.compile(statement); 45 | return await this.executor.execute(commands); 46 | } 47 | 48 | /** 49 | * Execute a SQL query on MongoDB and return a cursor 50 | * @param sql SQL query string 51 | * @param options Options for cursor execution 52 | * @returns Cursor for SELECT queries, null for other queries 53 | * @typeParam T - The type of documents that will be returned (defaults to Document) 54 | */ 55 | async executeCursor( 56 | sql: string, 57 | options?: CursorOptions 58 | ): Promise> { 59 | const statement = this.parse(sql); 60 | const commands = this.compile(statement); 61 | return await this.executor.executeCursor(commands, options); 62 | } 63 | 64 | /** 65 | * Parse a SQL query string 66 | * @param sql SQL query string 67 | * @returns Parsed SQL statement 68 | */ 69 | parse(sql: string): SqlStatement { 70 | return this.parser.parse(sql); 71 | } 72 | 73 | /** 74 | * Compile a SQL statement to MongoDB commands 75 | * @param statement SQL statement 76 | * @returns MongoDB commands 77 | */ 78 | compile(statement: SqlStatement): Command[] { 79 | return this.compiler.compile(statement); 80 | } 81 | 82 | /** 83 | * Get the command executor instance 84 | * @returns Command executor 85 | */ 86 | getExecutor(): CommandExecutor { 87 | return this.executor; 88 | } 89 | 90 | /** 91 | * No-op method for backward compatibility 92 | * QueryLeaf no longer manages MongoDB connections 93 | */ 94 | async close(): Promise { 95 | // No-op - MongoDB client is managed by the user 96 | } 97 | } 98 | 99 | /** 100 | * Create a QueryLeaf instance with a dummy client for testing 101 | * No actual MongoDB connection is made 102 | */ 103 | export class DummyQueryLeaf extends QueryLeaf { 104 | /** 105 | * Create a new DummyQueryLeaf instance 106 | * @param dbName Database name 107 | */ 108 | constructor(dbName: string) { 109 | super(new DummyMongoClient(), dbName); 110 | } 111 | } 112 | 113 | // Export interfaces and implementation classes 114 | export { 115 | SqlStatement, 116 | Command, 117 | SqlParser, 118 | SqlCompiler, 119 | CommandExecutor, 120 | ExecutionResult, 121 | CursorResult, 122 | SqlParserImpl, 123 | SqlCompilerImpl, 124 | MongoExecutor, 125 | DummyMongoClient, 126 | }; 127 | 128 | // Re-export interfaces 129 | export * from './interfaces'; 130 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { AST } from 'node-sql-parser'; 2 | import { Document, FindCursor, AggregationCursor } from 'mongodb'; 3 | 4 | /** 5 | * Represents a parsed SQL statement 6 | */ 7 | export interface SqlStatement { 8 | ast: AST; 9 | text: string; 10 | metadata?: { 11 | nestedFieldReplacements?: [string, string][]; // Placeholder to original field mapping 12 | }; 13 | } 14 | 15 | /** 16 | * Command types supported by the MongoDB executor 17 | */ 18 | export type CommandType = 'FIND' | 'INSERT' | 'UPDATE' | 'DELETE' | 'AGGREGATE'; 19 | 20 | /** 21 | * Base interface for all MongoDB commands 22 | */ 23 | export interface BaseCommand { 24 | type: CommandType; 25 | collection: string; 26 | } 27 | 28 | /** 29 | * Find command for MongoDB 30 | */ 31 | export interface FindCommand extends BaseCommand { 32 | type: 'FIND'; 33 | filter?: Record; 34 | projection?: Record; 35 | sort?: Record; 36 | limit?: number; 37 | skip?: number; 38 | group?: { 39 | _id: any; 40 | [key: string]: any; 41 | }; 42 | pipeline?: Record[]; 43 | lookup?: { 44 | from: string; 45 | localField: string; 46 | foreignField: string; 47 | as: string; 48 | }[]; 49 | } 50 | 51 | /** 52 | * Insert command for MongoDB 53 | */ 54 | export interface InsertCommand extends BaseCommand { 55 | type: 'INSERT'; 56 | documents: Record[]; 57 | } 58 | 59 | /** 60 | * Update command for MongoDB 61 | */ 62 | export interface UpdateCommand extends BaseCommand { 63 | type: 'UPDATE'; 64 | filter?: Record; 65 | update: Record; 66 | upsert?: boolean; 67 | } 68 | 69 | /** 70 | * Delete command for MongoDB 71 | */ 72 | export interface DeleteCommand extends BaseCommand { 73 | type: 'DELETE'; 74 | filter?: Record; 75 | } 76 | 77 | /** 78 | * Aggregate command for MongoDB 79 | */ 80 | export interface AggregateCommand extends BaseCommand { 81 | type: 'AGGREGATE'; 82 | pipeline: Record[]; 83 | } 84 | 85 | /** 86 | * Union type of all MongoDB commands 87 | */ 88 | export type Command = 89 | | FindCommand 90 | | InsertCommand 91 | | UpdateCommand 92 | | DeleteCommand 93 | | AggregateCommand; 94 | 95 | /** 96 | * SQL parser interface 97 | */ 98 | export interface SqlParser { 99 | parse(sql: string): SqlStatement; 100 | } 101 | 102 | /** 103 | * SQL to MongoDB compiler interface 104 | */ 105 | export interface SqlCompiler { 106 | compile(statement: SqlStatement): Command[]; 107 | } 108 | 109 | /** 110 | * Represents result types that can be returned by the executor 111 | */ 112 | export type ExecutionResult = 113 | | Document[] // Array of documents (default for FIND and AGGREGATE) 114 | | Document // Single document or operation result (for INSERT, UPDATE, DELETE) 115 | | null; // No result 116 | 117 | /** 118 | * Represents cursor types that can be returned by the cursor executor 119 | */ 120 | export type CursorResult = 121 | | FindCursor // Cursor from FIND command 122 | | AggregationCursor // Cursor from AGGREGATE command 123 | | null; // No result 124 | 125 | /** 126 | * Options for cursor execution 127 | */ 128 | export interface CursorOptions { 129 | /** 130 | * Number of documents to fetch per batch 131 | * This will be set directly on the query command, not on the cursor after creation 132 | */ 133 | batchSize?: number; 134 | } 135 | 136 | /** 137 | * MongoDB command executor interface 138 | */ 139 | export interface CommandExecutor { 140 | connect(): Promise; 141 | close(): Promise; 142 | /** 143 | * Execute MongoDB commands and return documents 144 | * @param commands Array of commands to execute 145 | * @returns Document results (no cursors) 146 | */ 147 | execute(commands: Command[]): Promise>; 148 | 149 | /** 150 | * Execute MongoDB commands and return cursors for FIND and AGGREGATE commands 151 | * @param commands Array of commands to execute 152 | * @param options Options for cursor execution 153 | * @returns Cursor for FIND and AGGREGATE commands, null for other commands 154 | */ 155 | executeCursor( 156 | commands: Command[], 157 | options?: CursorOptions 158 | ): Promise>; 159 | } 160 | 161 | /** 162 | * Main QueryLeaf interface 163 | */ 164 | export interface QueryLeaf { 165 | /** 166 | * Execute a SQL query and return documents 167 | * @param sql SQL query string 168 | * @returns Document results (no cursors) 169 | */ 170 | execute(sql: string): Promise>; 171 | 172 | /** 173 | * Execute a SQL query and return a cursor for SELECT queries 174 | * @param sql SQL query string 175 | * @param options Options for cursor execution 176 | * @returns Cursor for SELECT queries, null for other queries 177 | */ 178 | executeCursor(sql: string, options?: CursorOptions): Promise>; 179 | 180 | parse(sql: string): SqlStatement; 181 | compile(statement: SqlStatement): Command[]; 182 | getExecutor(): CommandExecutor; 183 | close(): Promise; 184 | } 185 | 186 | export interface Squongo extends QueryLeaf { 187 | execute(sql: string): Promise>; 188 | executeCursor(sql: string, options?: CursorOptions): Promise>; 189 | parse(sql: string): SqlStatement; 190 | compile(statement: SqlStatement): Command[]; 191 | getExecutor(): CommandExecutor; 192 | close(): Promise; 193 | } 194 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Core interfaces for the Squongo SQL to MongoDB compiler 3 | */ 4 | 5 | /** 6 | * Represents an abstract command in the intermediate representation 7 | */ 8 | export interface Command { 9 | type: string; 10 | [key: string]: any; 11 | } 12 | 13 | /** 14 | * Base interface for all SQL statements 15 | */ 16 | export interface SqlStatement { 17 | type: string; 18 | } 19 | 20 | /** 21 | * Represents a SQL SELECT statement 22 | */ 23 | export interface SelectStatement extends SqlStatement { 24 | type: 'SELECT'; 25 | fields: string[]; 26 | from: string; 27 | where?: WhereClause; 28 | limit?: number; 29 | offset?: number; 30 | orderBy?: OrderByClause[]; 31 | } 32 | 33 | /** 34 | * Represents a SQL INSERT statement 35 | */ 36 | export interface InsertStatement extends SqlStatement { 37 | type: 'INSERT'; 38 | into: string; 39 | columns: string[]; 40 | values: any[][]; 41 | } 42 | 43 | /** 44 | * Represents a SQL UPDATE statement 45 | */ 46 | export interface UpdateStatement extends SqlStatement { 47 | type: 'UPDATE'; 48 | table: string; 49 | set: { [key: string]: any }; 50 | where?: WhereClause; 51 | } 52 | 53 | /** 54 | * Represents a SQL DELETE statement 55 | */ 56 | export interface DeleteStatement extends SqlStatement { 57 | type: 'DELETE'; 58 | from: string; 59 | where?: WhereClause; 60 | } 61 | 62 | /** 63 | * Represents an ORDER BY clause 64 | */ 65 | export interface OrderByClause { 66 | field: string; 67 | direction: 'ASC' | 'DESC'; 68 | } 69 | 70 | /** 71 | * Represents a WHERE clause condition 72 | */ 73 | export interface WhereClause { 74 | operator: 'AND' | 'OR'; 75 | conditions: Condition[]; 76 | } 77 | 78 | /** 79 | * Represents a condition in a WHERE clause 80 | */ 81 | export interface Condition { 82 | type: 'COMPARISON' | 'LOGICAL' | 'IN' | 'BETWEEN' | 'LIKE' | 'NULL'; 83 | field?: string; 84 | operator?: string; 85 | value?: any; 86 | left?: Condition; 87 | right?: Condition; 88 | values?: any[]; 89 | not?: boolean; 90 | } 91 | 92 | /** 93 | * MongoDB command executor interface 94 | */ 95 | export interface CommandExecutor { 96 | execute(commands: Command[]): Promise; 97 | } 98 | 99 | /** 100 | * SQL Parser interface 101 | */ 102 | export interface SqlParser { 103 | parse(sql: string): SqlStatement; 104 | } 105 | 106 | /** 107 | * SQL Compiler interface - converts SQL AST to MongoDB commands 108 | */ 109 | export interface SqlCompiler { 110 | compile(statement: SqlStatement): Command[]; 111 | } 112 | 113 | /** 114 | * Main QueryLeaf interface 115 | */ 116 | export interface QueryLeaf { 117 | execute(sql: string): Promise; 118 | parse(sql: string): SqlStatement; 119 | compile(statement: SqlStatement): Command[]; 120 | getExecutor?(): any; 121 | close?(): Promise; 122 | } 123 | 124 | /** 125 | * Alias for QueryLeaf (backwards compatibility) 126 | */ 127 | export interface Squongo extends QueryLeaf { 128 | execute(sql: string): Promise; 129 | parse(sql: string): SqlStatement; 130 | compile(statement: SqlStatement): Command[]; 131 | } 132 | -------------------------------------------------------------------------------- /packages/lib/tests/integration/edge-cases.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import { testSetup, createLogger, ensureArray } from './test-setup'; 3 | 4 | const log = createLogger('edge-cases'); 5 | 6 | describe('Edge Cases Integration Tests', () => { 7 | beforeAll(async () => { 8 | await testSetup.init(); 9 | }, 30000); // 30 second timeout for container startup 10 | 11 | afterAll(async () => { 12 | // Make sure to close any outstanding connections 13 | const queryLeaf = testSetup.getQueryLeaf(); 14 | 15 | // Clean up any resources that squongo might be using 16 | if (typeof queryLeaf.close === 'function') { 17 | await queryLeaf.close(); 18 | } 19 | 20 | // Clean up test setup resources 21 | await testSetup.cleanup(); 22 | }, 10000); // Give it more time to clean up 23 | 24 | beforeEach(async () => { 25 | // Clean up collections before each test 26 | const db = testSetup.getDb(); 27 | await db.collection('edge_test').deleteMany({}); 28 | await db.collection('missing_collection').deleteMany({}); 29 | }); 30 | 31 | afterEach(async () => { 32 | // Clean up collections after each test 33 | const db = testSetup.getDb(); 34 | await db.collection('edge_test').deleteMany({}); 35 | }); 36 | 37 | test('should handle special characters in field names', async () => { 38 | // Arrange 39 | const db = testSetup.getDb(); 40 | await db.collection('edge_test').insertMany([ 41 | { 'field_with_underscores': 'value1', name: 'item1' }, 42 | { 'field_with_numbers123': 'value2', name: 'item2' }, 43 | { 'UPPERCASE_FIELD': 'value3', name: 'item3' }, 44 | { 'mixedCaseField': 'value4', name: 'item4' }, 45 | { 'snake_case_field': 'value5', name: 'item5' } 46 | ]); 47 | 48 | // Act 49 | const queryLeaf = testSetup.getQueryLeaf(); 50 | // Since SQL parsers often have issues with special characters, we'll use identifiers that are more likely 51 | // to be supported by most SQL parsers 52 | const sql = 'SELECT name, field_with_underscores FROM edge_test WHERE field_with_underscores = "value1"'; 53 | 54 | const results = ensureArray(await queryLeaf.execute(sql)); 55 | 56 | // Assert 57 | expect(results).toHaveLength(1); 58 | expect(results[0].name).toBe('item1'); 59 | expect(results[0].field_with_underscores).toBe('value1'); 60 | }); 61 | 62 | test('should gracefully handle invalid SQL syntax', async () => { 63 | // Arrange 64 | const queryLeaf = testSetup.getQueryLeaf(); 65 | const invalidSql = 'SELECT FROM users WHERE;'; // Missing column and invalid WHERE clause 66 | 67 | // Act & Assert 68 | await expect(queryLeaf.execute(invalidSql)).rejects.toThrow(); 69 | }); 70 | 71 | test('should gracefully handle valid SQL but unsupported features', async () => { 72 | // Arrange 73 | const queryLeaf = testSetup.getQueryLeaf(); 74 | // SQL with PIVOT which is not widely supported in most SQL implementations 75 | const unsupportedSql = 'SELECT * FROM (SELECT category, price FROM products) PIVOT (SUM(price) FOR category IN ("Electronics", "Furniture"))'; 76 | 77 | // Act & Assert 78 | await expect(queryLeaf.execute(unsupportedSql)).rejects.toThrow(); 79 | }); 80 | 81 | test('should handle behavior with missing collections', async () => { 82 | // Arrange 83 | const queryLeaf = testSetup.getQueryLeaf(); 84 | const sql = 'SELECT * FROM nonexistent_collection'; 85 | 86 | // Act 87 | const results = await queryLeaf.execute(sql); 88 | 89 | // Assert - should return empty array rather than throwing an error 90 | expect(Array.isArray(results)).toBe(true); 91 | expect(results).toHaveLength(0); 92 | }); 93 | 94 | test('should handle invalid data types appropriately', async () => { 95 | // Arrange 96 | const db = testSetup.getDb(); 97 | await db.collection('edge_test').insertMany([ 98 | { name: 'item1', value: 123 }, 99 | { name: 'item2', value: 'not a number' } 100 | ]); 101 | 102 | // Act 103 | const queryLeaf = testSetup.getQueryLeaf(); 104 | // Try to do numerical comparison on non-numeric data 105 | const sql = 'SELECT name FROM edge_test WHERE value > 100'; 106 | 107 | const results = ensureArray(await queryLeaf.execute(sql)); 108 | 109 | // Assert - should only find the numeric value that's valid for comparison 110 | expect(results).toHaveLength(1); 111 | expect(results[0].name).toBe('item1'); 112 | }); 113 | 114 | test('should handle MongoDB ObjectId conversions', async () => { 115 | // Arrange 116 | const db = testSetup.getDb(); 117 | const objectId = new ObjectId(); 118 | await db.collection('edge_test').insertOne({ 119 | _id: objectId, 120 | name: 'ObjectId Test' 121 | }); 122 | 123 | // Act 124 | const queryLeaf = testSetup.getQueryLeaf(); 125 | // Use the string representation of ObjectId in SQL 126 | const sql = `SELECT name FROM edge_test WHERE _id = '${objectId.toString()}'`; 127 | 128 | const results = ensureArray(await queryLeaf.execute(sql)); 129 | 130 | // Assert 131 | expect(results).toHaveLength(1); 132 | expect(results[0].name).toBe('ObjectId Test'); 133 | }); 134 | 135 | test('should handle extremely large result sets', async () => { 136 | // Arrange 137 | const db = testSetup.getDb(); 138 | const largeDataset = Array.from({ length: 1000 }, (_, i) => ({ 139 | index: i, 140 | name: `Item ${i}`, 141 | value: Math.random() * 1000 142 | })); 143 | 144 | await db.collection('edge_test').insertMany(largeDataset); 145 | 146 | // Act 147 | const queryLeaf = testSetup.getQueryLeaf(); 148 | const sql = 'SELECT * FROM edge_test'; 149 | 150 | const results = ensureArray(await queryLeaf.execute(sql)); 151 | 152 | // Assert 153 | expect(results).toHaveLength(1000); 154 | expect(results[0]).toHaveProperty('index'); 155 | expect(results[0]).toHaveProperty('name'); 156 | expect(results[0]).toHaveProperty('value'); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /packages/lib/tests/integration/nested-update-issue.test.ts: -------------------------------------------------------------------------------- 1 | import { testSetup } from './test-setup'; 2 | 3 | describe('Nested Fields Update Issue', () => { 4 | beforeAll(async () => { 5 | await testSetup.init(); 6 | }, 30000); // 30 second timeout for container startup 7 | 8 | afterAll(async () => { 9 | await testSetup.cleanup(); 10 | }); 11 | 12 | beforeEach(async () => { 13 | // Clean up test data 14 | const db = testSetup.getDb(); 15 | await db.collection('customers').deleteMany({}); 16 | 17 | // Insert test data with nested address object 18 | await db.collection('customers').insertOne({ 19 | name: 'John Doe', 20 | email: 'john@example.com', 21 | address: { 22 | street: '123 Main St', 23 | city: 'New York', 24 | zip: '10001' 25 | } 26 | }); 27 | }); 28 | 29 | afterEach(async () => { 30 | // Clean up test data 31 | const db = testSetup.getDb(); 32 | await db.collection('customers').deleteMany({}); 33 | }); 34 | 35 | 36 | test('should update a field within a nested object, not create a top-level field', async () => { 37 | // Arrange 38 | const queryLeaf = testSetup.getQueryLeaf(); 39 | const db = testSetup.getDb(); 40 | 41 | // Act - Update the nested city field in the address 42 | const updateSql = `UPDATE customers SET address.city = 'Calgary' WHERE name = 'John Doe'`; 43 | 44 | // Execute the SQL via QueryLeaf 45 | await queryLeaf.execute(updateSql); 46 | 47 | // Get the result after the update 48 | const updatedCustomer = await db.collection('customers').findOne({ name: 'John Doe' }); 49 | 50 | // Assert - should NOT have added a top-level 'city' field 51 | expect(updatedCustomer).not.toHaveProperty('city'); 52 | 53 | // Should have updated the nested address.city field 54 | expect(updatedCustomer?.address?.city).toBe('Calgary'); 55 | 56 | // Make sure other address fields remain intact 57 | expect(updatedCustomer?.address?.street).toBe('123 Main St'); 58 | expect(updatedCustomer?.address?.zip).toBe('10001'); 59 | }); 60 | 61 | // With our new multi-level nested field support, this test should now pass 62 | test('should update a deeply nested field', async () => { 63 | // Arrange 64 | const queryLeaf = testSetup.getQueryLeaf(); 65 | const db = testSetup.getDb(); 66 | 67 | // Insert a customer with a more deeply nested structure 68 | await db.collection('customers').insertOne({ 69 | name: 'Bob Johnson', 70 | email: 'bob@example.com', 71 | shipping: { 72 | address: { 73 | street: '789 Oak St', 74 | city: 'Chicago', 75 | state: 'IL', 76 | country: { 77 | name: 'USA', 78 | code: 'US' 79 | } 80 | } 81 | } 82 | }); 83 | 84 | // Act - Update a deeply nested field 85 | const updateSql = ` 86 | UPDATE customers 87 | SET shipping.address.country.name = 'Canada' 88 | WHERE name = 'Bob Johnson' 89 | `; 90 | 91 | // Execute the SQL via QueryLeaf 92 | await queryLeaf.execute(updateSql); 93 | 94 | // Get the result after the update 95 | const updatedCustomer = await db.collection('customers').findOne({ name: 'Bob Johnson' }); 96 | 97 | // Assert - the deeply nested field should be updated 98 | expect(updatedCustomer?.shipping?.address?.country?.name).toBe('Canada'); 99 | 100 | // The original code should still be there 101 | expect(updatedCustomer?.shipping?.address?.country?.code).toBe('US'); 102 | 103 | // Other fields should remain intact 104 | expect(updatedCustomer?.shipping?.address?.city).toBe('Chicago'); 105 | expect(updatedCustomer?.shipping?.address?.state).toBe('IL'); 106 | }); 107 | 108 | test('should update a field within an array element', async () => { 109 | // Arrange 110 | const queryLeaf = testSetup.getQueryLeaf(); 111 | const db = testSetup.getDb(); 112 | 113 | // Insert a customer with an array of addresses 114 | await db.collection('customers').insertOne({ 115 | name: 'Alice Johnson', 116 | email: 'alice@example.com', 117 | addresses: [ 118 | { 119 | type: 'home', 120 | street: '123 Maple St', 121 | city: 'Toronto', 122 | postalCode: 'M5V 2N4', 123 | country: 'Canada' 124 | }, 125 | { 126 | type: 'work', 127 | street: '456 Bay St', 128 | city: 'Toronto', 129 | postalCode: 'M5H 2S1', 130 | country: 'Canada' 131 | } 132 | ] 133 | }); 134 | 135 | // Act - Update a field in the first array element 136 | const updateSql = `UPDATE customers SET addresses[0].postalCode = 'T1K 4B8' WHERE name = 'Alice Johnson'`; 137 | 138 | // Execute the SQL via QueryLeaf 139 | await queryLeaf.execute(updateSql); 140 | 141 | // Get the result after the update 142 | const updatedCustomer = await db.collection('customers').findOne({ name: 'Alice Johnson' }); 143 | 144 | // Assert - the field in the array element should be updated 145 | expect(updatedCustomer?.addresses[0]?.postalCode).toBe('T1K 4B8'); 146 | 147 | // Make sure other fields in the array element remain intact 148 | expect(updatedCustomer?.addresses[0]?.type).toBe('home'); 149 | expect(updatedCustomer?.addresses[0]?.street).toBe('123 Maple St'); 150 | expect(updatedCustomer?.addresses[0]?.city).toBe('Toronto'); 151 | 152 | // Make sure the second array element is unchanged 153 | expect(updatedCustomer?.addresses[1]?.postalCode).toBe('M5H 2S1'); 154 | 155 | // Make sure no top-level fields were created by mistake 156 | expect(updatedCustomer).not.toHaveProperty('postalCode'); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /packages/lib/tests/integration/test-setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders } from '../utils/mongo-container'; 2 | import { QueryLeaf, ExecutionResult } from '../../src/index'; 3 | import { Db, Document } from 'mongodb'; 4 | import debug from 'debug'; 5 | 6 | /** 7 | * Base test setup for integration tests 8 | */ 9 | export class IntegrationTestSetup { 10 | public mongoContainer: MongoTestContainer; 11 | public TEST_DB = 'queryleaf_test'; 12 | 13 | constructor() { 14 | this.mongoContainer = new MongoTestContainer(); 15 | } 16 | 17 | /** 18 | * Initialize the test environment 19 | */ 20 | async init(): Promise { 21 | await this.mongoContainer.start(); 22 | const db = this.mongoContainer.getDatabase(this.TEST_DB); 23 | await loadFixtures(db); 24 | } 25 | 26 | /** 27 | * Clean up the test environment 28 | */ 29 | async cleanup(): Promise { 30 | const log = debug('queryleaf:test:cleanup'); 31 | try { 32 | // Stop the container - this will close the connection 33 | await this.mongoContainer.stop(); 34 | } catch (err) { 35 | log('Error stopping MongoDB container:', err); 36 | } 37 | } 38 | 39 | /** 40 | * Get a database instance 41 | */ 42 | getDb(): Db { 43 | return this.mongoContainer.getDatabase(this.TEST_DB); 44 | } 45 | 46 | /** 47 | * Create a new QueryLeaf instance 48 | */ 49 | getQueryLeaf() { 50 | const client = this.mongoContainer.getClient(); 51 | return new QueryLeaf(client, this.TEST_DB); 52 | } 53 | } 54 | 55 | /** 56 | * Create a shared instance for test files 57 | */ 58 | export const testSetup = new IntegrationTestSetup(); 59 | 60 | /** 61 | * Export fixture data for tests 62 | */ 63 | export { testUsers, testProducts, testOrders }; 64 | 65 | /** 66 | * Create debug logger for tests 67 | */ 68 | export const createLogger = (namespace: string) => debug(`queryleaf:test:${namespace}`); 69 | 70 | /** 71 | * Helper function to handle type checking for results from queryLeaf.execute() 72 | * This helps ensure proper type checking in tests for array results 73 | * @param result The result from queryLeaf.execute() 74 | * @returns The result as an array (throws if not an array) 75 | */ 76 | export function ensureArray(result: ExecutionResult): Array { 77 | if (!Array.isArray(result)) { 78 | throw new Error('Expected result to be an array, but got: ' + typeof result); 79 | } 80 | return result as Array; 81 | } 82 | 83 | /** 84 | * Helper function to handle type checking for results from queryLeaf.execute() for operation results 85 | * @param result The result from queryLeaf.execute() 86 | * @returns The result as a Document (throws if it's an array or cursor) 87 | */ 88 | export function ensureDocument(result: ExecutionResult): Document { 89 | if (Array.isArray(result) || result === null) { 90 | throw new Error('Expected result to be a document, but got: ' + 91 | (Array.isArray(result) ? 'array' : 92 | result === null ? 'null' : typeof result)); 93 | } 94 | return result; 95 | } 96 | -------------------------------------------------------------------------------- /packages/lib/tests/unit/group-test.js: -------------------------------------------------------------------------------- 1 | // A simple test for group by functionality 2 | const { SqlParserImpl } = require('../parser'); 3 | const { SqlCompilerImpl } = require('../compiler'); 4 | 5 | const parser = new SqlParserImpl(); 6 | const compiler = new SqlCompilerImpl(); 7 | 8 | function testGroupBy() { 9 | const sql = "SELECT category, COUNT(*) as count, AVG(price) as avg_price FROM products GROUP BY category"; 10 | const statement = parser.parse(sql); 11 | console.log('Parsed statement:', JSON.stringify(statement.ast, null, 2)); 12 | 13 | const commands = compiler.compile(statement); 14 | console.log('Generated commands:', JSON.stringify(commands, null, 2)); 15 | 16 | // Check the results 17 | if (commands.length !== 1) { 18 | console.error('Expected 1 command, got', commands.length); 19 | return false; 20 | } 21 | 22 | const command = commands[0]; 23 | if (command.type !== 'FIND') { 24 | console.error('Expected FIND command, got', command.type); 25 | return false; 26 | } 27 | 28 | if (!command.pipeline) { 29 | console.error('Expected pipeline, but none found'); 30 | return false; 31 | } 32 | 33 | // Find group stage in pipeline 34 | const groupStage = command.pipeline.find(stage => '$group' in stage); 35 | if (!groupStage) { 36 | console.error('Expected $group stage, but none found'); 37 | return false; 38 | } 39 | 40 | console.log('Group stage:', JSON.stringify(groupStage, null, 2)); 41 | 42 | if (!groupStage.$group.count) { 43 | console.error('Expected count in group stage, but not found'); 44 | return false; 45 | } 46 | 47 | if (!groupStage.$group.avg_price) { 48 | console.error('Expected avg_price in group stage, but not found'); 49 | return false; 50 | } 51 | 52 | console.log('Group by test PASSED'); 53 | return true; 54 | } 55 | 56 | testGroupBy(); -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "declaration": true 7 | }, 8 | "include": ["src/**/*"] 9 | } -------------------------------------------------------------------------------- /packages/postgres-server/README.md: -------------------------------------------------------------------------------- 1 |

2 | QueryLeaf Logo 3 |

4 | 5 |

@queryleaf/postgres-server

6 | 7 |

PostgreSQL wire-compatible server for QueryLeaf

8 | 9 | ## Overview 10 | 11 | `@queryleaf/postgres-server` provides a PostgreSQL wire protocol compatible server for QueryLeaf, allowing you to connect to MongoDB using standard PostgreSQL clients and drivers. It implements the PostgreSQL wire protocol and leverages the core `@queryleaf/lib` package to translate SQL queries into MongoDB commands. 12 | 13 | ## Key Features 14 | 15 | - Connect to MongoDB using any PostgreSQL client (pgAdmin, psql, etc.) 16 | - Full SQL support for querying MongoDB collections 17 | - Transparent authentication passthrough 18 | - Compatible with any tool that uses PostgreSQL drivers 19 | - Convert MongoDB documents to PostgreSQL-compatible row format 20 | 21 | ## Installation 22 | 23 | ```bash 24 | # Global installation 25 | npm install -g @queryleaf/postgres-server 26 | # or 27 | yarn global add @queryleaf/postgres-server 28 | 29 | # Local installation 30 | npm install @queryleaf/postgres-server 31 | # or 32 | yarn add @queryleaf/postgres-server 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Running the Server 38 | 39 | ```bash 40 | # Start the server with default settings 41 | queryleaf-pg-server 42 | 43 | # With specific MongoDB URI and port 44 | queryleaf-pg-server --uri mongodb://localhost:27017 --port 5432 45 | 46 | # With authentication 47 | queryleaf-pg-server --uri mongodb://user:pass@localhost:27017 48 | 49 | # Help 50 | queryleaf-pg-server --help 51 | ``` 52 | 53 | ### Connecting with PostgreSQL Clients 54 | 55 | Connect using any PostgreSQL client with these connection parameters: 56 | - Host: localhost (or wherever the server is running) 57 | - Port: 5432 (or your configured port) 58 | - Database: your MongoDB database name 59 | - Username/Password: if required by your MongoDB deployment 60 | 61 | Example with psql: 62 | ```bash 63 | psql -h localhost -p 5432 -d mydatabase 64 | ``` 65 | 66 | Example connection string: 67 | ``` 68 | postgresql://localhost:5432/mydatabase 69 | ``` 70 | 71 | ## SQL Query Examples 72 | 73 | Once connected, you can use SQL syntax to query MongoDB: 74 | 75 | ```sql 76 | -- Basic SELECT with WHERE 77 | SELECT name, email FROM users WHERE age > 21; 78 | 79 | -- Nested field access 80 | SELECT name, address.city FROM users WHERE address.zip = '10001'; 81 | 82 | -- Array access 83 | SELECT items[0].name FROM orders WHERE items[0].price > 100; 84 | 85 | -- GROUP BY with aggregation 86 | SELECT status, COUNT(*) as count FROM orders GROUP BY status; 87 | 88 | -- JOIN between collections 89 | SELECT u.name, o.total FROM users u JOIN orders o ON u._id = o.userId; 90 | ``` 91 | 92 | ## Configuration 93 | 94 | You can configure the server using command-line arguments or environment variables: 95 | 96 | | Argument | Environment Variable | Description | 97 | |----------------|-------------------------|----------------------------------| 98 | | `--uri` | `MONGODB_URI` | MongoDB connection URI | 99 | | `--port` | `POSTGRES_PORT` | Server port (default: 5432) | 100 | | `--host` | `POSTGRES_HOST` | Server host (default: 0.0.0.0) | 101 | | `--auth` | `ENABLE_AUTH` | Enable authentication passthrough| 102 | | `--debug` | `DEBUG` | Enable debug output | 103 | 104 | ## Links 105 | 106 | - [Website](https://queryleaf.com) 107 | - [Documentation](https://queryleaf.com/docs) 108 | - [GitHub Repository](https://github.com/beekeeper-studio/queryleaf) 109 | 110 | ## License 111 | 112 | QueryLeaf is dual-licensed: 113 | 114 | - [AGPL-3.0](https://github.com/beekeeper-studio/queryleaf/blob/main/LICENSE.md) for open source use 115 | - [Commercial license](https://github.com/beekeeper-studio/queryleaf/blob/main/COMMERCIAL_LICENSE.md) for commercial use 116 | 117 | For commercial licensing options, visit [queryleaf.com](https://queryleaf.com). -------------------------------------------------------------------------------- /packages/postgres-server/bin/queryleaf-pg-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../dist/pg-server"); -------------------------------------------------------------------------------- /packages/postgres-server/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: [ 6 | "**/tests/**/*.test.ts" 7 | ], 8 | collectCoverage: true, 9 | coverageDirectory: 'coverage', 10 | collectCoverageFrom: [ 11 | 'src/**/*.ts', 12 | '!src/**/*.d.ts', 13 | ], 14 | // Resolve @queryleaf/lib from the monorepo 15 | // Add options for path mapping 16 | modulePaths: ['/../'], 17 | // Tell Jest to transpile lib files too when importing 18 | transformIgnorePatterns: [ 19 | '/node_modules/', 20 | // Don't ignore lib package 21 | '!/node_modules/@queryleaf/lib' 22 | ], 23 | }; -------------------------------------------------------------------------------- /packages/postgres-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@queryleaf/postgres-server", 3 | "version": "0.1.0", 4 | "description": "PostgreSQL wire-compatible server for QueryLeaf", 5 | "main": "dist/pg-server.js", 6 | "bin": { 7 | "queryleaf-pg-server": "./bin/queryleaf-pg-server" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf dist", 11 | "build": "yarn clean && tsc", 12 | "typecheck": "tsc --noEmit", 13 | "start": "ts-node src/pg-server.ts", 14 | "test": "yarn test:unit && yarn test:integration", 15 | "test:unit": "jest tests/unit", 16 | "test:integration": "jest tests/integration --runInBand", 17 | "example:protocol": "DEBUG=queryleaf:* ts-node examples/protocol-example.ts", 18 | "lockversion": "sed -i -E \"s|(\\\"@queryleaf/lib\\\"\\s*:\\s*\\\")[^\\\"]+(\\\")|\\1$VERSION\\2|\" package.json" 19 | }, 20 | "keywords": [ 21 | "sql", 22 | "mongodb", 23 | "postgresql", 24 | "postgres", 25 | "wire protocol", 26 | "compiler", 27 | "query", 28 | "server" 29 | ], 30 | "author": "", 31 | "license": "AGPL-3.0", 32 | "dependencies": { 33 | "@queryleaf/lib": "0.1.0", 34 | "debug": "^4.4.0", 35 | "mongodb": "^6.14.2", 36 | "pg-protocol": "^1.8.0", 37 | "pg-query-stream": "^4.5.3", 38 | "yargs": "^17.7.2" 39 | }, 40 | "devDependencies": { 41 | "@types/debug": "^4.1.12", 42 | "@types/jest": "^29.5.14", 43 | "@types/mongodb": "^4.0.7", 44 | "@types/pg": "^8.11.0", 45 | "@types/pg-query-stream": "^3.4.0", 46 | "@types/yargs": "^17.0.32", 47 | "jest": "^29.7.0", 48 | "mongodb-memory-server": "^10.1.4", 49 | "pg": "^8.11.3", 50 | "portfinder": "^1.0.32", 51 | "testcontainers": "^10.21.0", 52 | "ts-jest": "^29.2.6", 53 | "ts-node": "^10.9.2", 54 | "wait-port": "1.0.4" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/postgres-server/tests/integration/minimal-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { GenericContainer, StartedTestContainer } from 'testcontainers'; 3 | import { PostgresServer } from '../../src/pg-server'; 4 | import { Client } from 'pg'; 5 | import debug from 'debug'; 6 | import portfinder from 'portfinder'; 7 | import waitPort from 'wait-port'; 8 | 9 | const log = debug('queryleaf:test:minimal'); 10 | 11 | // Mock QueryLeaf class for testing 12 | class MockQueryLeaf { 13 | constructor(public client: any, public dbName: string) {} 14 | 15 | execute(query: string) { 16 | log(`Mock executing query: ${query}`); 17 | if (query.includes('users')) { 18 | return [ 19 | { name: 'John', age: 30 }, 20 | { name: 'Jane', age: 25 } 21 | ]; 22 | } 23 | // Return test data 24 | if (query.trim().toLowerCase() === 'select test') { 25 | return [{ test: 'success' }]; 26 | } 27 | return []; 28 | } 29 | 30 | getDatabase() { 31 | return this.dbName; 32 | } 33 | } 34 | 35 | // Very long test timeout 36 | jest.setTimeout(120000); 37 | 38 | describe('Minimal PostgreSQL Server Integration Test', () => { 39 | let mongoContainer: StartedTestContainer; 40 | let mongoClient: MongoClient; 41 | let pgServer: PostgresServer; 42 | let pgPort: number; 43 | let pgClient: Client; 44 | 45 | beforeAll(async () => { 46 | // Start MongoDB container 47 | log('Starting MongoDB container...'); 48 | mongoContainer = await new GenericContainer('mongo:6.0') 49 | .withExposedPorts(27017) 50 | .start(); 51 | 52 | const mongoHost = mongoContainer.getHost(); 53 | const mongoPort = mongoContainer.getMappedPort(27017); 54 | const mongoUri = `mongodb://${mongoHost}:${mongoPort}`; 55 | 56 | log(`MongoDB container started at ${mongoUri}`); 57 | 58 | // Connect to MongoDB 59 | mongoClient = new MongoClient(mongoUri); 60 | await mongoClient.connect(); 61 | 62 | // Create test database and load sample data 63 | const db = mongoClient.db('minimal_test'); 64 | await db.collection('users').insertMany([ 65 | { name: 'John', age: 30 }, 66 | { name: 'Jane', age: 25 } 67 | ]); 68 | 69 | // Get free port for PostgreSQL server 70 | pgPort = await portfinder.getPortPromise({ 71 | port: 5432, 72 | stopPort: 6000 73 | }); 74 | 75 | log(`Using port ${pgPort} for PostgreSQL server`); 76 | 77 | // Create a mock QueryLeaf instance instead of using the real one 78 | const mockQueryLeaf = new MockQueryLeaf(mongoClient, 'minimal_test'); 79 | 80 | // Start PostgreSQL server with the mock 81 | log('Creating PostgreSQL server instance with mock QueryLeaf...'); 82 | // @ts-ignore - using mock class 83 | pgServer = new PostgresServer(mongoClient, 'minimal_test', { 84 | port: pgPort, 85 | host: '127.0.0.1', 86 | maxConnections: 5 87 | }); 88 | 89 | // Replace the real QueryLeaf with our mock 90 | Object.defineProperty(pgServer, 'queryLeaf', { 91 | value: mockQueryLeaf 92 | }); 93 | 94 | // Wait for server to start 95 | log(`Starting PostgreSQL server on port ${pgPort}...`); 96 | try { 97 | await pgServer.listen(pgPort, '127.0.0.1'); 98 | log('PostgreSQL server started successfully'); 99 | } catch (err) { 100 | log(`Error starting PostgreSQL server: ${err instanceof Error ? err.message : String(err)}`); 101 | throw err; 102 | } 103 | 104 | log('Waiting 2 seconds for initialization...'); 105 | await new Promise(resolve => setTimeout(resolve, 2000)); 106 | 107 | // Create PostgreSQL client 108 | pgClient = new Client({ 109 | host: '127.0.0.1', 110 | port: pgPort, 111 | database: 'minimal_test', 112 | user: 'test', 113 | password: 'test' 114 | }); 115 | 116 | await pgClient.connect(); 117 | log('PostgreSQL client connected'); 118 | }); 119 | 120 | afterAll(async () => { 121 | // Clean up resources 122 | log('Cleaning up test resources...'); 123 | 124 | if (pgClient) { 125 | try { 126 | await pgClient.end(); 127 | log('PostgreSQL client disconnected'); 128 | } catch (err) { 129 | log('Error disconnecting PostgreSQL client:', err); 130 | } 131 | } 132 | 133 | if (pgServer) { 134 | try { 135 | await pgServer.shutdown(); 136 | log('PostgreSQL server stopped'); 137 | } catch (err) { 138 | log('Error stopping PostgreSQL server:', err); 139 | } 140 | } 141 | 142 | if (mongoClient) { 143 | try { 144 | await mongoClient.close(); 145 | log('MongoDB client closed'); 146 | } catch (err) { 147 | log('Error closing MongoDB client:', err); 148 | } 149 | } 150 | 151 | if (mongoContainer) { 152 | try { 153 | await mongoContainer.stop(); 154 | log('MongoDB container stopped'); 155 | } catch (err) { 156 | log('Error stopping MongoDB container:', err); 157 | } 158 | } 159 | }); 160 | 161 | it('should execute a simple test query', async () => { 162 | const result = await pgClient.query('SELECT test'); 163 | expect(result.rows).toHaveLength(1); 164 | expect(result.rows[0]).toHaveProperty('test'); 165 | expect(result.rows[0].test).toBe('success'); 166 | }, 90000); 167 | }); -------------------------------------------------------------------------------- /packages/postgres-server/tests/integration/minimal.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { GenericContainer, StartedTestContainer } from 'testcontainers'; 3 | import { PostgresServer } from '../../src/pg-server'; 4 | import { Client } from 'pg'; 5 | import debug from 'debug'; 6 | 7 | const log = debug('queryleaf:test:minimal-integration'); 8 | 9 | // Set a very long timeout for this test 10 | jest.setTimeout(120000); 11 | 12 | // Mock QueryLeaf class for testing 13 | class MockQueryLeaf { 14 | constructor(public client: any, public dbName: string) {} 15 | 16 | execute(query: string): any[] { 17 | log(`Mock executing query: ${query}`); 18 | if (query.includes('test')) { 19 | return [{ test: 'success' }]; 20 | } 21 | return []; 22 | } 23 | 24 | getDatabase() { 25 | return this.dbName; 26 | } 27 | } 28 | 29 | describe('Minimal PostgreSQL Integration Test', () => { 30 | let mongoContainer: StartedTestContainer; 31 | let mongoClient: MongoClient; 32 | let pgServer: PostgresServer; 33 | let pgClient: Client; 34 | const TEST_PORT = 5459; // Use a different port from other tests 35 | 36 | beforeAll(async () => { 37 | // Start MongoDB container 38 | log('Starting MongoDB container...'); 39 | mongoContainer = await new GenericContainer('mongo:6.0') 40 | .withExposedPorts(27017) 41 | .start(); 42 | 43 | const mongoHost = mongoContainer.getHost(); 44 | const mongoPort = mongoContainer.getMappedPort(27017); 45 | const mongoUri = `mongodb://${mongoHost}:${mongoPort}`; 46 | 47 | log(`MongoDB container started at ${mongoUri}`); 48 | 49 | // Connect to MongoDB 50 | mongoClient = new MongoClient(mongoUri); 51 | await mongoClient.connect(); 52 | 53 | // Create mock QueryLeaf 54 | const mockQueryLeaf = new MockQueryLeaf(mongoClient, 'minimal_test'); 55 | 56 | // Create PostgreSQL server 57 | log('Creating PostgreSQL server instance...'); 58 | pgServer = new PostgresServer(mongoClient, 'minimal_test', { 59 | port: TEST_PORT, 60 | host: '127.0.0.1', 61 | maxConnections: 5 62 | }); 63 | 64 | // Replace the real QueryLeaf with our mock 65 | Object.defineProperty(pgServer, 'queryLeaf', { 66 | value: mockQueryLeaf 67 | }); 68 | 69 | // Start server 70 | log(`Starting PostgreSQL server on port ${TEST_PORT}...`); 71 | await pgServer.listen(TEST_PORT, '127.0.0.1'); 72 | log('PostgreSQL server started'); 73 | 74 | // Create PostgreSQL client 75 | pgClient = new Client({ 76 | host: '127.0.0.1', 77 | port: TEST_PORT, 78 | database: 'minimal_test', 79 | user: 'test', 80 | password: 'test' 81 | }); 82 | 83 | // Connect to server 84 | log('Connecting to PostgreSQL server...'); 85 | await pgClient.connect(); 86 | log('PostgreSQL client connected'); 87 | }); 88 | 89 | afterAll(async () => { 90 | // Clean up resources 91 | log('Cleaning up resources...'); 92 | 93 | if (pgClient) { 94 | await pgClient.end(); 95 | log('PostgreSQL client disconnected'); 96 | } 97 | 98 | if (pgServer) { 99 | await pgServer.shutdown(); 100 | log('PostgreSQL server stopped'); 101 | } 102 | 103 | if (mongoClient) { 104 | await mongoClient.close(); 105 | log('MongoDB client disconnected'); 106 | } 107 | 108 | if (mongoContainer) { 109 | await mongoContainer.stop(); 110 | log('MongoDB container stopped'); 111 | } 112 | 113 | log('All resources cleaned up'); 114 | }); 115 | 116 | it('should execute a simple test query', async () => { 117 | const result = await pgClient.query('SELECT test'); 118 | log(`Query result: ${JSON.stringify(result)}`); 119 | expect(result.rows).toHaveLength(1); 120 | expect(result.rows[0]).toHaveProperty('test'); 121 | expect(result.rows[0].test).toBe('success'); 122 | }, 90000); 123 | }); -------------------------------------------------------------------------------- /packages/postgres-server/tests/integration/protocol.test.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { PostgresServer } from '../../src/pg-server'; 3 | import { MongoClient } from 'mongodb'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | import debug from 'debug'; 6 | import portfinder from 'portfinder'; 7 | import waitPort from 'wait-port'; 8 | import { Client } from 'pg'; 9 | 10 | const log = debug('queryleaf:test:protocol'); 11 | 12 | // For a fixed test port 13 | const TEST_PORT = 5460; 14 | 15 | // Set a longer timeout for these tests 16 | jest.setTimeout(60000); // 60 seconds 17 | 18 | describe('PostgreSQL Protocol Implementation', () => { 19 | let mongoServer: MongoMemoryServer; 20 | let mongoClient: MongoClient; 21 | let pgServer: PostgresServer; 22 | const TEST_DB = 'test_protocol_db'; 23 | 24 | beforeAll(async () => { 25 | // Set up MongoDB memory server 26 | mongoServer = await MongoMemoryServer.create(); 27 | const mongoUri = mongoServer.getUri(); 28 | 29 | // Connect to MongoDB 30 | mongoClient = new MongoClient(mongoUri); 31 | await mongoClient.connect(); 32 | 33 | // Get a free port 34 | const port = await portfinder.getPortPromise({ 35 | port: TEST_PORT, 36 | stopPort: TEST_PORT + 100 37 | }); 38 | 39 | // Start PostgreSQL server 40 | pgServer = new PostgresServer(mongoClient, TEST_DB, { 41 | port, 42 | host: 'localhost', 43 | maxConnections: 5 44 | }); 45 | 46 | await pgServer.listen(port, 'localhost'); 47 | 48 | // Wait for server to be ready 49 | log(`Waiting for PostgreSQL server at localhost:${port}...`); 50 | const portResult = await waitPort({ 51 | host: 'localhost', 52 | port, 53 | timeout: 10000, 54 | output: 'silent' 55 | }); 56 | 57 | if (!portResult.open) { 58 | throw new Error(`Port ${port} is not open after waiting`); 59 | } 60 | 61 | // Additional wait to ensure server is fully initialized 62 | await new Promise(resolve => setTimeout(resolve, 500)); 63 | log('PostgreSQL server is ready'); 64 | }); 65 | 66 | afterAll(async () => { 67 | // Clean up resources 68 | if (pgServer) { 69 | await pgServer.shutdown(); 70 | } 71 | 72 | if (mongoClient) { 73 | await mongoClient.close(); 74 | } 75 | 76 | if (mongoServer) { 77 | await mongoServer.stop(); 78 | } 79 | }); 80 | 81 | it('should handle authentication and basic query execution', async () => { 82 | // Create a pg client to test our implementation 83 | const client = new Client({ 84 | host: 'localhost', 85 | port: TEST_PORT, 86 | database: TEST_DB, 87 | user: 'testuser', 88 | password: 'testpass', 89 | // Set a reasonable connection timeout 90 | connectionTimeoutMillis: 5000 91 | }); 92 | 93 | try { 94 | // Connect to the server (tests startup message and auth handling) 95 | await client.connect(); 96 | log('Client connected successfully'); 97 | 98 | // Execute test query 99 | const result = await client.query('SELECT test'); 100 | 101 | // Verify query response 102 | expect(result.rows.length).toBe(1); 103 | expect(result.rows[0].test).toBe('success'); 104 | 105 | log('Query executed successfully'); 106 | } finally { 107 | // Close connection 108 | await client.end(); 109 | log('Client disconnected'); 110 | } 111 | }); 112 | 113 | it('should handle transaction control statements', async () => { 114 | // Create a pg client to test our implementation 115 | const client = new Client({ 116 | host: 'localhost', 117 | port: TEST_PORT, 118 | database: TEST_DB, 119 | user: 'testuser', 120 | password: 'testpass', 121 | // Set a reasonable connection timeout 122 | connectionTimeoutMillis: 5000 123 | }); 124 | 125 | try { 126 | // Connect to the server 127 | await client.connect(); 128 | log('Client connected successfully'); 129 | 130 | // Begin transaction 131 | const beginResult = await client.query('BEGIN'); 132 | expect(beginResult.command).toBe('BEGIN'); 133 | 134 | // Should be in transaction block - use a valid query format for our server 135 | const inTransactionResult = await client.query('SELECT test'); 136 | 137 | // Commit transaction 138 | const commitResult = await client.query('COMMIT'); 139 | expect(commitResult.command).toBe('COMMIT'); 140 | 141 | log('Transaction control statements handled successfully'); 142 | } finally { 143 | // Close connection 144 | await client.end(); 145 | log('Client disconnected'); 146 | } 147 | }); 148 | }); 149 | 150 | -------------------------------------------------------------------------------- /packages/postgres-server/tests/unit/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import { Socket } from 'net'; 4 | 5 | // Mock the QueryLeaf import 6 | jest.mock('@queryleaf/lib', () => { 7 | return { 8 | QueryLeaf: class MockQueryLeaf { 9 | constructor(public client: any, public dbName: string) {} 10 | 11 | async execute(sql: string): Promise { 12 | if (sql === 'SELECT * FROM users') { 13 | return [ 14 | { name: 'John Doe', age: 30, email: 'john@example.com' }, 15 | { name: 'Jane Smith', age: 25, email: 'jane@example.com' } 16 | ]; 17 | } 18 | return []; 19 | } 20 | } 21 | }; 22 | }); 23 | 24 | // Create a mock Socket class 25 | class MockSocket { 26 | public data: Buffer[] = []; 27 | public writable = true; 28 | public listeners: Record void>> = { 29 | data: [], 30 | error: [], 31 | close: [], 32 | end: [], 33 | timeout: [], 34 | drain: [] 35 | }; 36 | 37 | write(data: Buffer): boolean { 38 | this.data.push(data); 39 | return true; 40 | } 41 | 42 | on(event: string, callback: (data: any) => void): this { 43 | if (!this.listeners[event]) { 44 | this.listeners[event] = []; 45 | } 46 | this.listeners[event].push(callback); 47 | return this; 48 | } 49 | 50 | emit(event: string, data?: any): boolean { 51 | if (this.listeners[event]) { 52 | for (const listener of this.listeners[event]) { 53 | listener(data); 54 | } 55 | return true; 56 | } 57 | return false; 58 | } 59 | 60 | end(): void { 61 | this.emit('close'); 62 | } 63 | 64 | setTimeout(timeout: number): this { 65 | // Mock implementation of setTimeout 66 | return this; 67 | } 68 | 69 | // Add missing socket properties for testing 70 | remoteAddress = '127.0.0.1'; 71 | remotePort = 12345; 72 | } 73 | 74 | // Manually import ProtocolHandler with relative path 75 | import { ProtocolHandler } from '../../src/protocol-handler'; 76 | import { QueryLeaf } from '@queryleaf/lib'; 77 | 78 | describe('Basic PostgreSQL Protocol Tests', () => { 79 | let mongoServer: MongoMemoryServer; 80 | let mongoClient: MongoClient; 81 | 82 | beforeAll(async () => { 83 | mongoServer = await MongoMemoryServer.create(); 84 | const mongoUri = mongoServer.getUri(); 85 | mongoClient = new MongoClient(mongoUri); 86 | await mongoClient.connect(); 87 | }); 88 | 89 | afterAll(async () => { 90 | await mongoClient.close(); 91 | await mongoServer.stop(); 92 | }); 93 | 94 | test('ProtocolHandler handles authentication', async () => { 95 | const socket = new MockSocket(); 96 | const queryLeaf = new QueryLeaf(mongoClient, 'test'); 97 | const handler = new ProtocolHandler(socket as unknown as Socket, queryLeaf); 98 | 99 | // Simulate startup message with proper length 100 | const startupBuffer = Buffer.alloc(62); 101 | // Set message length (includes the length itself) 102 | startupBuffer.writeUInt32BE(62, 0); 103 | // Protocol version (196608 = 3.0) 104 | startupBuffer.writeUInt32BE(196608, 4); 105 | // Write "user\0postgres\0database\0testdb\0\0" with proper padding 106 | Buffer.from('user\0postgres\0database\0testdb\0\0').copy(startupBuffer, 8); 107 | 108 | // Emit data event 109 | socket.emit('data', startupBuffer); 110 | 111 | // Wait for async processing 112 | await new Promise(resolve => setTimeout(resolve, 50)); 113 | 114 | // Check if authentication request was sent 115 | expect(socket.data.length).toBeGreaterThan(0); 116 | 117 | // Simulate password message 118 | const passwordBuffer = Buffer.alloc(16); 119 | passwordBuffer.write('p', 0); // Message type 120 | passwordBuffer.writeUInt32BE(12, 1); // Length 121 | Buffer.from('password\0').copy(passwordBuffer, 5); // Password 122 | 123 | // Reset data array 124 | socket.data = []; 125 | 126 | // Emit data event 127 | socket.emit('data', passwordBuffer); 128 | 129 | // Wait for async processing 130 | await new Promise(resolve => setTimeout(resolve, 50)); 131 | 132 | // Check if authentication successful messages were sent 133 | expect(socket.data.length).toBeGreaterThan(0); 134 | }); 135 | 136 | test('ProtocolHandler handles SELECT query', async () => { 137 | const socket = new MockSocket(); 138 | const queryLeaf = new QueryLeaf(mongoClient, 'test'); 139 | const handler = new ProtocolHandler(socket as unknown as Socket, queryLeaf); 140 | 141 | // Set authenticated flag manually 142 | (handler as any).authenticated = true; 143 | 144 | // Simulate query message with proper format 145 | const queryString = 'SELECT * FROM users'; 146 | const queryLength = 1 + 4 + queryString.length + 1; // message type + length field + string + null terminator 147 | const queryBuffer = Buffer.alloc(queryLength); 148 | queryBuffer.write('Q', 0); // Message type 'Q' 149 | queryBuffer.writeUInt32BE(queryLength - 1, 1); // Length (don't include the message type in length) 150 | Buffer.from(queryString + '\0').copy(queryBuffer, 5); // Query with null terminator 151 | 152 | // Reset data array 153 | socket.data = []; 154 | 155 | // Emit data event 156 | socket.emit('data', queryBuffer); 157 | 158 | // Allow async operations to complete with generous timeout 159 | await new Promise(resolve => setTimeout(resolve, 500)); 160 | 161 | // Check if response was sent 162 | expect(socket.data.length).toBeGreaterThan(0); 163 | }); 164 | }); -------------------------------------------------------------------------------- /packages/postgres-server/tests/unit/server.basic.test.ts: -------------------------------------------------------------------------------- 1 | describe('PostgreSQL Server', () => { 2 | test('Basic functionality works', () => { 3 | // Simple test to ensure tests can run 4 | expect(1 + 1).toBe(2); 5 | }); 6 | 7 | test('Protocol formatting functions', () => { 8 | // Test Buffer encoding and message formatting 9 | const message = Buffer.from('test'); 10 | const formatted = Buffer.concat([Buffer.from([84]), Buffer.alloc(4), message]); 11 | formatted.writeUInt32BE(message.length + 4, 1); 12 | 13 | // Check if the message formatting works correctly 14 | expect(formatted[0]).toBe(84); // 'T' in ASCII 15 | expect(formatted.readUInt32BE(1)).toBe(message.length + 4); 16 | expect(formatted.subarray(5).toString()).toBe('test'); 17 | }); 18 | }); -------------------------------------------------------------------------------- /packages/postgres-server/tests/unit/server.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { PostgresServer } from '../../src/pg-server'; 3 | import { Client } from 'pg'; 4 | import { MongoMemoryServer } from 'mongodb-memory-server'; 5 | 6 | // Create a test database 7 | let mongoServer: MongoMemoryServer; 8 | let mongoClient: MongoClient; 9 | let pgServer: any; 10 | 11 | const TEST_PORT = 5433; 12 | const TEST_HOST = '127.0.0.1'; 13 | const TEST_DB = 'test_db'; 14 | 15 | // Set a longer timeout for all tests in this file 16 | jest.setTimeout(120000); 17 | 18 | describe('PostgreSQL Server', () => { 19 | beforeAll(async () => { 20 | // Start MongoDB memory server 21 | mongoServer = await MongoMemoryServer.create(); 22 | const mongoUri = mongoServer.getUri(); 23 | 24 | // Connect to MongoDB 25 | mongoClient = new MongoClient(mongoUri); 26 | await mongoClient.connect(); 27 | 28 | // Create test data 29 | const db = mongoClient.db(TEST_DB); 30 | await db.collection('users').insertMany([ 31 | { name: 'Alice', age: 30 }, 32 | { name: 'Bob', age: 25 }, 33 | { name: 'Charlie', age: 35 }, 34 | ]); 35 | 36 | // Start PostgreSQL server 37 | pgServer = new PostgresServer(mongoClient, TEST_DB, { 38 | port: TEST_PORT, 39 | host: TEST_HOST, 40 | maxConnections: 10, 41 | }); 42 | 43 | // Explicitly start the server 44 | await pgServer.listen(TEST_PORT, TEST_HOST); 45 | 46 | // Wait a bit for the server to fully initialize 47 | await new Promise(resolve => setTimeout(resolve, 2000)); 48 | }, 30000); 49 | 50 | afterAll(async () => { 51 | // Clean up 52 | await mongoClient.close(); 53 | await mongoServer.stop(); 54 | 55 | // Shutdown server 56 | if (pgServer) { 57 | await pgServer.shutdown(); 58 | } 59 | }); 60 | 61 | it('should connect to the server', async () => { 62 | const client = new Client({ 63 | host: TEST_HOST, 64 | port: TEST_PORT, 65 | database: TEST_DB, 66 | user: 'test', 67 | password: 'test', 68 | }); 69 | 70 | await client.connect(); 71 | await client.end(); 72 | }, 60000); 73 | 74 | it('should execute a simple query', async () => { 75 | const client = new Client({ 76 | host: TEST_HOST, 77 | port: TEST_PORT, 78 | database: TEST_DB, 79 | user: 'test', 80 | password: 'test', 81 | }); 82 | 83 | await client.connect(); 84 | 85 | const result = await client.query('SELECT * FROM users'); 86 | expect(result.rows).toHaveLength(3); 87 | expect(result.rows[0]).toHaveProperty('name'); 88 | expect(result.rows[0]).toHaveProperty('age'); 89 | 90 | await client.end(); 91 | }, 60000); 92 | }); -------------------------------------------------------------------------------- /packages/postgres-server/tests/utils/test-setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoTestContainer, loadFixtures, testUsers, testProducts, testOrders, testDocuments } from './mongo-container'; 2 | import { PostgresServer } from '../../src/pg-server'; 3 | import { QueryLeaf } from '@queryleaf/lib'; 4 | import { Db } from 'mongodb'; 5 | import debug from 'debug'; 6 | import portfinder from 'portfinder'; 7 | import waitPort from 'wait-port'; 8 | 9 | const log = debug('queryleaf:test:pg-integration'); 10 | 11 | /** 12 | * Base test setup for PostgreSQL server integration tests 13 | */ 14 | export class PgIntegrationTestSetup { 15 | public mongoContainer: MongoTestContainer; 16 | public TEST_DB = 'queryleaf_pg_test'; 17 | public pgServer: PostgresServer | null = null; 18 | public pgPort: number = 0; 19 | public pgHost: string = '127.0.0.1'; 20 | 21 | constructor() { 22 | this.mongoContainer = new MongoTestContainer(); 23 | } 24 | 25 | /** 26 | * Initialize the test environment 27 | */ 28 | async init(): Promise { 29 | // Start MongoDB container 30 | await this.mongoContainer.start(); 31 | const db = this.mongoContainer.getDatabase(this.TEST_DB); 32 | await loadFixtures(db); 33 | 34 | // Use TEST_PORT from integration test if available in the scope 35 | this.pgPort = 5458; // Fixed test port 36 | 37 | log(`Using port ${this.pgPort} for PostgreSQL server`); 38 | 39 | // Start PostgreSQL server 40 | const mongoClient = this.mongoContainer.getClient(); 41 | const queryLeaf = new QueryLeaf(mongoClient, this.TEST_DB); 42 | 43 | this.pgServer = new PostgresServer(mongoClient, this.TEST_DB, { 44 | port: this.pgPort, 45 | host: this.pgHost, 46 | maxConnections: 10 47 | }); 48 | 49 | // Explicitly call listen since we modified the constructor to not auto-listen 50 | await this.pgServer.listen(this.pgPort, this.pgHost); 51 | 52 | // Wait for the server to be ready 53 | log(`Waiting for PostgreSQL server at ${this.pgHost}:${this.pgPort}...`); 54 | 55 | try { 56 | const portResult = await waitPort({ 57 | host: this.pgHost, 58 | port: this.pgPort, 59 | timeout: 20000, 60 | output: 'silent' 61 | }); 62 | 63 | log(`Port check result: ${JSON.stringify(portResult)}`); 64 | 65 | if (!portResult.open) { 66 | throw new Error(`Port ${this.pgPort} is not open after waiting`); 67 | } 68 | 69 | // Give the server a moment to fully initialize 70 | log('Port is open, waiting 2 seconds for full initialization...'); 71 | await new Promise(resolve => setTimeout(resolve, 2000)); 72 | log('PostgreSQL server is ready'); 73 | } catch (error) { 74 | log(`Failed waiting for port: ${error instanceof Error ? error.message : String(error)}`); 75 | throw error; 76 | } 77 | } 78 | 79 | /** 80 | * Clean up the test environment 81 | */ 82 | async cleanup(): Promise { 83 | try { 84 | // Stop the PostgreSQL server 85 | if (this.pgServer) { 86 | log('Stopping PostgreSQL server...'); 87 | await this.pgServer.shutdown(); 88 | this.pgServer = null; 89 | } 90 | 91 | // Stop the MongoDB container 92 | log('Stopping MongoDB container...'); 93 | await this.mongoContainer.stop(); 94 | } catch (err) { 95 | log('Error during cleanup:', err); 96 | } 97 | } 98 | 99 | /** 100 | * Get PostgreSQL connection info 101 | */ 102 | getPgConnectionInfo() { 103 | return { 104 | host: this.pgHost, 105 | port: this.pgPort, 106 | database: this.TEST_DB, 107 | user: 'test', // Any username works with our simple implementation 108 | password: 'test', // Any password works with our simple implementation 109 | }; 110 | } 111 | 112 | /** 113 | * Get a database instance 114 | */ 115 | getDb(): Db { 116 | return this.mongoContainer.getDatabase(this.TEST_DB); 117 | } 118 | } 119 | 120 | /** 121 | * Create a shared test setup instance 122 | */ 123 | export const testSetup = new PgIntegrationTestSetup(); 124 | 125 | /** 126 | * Export fixture data for tests 127 | */ 128 | export { testUsers, testProducts, testOrders, testDocuments }; 129 | 130 | /** 131 | * Create debug logger for tests 132 | */ 133 | export const createLogger = (namespace: string) => debug(`queryleaf:test:${namespace}`); -------------------------------------------------------------------------------- /packages/postgres-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 9 | } -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 |

2 | QueryLeaf Logo 3 |

4 | 5 |

@queryleaf/server

6 | 7 |

SQL to MongoDB query translator - Web Server

8 | 9 | ## Overview 10 | 11 | `@queryleaf/server` provides a REST API server for QueryLeaf, allowing you to execute SQL queries against MongoDB through HTTP requests. It's built on Express.js and leverages the core `@queryleaf/lib` package to parse SQL, transform it into MongoDB commands, and execute those commands. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | # Global installation 17 | npm install -g @queryleaf/server 18 | # or 19 | yarn global add @queryleaf/server 20 | 21 | # Local installation 22 | npm install @queryleaf/server 23 | # or 24 | yarn add @queryleaf/server 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Running the Server 30 | 31 | ```bash 32 | # Start the server with default settings 33 | queryleaf-server 34 | 35 | # With specific MongoDB URI and port 36 | queryleaf-server --uri mongodb://localhost:27017 --port 3000 37 | 38 | # With authentication 39 | queryleaf-server --uri mongodb://user:pass@localhost:27017 40 | 41 | # Help 42 | queryleaf-server --help 43 | ``` 44 | 45 | ### API Endpoints 46 | 47 | #### Execute SQL Query 48 | 49 | ``` 50 | POST /api/query 51 | ``` 52 | 53 | Request body: 54 | ```json 55 | { 56 | "database": "mydb", 57 | "query": "SELECT * FROM users WHERE age > 21" 58 | } 59 | ``` 60 | 61 | Response: 62 | ```json 63 | { 64 | "results": [ 65 | { "name": "Alice", "age": 25, "email": "alice@example.com" }, 66 | { "name": "Bob", "age": 30, "email": "bob@example.com" } 67 | ], 68 | "count": 2, 69 | "message": "Query executed successfully" 70 | } 71 | ``` 72 | 73 | #### Get Server Information 74 | 75 | ``` 76 | GET /api/info 77 | ``` 78 | 79 | Response: 80 | ```json 81 | { 82 | "version": "0.1.0", 83 | "name": "QueryLeaf Server", 84 | "mongodb": { 85 | "connected": true, 86 | "version": "6.0.0" 87 | } 88 | } 89 | ``` 90 | 91 | ## Configuration 92 | 93 | You can configure the server using command-line arguments or environment variables: 94 | 95 | | Argument | Environment Variable | Description | 96 | |----------------|----------------------|----------------------------------| 97 | | `--uri` | `MONGODB_URI` | MongoDB connection URI | 98 | | `--port` | `PORT` | Server port (default: 3000) | 99 | | `--host` | `HOST` | Server host (default: 0.0.0.0) | 100 | | `--cors` | `ENABLE_CORS` | Enable CORS (default: true) | 101 | | `--rate-limit` | `RATE_LIMIT` | Rate limit (reqs/min, default: 60) | 102 | 103 | ## Security 104 | 105 | The server includes security features: 106 | - Rate limiting to prevent abuse 107 | - Helmet.js for HTTP security headers 108 | - Express security best practices 109 | - Optional authentication 110 | 111 | ## Links 112 | 113 | - [Website](https://queryleaf.com) 114 | - [Documentation](https://queryleaf.com/docs) 115 | - [GitHub Repository](https://github.com/beekeeper-studio/queryleaf) 116 | 117 | ## License 118 | 119 | QueryLeaf is dual-licensed: 120 | 121 | - [AGPL-3.0](https://github.com/beekeeper-studio/queryleaf/blob/main/LICENSE.md) for open source use 122 | - [Commercial license](https://github.com/beekeeper-studio/queryleaf/blob/main/COMMERCIAL_LICENSE.md) for commercial use 123 | 124 | For commercial licensing options, visit [queryleaf.com](https://queryleaf.com). -------------------------------------------------------------------------------- /packages/server/bin/queryleaf-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/server.js'); 4 | -------------------------------------------------------------------------------- /packages/server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/tests'], 5 | transform: { 6 | '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }] 7 | }, 8 | testMatch: ['**/tests/**/*.test.ts', '**/tests/**/*.test.js'], 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | openHandlesTimeout: 30000, 11 | detectOpenHandles: true, 12 | }; -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@queryleaf/server", 3 | "version": "0.1.0", 4 | "description": "SQL to MongoDB query translator - Web Server", 5 | "main": "dist/server.js", 6 | "bin": { 7 | "queryleaf-server": "./bin/queryleaf-server" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf dist", 11 | "build": "yarn clean && tsc", 12 | "typecheck": "tsc --noEmit", 13 | "start": "ts-node src/server.ts", 14 | "test": "yarn test:unit && yarn test:integration", 15 | "test:unit": "jest tests/unit", 16 | "test:integration": "jest tests/integration --runInBand", 17 | "lint": "eslint --ext .ts src", 18 | "lint:fix": "eslint --ext .ts --fix src", 19 | "lockversion": "sed -i -E \"s|(\\\"@queryleaf/lib\\\"\\s*:\\s*\\\")[^\\\"]+(\\\")|\\1$VERSION\\2|\" package.json" 20 | }, 21 | "keywords": [ 22 | "sql", 23 | "mongodb", 24 | "compiler", 25 | "query", 26 | "server", 27 | "api" 28 | ], 29 | "author": "", 30 | "license": "AGPL-3.0", 31 | "dependencies": { 32 | "@queryleaf/lib": "0.1.0", 33 | "body-parser": "^1.20.2", 34 | "cors": "^2.8.5", 35 | "express": "^4.18.2", 36 | "express-rate-limit": "^7.1.5", 37 | "helmet": "^7.1.0", 38 | "mongodb": "^6.14.2", 39 | "morgan": "^1.10.0", 40 | "swagger-ui-express": "^5.0.0" 41 | }, 42 | "devDependencies": { 43 | "@types/cors": "^2.8.17", 44 | "@types/express": "^4.17.21", 45 | "@types/jest": "^29.5.14", 46 | "@types/mongodb": "^4.0.7", 47 | "@types/morgan": "^1.9.9", 48 | "@types/swagger-ui-express": "^4.1.6", 49 | "jest": "^29.7.0", 50 | "node-fetch": "2", 51 | "ts-jest": "^29.2.6", 52 | "ts-node": "^10.9.2" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/server/tests/integration/server.test.ts: -------------------------------------------------------------------------------- 1 | 2 | describe('Server tests', () => { 3 | // Very basic test for server functionality 4 | it('Server imports should compile correctly', () => { 5 | // no-op tests right now 6 | expect(true).toBe(true); 7 | }); 8 | 9 | }); -------------------------------------------------------------------------------- /packages/server/tests/unit/server.test.ts: -------------------------------------------------------------------------------- 1 | 2 | describe('Server tests', () => { 3 | // Very basic test for server functionality 4 | it('Server imports should compile correctly', () => { 5 | // no-op tests right now 6 | expect(true).toBe(true); 7 | }); 8 | 9 | }); -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 9 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.4.3 2 | mkdocs-material>=9.1.0 3 | pymdown-extensions>=10.0 4 | mkdocs-glightbox>=0.3.0 5 | mkdocs-minify-plugin>=0.6.0 -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "declaration": true, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | } 16 | }, 17 | "exclude": ["node_modules", "dist"] 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "files": [] 4 | } --------------------------------------------------------------------------------