├── .cfformat.json ├── .github └── workflows │ ├── cron.yml │ ├── pr.yml │ ├── prerelease.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── ModuleConfig.cfc ├── README.md ├── box.json ├── models ├── Grammars │ ├── AutoDiscover.cfc │ ├── BaseGrammar.cfc │ ├── DerbyGrammar.cfc │ ├── MySQLGrammar.cfc │ ├── OracleGrammar.cfc │ ├── PostgresGrammar.cfc │ ├── SQLiteGrammar.cfc │ └── SqlServerGrammar.cfc ├── Query │ ├── Expression.cfc │ ├── JoinClause.cfc │ ├── QueryBuilder.cfc │ └── QueryUtils.cfc ├── SQLCommenter │ ├── ColdBoxSQLCommenter.cfc │ ├── Commenters │ │ ├── BindingsCommenter.cfc │ │ ├── DBInfoCommenter.cfc │ │ ├── FrameworkCommenter.cfc │ │ ├── ICommenter.cfc │ │ └── RouteInfoCommenter.cfc │ ├── NullSQLCommenter.cfc │ └── SQLCommenter.cfc └── Schema │ ├── Blueprint.cfc │ ├── Column.cfc │ ├── SchemaBuilder.cfc │ ├── SchemaCommand.cfc │ └── TableIndex.cfc ├── server-adobe@2021.json ├── server-adobe@2023.json ├── server-adobe@2025.json ├── server-adobe@be.json ├── server-boxlang-cfml@1.json ├── server-boxlang@1.json ├── server-boxlang@be.json ├── server-lucee@5.json ├── server-lucee@6.json ├── server-lucee@be.json └── tests ├── Application.cfc ├── index.cfm ├── profiling ├── Application.cfc ├── resources │ ├── Profiler.cfc │ └── coolblog.sql ├── runner-profiler.cfm └── specs │ └── qbProfile.cfc ├── resources ├── AbstractQueryBuilderSpec.cfc └── AbstractSchemaBuilderSpec.cfc ├── runner.cfm └── specs ├── Query ├── Abstract │ ├── BuilderAliasSpec.cfc │ ├── BuilderColumnCallbackSpec.cfc │ ├── BuilderGetSpec.cfc │ ├── BuilderJoinSpec.cfc │ ├── BuilderSelectSpec.cfc │ ├── BuilderWhereSpec.cfc │ ├── ControlFlowSpec.cfc │ ├── JoinClauseSpec.cfc │ ├── PaginationSpec.cfc │ ├── PretendSpec.cfc │ ├── QueryDebuggingSpec.cfc │ ├── QueryDefaultsSpec.cfc │ ├── QueryExecutionSpec.cfc │ ├── QueryLogSpec.cfc │ └── QueryUtilsSpec.cfc ├── DerbyQueryBuilderSpec.cfc ├── MySQLQueryBuilderSpec.cfc ├── OracleQueryBuilderSpec.cfc ├── PostgresQueryBuilderSpec.cfc ├── SQLiteQueryBuilderSpec.cfc └── SqlServerQueryBuilderSpec.cfc ├── SQLCommenterSpec.cfc └── Schema ├── DerbySchemaBuilderSpec.cfc ├── MySQLSchemaBuilderSpec.cfc ├── OracleSchemaBuilderSpec.cfc ├── PostgresSchemaBuilderSpec.cfc ├── SQLiteSchemaBuilderSpec.cfc └── SqlServerSchemaBuilderSpec.cfc /.cfformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "alignment.consecutive.assignments":false, 3 | "alignment.consecutive.params":false, 4 | "alignment.consecutive.properties":false, 5 | "array.empty_padding":false, 6 | "array.multiline.element_count":4, 7 | "array.multiline.leading_comma":false, 8 | "array.multiline.leading_comma.padding":true, 9 | "array.multiline.min_length":40, 10 | "array.padding":true, 11 | "binary_operators.padding":true, 12 | "brackets.padding":true, 13 | "comment.asterisks":"align", 14 | "for_loop_semicolons.padding":true, 15 | "function_anonymous.empty_padding":false, 16 | "function_anonymous.group_to_block_spacing":"spaced", 17 | "function_anonymous.multiline.element_count":4, 18 | "function_anonymous.multiline.leading_comma":false, 19 | "function_anonymous.multiline.leading_comma.padding":true, 20 | "function_anonymous.multiline.min_length":40, 21 | "function_anonymous.padding":true, 22 | "function_call.casing.builtin":"cfdocs", 23 | "function_call.casing.userdefined":"camel", 24 | "function_call.empty_padding":false, 25 | "function_call.multiline.element_count":4, 26 | "function_call.multiline.leading_comma":false, 27 | "function_call.multiline.leading_comma.padding":true, 28 | "function_call.multiline.min_length":40, 29 | "function_call.padding":true, 30 | "function_declaration.empty_padding":false, 31 | "function_declaration.group_to_block_spacing":"spaced", 32 | "function_declaration.multiline.element_count":4, 33 | "function_declaration.multiline.leading_comma":false, 34 | "function_declaration.multiline.leading_comma.padding":true, 35 | "function_declaration.multiline.min_length":40, 36 | "function_declaration.padding":true, 37 | "indent_size":4, 38 | "keywords.block_to_keyword_spacing":"spaced", 39 | "keywords.empty_group_spacing":false, 40 | "keywords.group_to_block_spacing":"spaced", 41 | "keywords.padding_inside_group":true, 42 | "keywords.spacing_to_block":"spaced", 43 | "keywords.spacing_to_group":true, 44 | "max_columns":120, 45 | "method_call.chain.multiline":3, 46 | "newline":"\n", 47 | "parentheses.padding":true, 48 | "strings.attributes.quote":"double", 49 | "strings.quote":"double", 50 | "struct.empty_padding":false, 51 | "struct.multiline.element_count":4, 52 | "struct.multiline.leading_comma":false, 53 | "struct.multiline.leading_comma.padding":true, 54 | "struct.multiline.min_length":40, 55 | "struct.padding":true, 56 | "struct.separator":": ", 57 | "tab_indent":false 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0 * * 1 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | name: Tests 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang@1", "boxlang-cfml@1"] 15 | experimental: [ false ] 16 | fullNull: ["true", "false"] 17 | include: 18 | - cfengine: "lucee@be" 19 | experimental: true 20 | - cfengine: "adobe@be" 21 | experimental: true 22 | - cfengine: "boxlang@be" 23 | experimental: true 24 | steps: 25 | - name: Checkout Repository 26 | uses: actions/checkout@v3.2.0 27 | 28 | - name: Setup Java JDK 29 | uses: actions/setup-java@v3.9.0 30 | with: 31 | distribution: 'zulu' 32 | java-version: 21 33 | 34 | - name: Setup CommandBox CLI 35 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 36 | 37 | - name: Install CommandBox-BoxLang 38 | run: box install commandbox-boxlang 39 | 40 | - name: Install dependencies 41 | run: | 42 | box install 43 | 44 | - name: Start server 45 | run: | 46 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 47 | curl http://127.0.0.1:60299 48 | 49 | - name: Run TestBox Tests 50 | env: 51 | FULL_NULL: ${{matrix.fullNull}} 52 | continue-on-error: ${{ matrix.experimental }} 53 | run: box testbox run -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PRs and Branches 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "main" 7 | - "master" 8 | - "development" 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | - development 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | name: Tests 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang@1", "boxlang-cfml@1"] 23 | experimental: [ false ] 24 | fullNull: ["true", "false"] 25 | include: 26 | - cfengine: "lucee@be" 27 | experimental: true 28 | - cfengine: "adobe@be" 29 | experimental: true 30 | - cfengine: "boxlang@be" 31 | experimental: true 32 | steps: 33 | - name: Checkout Repository 34 | uses: actions/checkout@v3.2.0 35 | 36 | - name: Setup Java JDK 37 | uses: actions/setup-java@v3.9.0 38 | with: 39 | distribution: 'zulu' 40 | java-version: 21 41 | 42 | - name: Setup CommandBox CLI 43 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 44 | 45 | - name: Install CommandBox-BoxLang 46 | run: box install commandbox-boxlang 47 | 48 | - name: Install dependencies 49 | run: | 50 | box install 51 | 52 | - name: Start server 53 | run: | 54 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 55 | curl http://127.0.0.1:60299 56 | 57 | - name: Run TestBox Tests 58 | env: 59 | FULL_NULL: ${{matrix.fullNull}} 60 | continue-on-error: ${{ matrix.experimental }} 61 | run: box testbox run 62 | 63 | format: 64 | runs-on: ubuntu-latest 65 | name: Format 66 | steps: 67 | - name: Checkout Repository 68 | uses: actions/checkout@v3.2.0 69 | 70 | - name: Setup Java JDK 71 | uses: actions/setup-java@v3.9.0 72 | with: 73 | distribution: 'zulu' 74 | java-version: 11 75 | 76 | - name: Setup CommandBox CLI 77 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 78 | 79 | - name: Install CFFormat 80 | run: box install commandbox-cfformat 81 | 82 | - name: Run CFFormat 83 | run: box run-script format 84 | 85 | - name: Commit Format Changes 86 | uses: stefanzweifel/git-auto-commit-action@v4 87 | with: 88 | commit_message: Apply cfformat changes -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Prerelease 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | 8 | jobs: 9 | tests: 10 | name: Tests 11 | if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang@1", "boxlang-cfml@1"] 17 | experimental: [ false ] 18 | fullNull: ["true", "false"] 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3.2.0 22 | 23 | - name: Setup Java JDK 24 | uses: actions/setup-java@v3.9.0 25 | with: 26 | distribution: 'zulu' 27 | java-version: 21 28 | 29 | - name: Setup CommandBox CLI 30 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 31 | 32 | - name: Install CommandBox-BoxLang 33 | run: box install commandbox-boxlang 34 | 35 | - name: Install dependencies 36 | run: | 37 | box install 38 | 39 | - name: Start server 40 | run: | 41 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 42 | curl http://127.0.0.1:60299 43 | 44 | - name: Run TestBox Tests 45 | env: 46 | FULL_NULL: ${{matrix.fullNull}} 47 | continue-on-error: ${{ matrix.experimental }} 48 | run: box testbox run 49 | 50 | # release: 51 | # name: Semantic Release 52 | # if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 53 | # needs: tests 54 | # runs-on: ubuntu-latest 55 | # env: 56 | # GA_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 57 | # steps: 58 | # - name: Checkout Repository 59 | # uses: actions/checkout@v3.2.0 60 | # with: 61 | # fetch-depth: 0 62 | 63 | # - name: Setup Java JDK 64 | # uses: actions/setup-java@v3.9.0 65 | # with: 66 | # distribution: 'zulu' 67 | # java-version: 11 68 | 69 | # - name: Set Up CommandBox 70 | # uses: elpete/setup-commandbox@v1.0.0 71 | 72 | # - name: Install and Configure Semantic Release 73 | # run: | 74 | # box install commandbox-semantic-release 75 | # box config set endpoints.forgebox.APIToken=${{ secrets.FORGEBOX_TOKEN }} 76 | # box config set modules.commandbox-semantic-release.plugins='{ "VerifyConditions": "GitHubActionsConditionsVerifier@commandbox-semantic-release", "FetchLastRelease": "ForgeBoxReleaseFetcher@commandbox-semantic-release", "RetrieveCommits": "JGitCommitsRetriever@commandbox-semantic-release", "ParseCommit": "ConventionalChangelogParser@commandbox-semantic-release", "FilterCommits": "DefaultCommitFilterer@commandbox-semantic-release", "AnalyzeCommits": "DefaultCommitAnalyzer@commandbox-semantic-release", "VerifyRelease": "NullReleaseVerifier@commandbox-semantic-release", "GenerateNotes": "GitHubMarkdownNotesGenerator@commandbox-semantic-release", "UpdateChangelog": "FileAppendChangelogUpdater@commandbox-semantic-release", "CommitArtifacts": "NullArtifactsCommitter@commandbox-semantic-release", "PublishRelease": "ForgeBoxReleasePublisher@commandbox-semantic-release", "PublicizeRelease": "GitHubReleasePublicizer@commandbox-semantic-release" }' 77 | 78 | # - name: Run Semantic Release 79 | # env: 80 | # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | # run: box semantic-release --prerelease -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | tests: 11 | name: Tests 12 | if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang@1", "boxlang-cfml@1"] 18 | experimental: [ false ] 19 | fullNull: ["true", "false"] 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v3.2.0 23 | 24 | - name: Setup Java JDK 25 | uses: actions/setup-java@v3.9.0 26 | with: 27 | distribution: 'zulu' 28 | java-version: 11 29 | 30 | - name: Setup CommandBox CLI 31 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 32 | 33 | - name: Install CommandBox-BoxLang 34 | run: box install commandbox-boxlang 35 | 36 | - name: Install dependencies 37 | run: | 38 | box install 39 | 40 | - name: Start server 41 | run: | 42 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 43 | curl http://127.0.0.1:60299 44 | 45 | - name: Run TestBox Tests 46 | env: 47 | FULL_NULL: ${{matrix.fullNull}} 48 | continue-on-error: ${{ matrix.experimental }} 49 | run: box testbox run 50 | 51 | release: 52 | name: Semantic Release 53 | if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 54 | needs: tests 55 | runs-on: ubuntu-latest 56 | env: 57 | GA_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 58 | steps: 59 | - name: Checkout Repository 60 | uses: actions/checkout@v3.2.0 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Setup Java JDK 65 | uses: actions/setup-java@v3.9.0 66 | with: 67 | distribution: 'zulu' 68 | java-version: 21 69 | 70 | - name: Setup CommandBox CLI 71 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 72 | 73 | - name: Install and Configure Semantic Release 74 | run: | 75 | box install commandbox-semantic-release@^3.0.0 76 | box config set endpoints.forgebox.APIToken=${{ secrets.FORGEBOX_TOKEN }} 77 | box config set modules.commandbox-semantic-release.targetBranch=main 78 | box config set modules.commandbox-semantic-release.plugins='{ "VerifyConditions": "GitHubActionsConditionsVerifier@commandbox-semantic-release", "FetchLastRelease": "ForgeBoxReleaseFetcher@commandbox-semantic-release", "RetrieveCommits": "JGitCommitsRetriever@commandbox-semantic-release", "ParseCommit": "ConventionalChangelogParser@commandbox-semantic-release", "FilterCommits": "DefaultCommitFilterer@commandbox-semantic-release", "AnalyzeCommits": "DefaultCommitAnalyzer@commandbox-semantic-release", "VerifyRelease": "NullReleaseVerifier@commandbox-semantic-release", "GenerateNotes": "GitHubMarkdownNotesGenerator@commandbox-semantic-release", "UpdateChangelog": "FileAppendChangelogUpdater@commandbox-semantic-release", "CommitArtifacts": "GitHubArtifactsCommitter@commandbox-semantic-release", "PublishRelease": "ForgeBoxReleasePublisher@commandbox-semantic-release", "PublicizeRelease": "GitHubReleasePublicizer@commandbox-semantic-release" }' 79 | 80 | - name: Run Semantic Release 81 | env: 82 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | run: box semantic-release 84 | 85 | - name: Generate API Docs 86 | run: | 87 | box install commandbox-docbox 88 | box run-script generateAPIDocs 89 | 90 | - name: Get Current Version 91 | id: current_version 92 | run: echo "version=`cat box.json | jq '.version' -r`" >> $GITHUB_OUTPUT 93 | 94 | - name: Upload API Docs to S3 95 | uses: jakejarvis/s3-sync-action@master 96 | with: 97 | args: --acl public-read 98 | env: 99 | AWS_S3_BUCKET: "apidocs.ortussolutions.com" 100 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} 101 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_SECRET }} 102 | SOURCE_DIR: ".tmp/apidocs" 103 | DEST_DIR: "${{ github.repository }}/${{ steps.current_version.outputs.version }}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /testbox/ 2 | /modules/ 3 | tests/results 4 | Engine.war 5 | *.sublime-workspace 6 | .tmp 7 | .engine/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eric Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.title = "qb"; 4 | this.author = "Eric Peterson"; 5 | this.webURL = "https://github.com/coldbox-modules/qb"; 6 | this.description = "A query builder for the rest of us"; 7 | this.cfmapping = "qb"; 8 | 9 | function configure() { 10 | settings = { 11 | "defaultGrammar": "AutoDiscover@qb", 12 | "defaultReturnFormat": "array", 13 | "preventDuplicateJoins": false, 14 | "convertEmptyStringsToNull": true, 15 | "numericSQLType": "NUMERIC", 16 | "integerSQLType": "INTEGER", 17 | "decimalSQLType": "DECIMAL", 18 | "defaultOptions": {}, 19 | "sqlCommenter": { 20 | "enabled": false, 21 | "commenters": [ 22 | { "class": "FrameworkCommenter@qb", "properties": {} }, 23 | { "class": "RouteInfoCommenter@qb", "properties": {} }, 24 | { "class": "DBInfoCommenter@qb", "properties": {} } 25 | ] 26 | }, 27 | "shouldMaxRowsOverrideToAll": function( maxRows ) { 28 | return maxRows <= 0; 29 | } 30 | }; 31 | 32 | interceptorSettings = { "customInterceptionPoints": "preQBExecute,postQBExecute" }; 33 | } 34 | 35 | function onLoad() { 36 | // Fill out the sqlCommenter commenters array in case users 37 | // forget to define it when overriding in their `config/ColdBox.cfc` 38 | if ( !settings.sqlCommenter.keyExists( "commenters" ) ) { 39 | param settings.sqlCommenter.commenters = [ 40 | { "class": "FrameworkCommenter@qb", "properties": {} }, 41 | { "class": "RouteInfoCommenter@qb", "properties": {} }, 42 | { "class": "DBInfoCommenter@qb", "properties": {} } 43 | ]; 44 | } 45 | 46 | binder 47 | .map( alias = "QueryUtils@qb", force = true ) 48 | .to( "qb.models.Query.QueryUtils" ) 49 | .initArg( name = "convertEmptyStringsToNull", value = settings.convertEmptyStringsToNull ) 50 | .initArg( name = "numericSQLType", value = settings.numericSQLType ) 51 | .initArg( name = "integerSQLType", value = settings.integerSQLType ) 52 | .initArg( name = "decimalSQLType", value = settings.decimalSQLType ); 53 | 54 | binder 55 | .map( alias = "QueryBuilder@qb", force = true ) 56 | .to( "qb.models.Query.QueryBuilder" ) 57 | .initArg( name = "grammar", ref = settings.defaultGrammar ) 58 | .initArg( name = "utils", ref = "QueryUtils@qb" ) 59 | .initArg( name = "preventDuplicateJoins", value = settings.preventDuplicateJoins ) 60 | .initArg( name = "returnFormat", value = settings.defaultReturnFormat ) 61 | .initArg( name = "defaultOptions", value = settings.defaultOptions ) 62 | .initArg( name = "sqlCommenter", ref = "ColdBoxSQLCommenter@qb" ) 63 | .initArg( name = "shouldMaxRowsOverrideToAll", value = settings.shouldMaxRowsOverrideToAll ); 64 | 65 | binder 66 | .map( alias = "SchemaBuilder@qb", force = true ) 67 | .to( "qb.models.Schema.SchemaBuilder" ) 68 | .initArg( name = "grammar", ref = settings.defaultGrammar ); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qb 2 | [![Build Status](https://img.shields.io/travis/coldbox-modules/qb/master.svg?style=flat-square)](https://travis-ci.org/coldbox-modules/qb) 3 | 4 | ## Introduction 5 | 6 | qb is a fluent query builder for CFML. It is **heavily** inspired by [Eloquent](https://laravel.com/docs/5.3/eloquent) from [Laravel](https://laravel.com/). 7 | 8 | Using qb, you can: 9 | 10 | + Quickly scaffold simple queries 11 | + Make complex, out-of-order queries possible 12 | + Abstract away differences between database engines 13 | 14 | ## Requirements 15 | 16 | + Adobe ColdFusion 2018+ 17 | + Lucee 5+ 18 | 19 | ## Installation 20 | 21 | Installation is easy through [CommandBox](https://www.ortussolutions.com/products/commandbox) and [ForgeBox](https://www.coldbox.org/forgebox). Simply type `box install qb` to get started. 22 | 23 | ## Code Samples 24 | 25 | Compare these two examples: 26 | 27 | ```cfc 28 | // Plain old CFML 29 | q = queryExecute("SELECT * FROM users"); 30 | 31 | // qb 32 | query = wirebox.getInstance('QueryBuilder@qb'); 33 | q = query.from('users').get(); 34 | ``` 35 | 36 | The differences become even more stark when we introduce more complexity: 37 | 38 | ```cfc 39 | // Plain old CFML 40 | q = queryExecute( 41 | "SELECT * FROM posts WHERE published_at IS NOT NULL AND author_id IN ?", 42 | [ { value = '5,10,27', cfsqltype = 'NUMERIC', list = true } ] 43 | ); 44 | 45 | // qb 46 | query = wirebox.getInstance('QueryBuilder@qb'); 47 | q = query.from('posts') 48 | .whereNotNull('published_at') 49 | .whereIn('author_id', [5, 10, 27]) 50 | .get(); 51 | ``` 52 | 53 | With Quick you can easily handle setting order by statements before the columns you want or join statements after a where clause: 54 | 55 | ```cfc 56 | query = wirebox.getInstance('QueryBuilder@qb'); 57 | q = query.from('posts') 58 | .orderBy('published_at') 59 | .select('post_id', 'author_id', 'title', 'body') 60 | .whereLike('author', 'Ja%') 61 | .join('authors', 'authors.id', '=', 'posts.author_id') 62 | .get(); 63 | 64 | // Becomes 65 | 66 | q = queryExecute( 67 | "SELECT post_id, author_id, title, body FROM posts INNER JOIN authors ON authors.id = posts.author_id WHERE author LIKE ? ORDER BY published_at", 68 | [ { value = 'Ja%', cfsqltype = 'VARCHAR', list = false, null = false } ] 69 | ); 70 | ``` 71 | 72 | qb enables you to explore new ways of organizing your code by letting you pass around a query builder object that will compile down to the right SQL without you having to keep track of the order, whitespace, or other SQL gotchas! 73 | 74 | Here's a gist with an example of the powerful models you can create with this! 75 | https://gist.github.com/elpete/80d641b98025f16059f6476561d88202 76 | 77 | ## SQLite Datasource Setup 78 | 79 | To use the SQLite grammar for qb you will need to setup a datasource that connects to a SQLite database. 80 | 81 | ### Install the SQLite JDBC driver 82 | 83 | 1. Download the [latest release](https://github.com/xerial/sqlite-jdbc/releases) of the SQLite JDBC Driver i.e. https://github.com/xerial/sqlite-jdbc/releases/download/3.40.0.0/sqlite-jdbc-3.40.0.0.jar 84 | 2. Drop it in the `/lib` directory 85 | 3. Configure the application to load the library by adding this line in your `Application.cfc` file. 86 | 87 | ``` 88 | this.javaSettings = { loadPaths : [ ".\lib" ] }; 89 | ``` 90 | 4. Restart the server 91 | 92 | ### Configure the Datasource 93 | 94 | You can configure your datasource for Lucee or Adobe Coldfusion using the steps below. You can also use [cfconfig](https://cfconfig.ortusbooks.com/using-the-cli/installation) with CommandBox to do it automatically for you. 95 | 96 | For both Lucee and ACF you need to set the JDBC Driver class to `org.sqlite.JDBC`. Then you need to specify the JDBC connection string as `jdbc:sqlite:`. i.e. `jdbc:sqlite:C:/data/my_database.db` 97 | 98 | **Lucee** 99 | 1. Navigate to Datasources in the Lucee administrator 100 | 2. Enter datasource name 101 | 3. Select Type: Other - JDBC Driver 102 | 4. Click Create 103 | 5. Enter `org.sqlite.JDBC` for Class 104 | 6. Enter the Connection String: `jdbc:sqlite:` 105 | 7. Click Create 106 | 107 | **ACF** 108 | 1. Navigate to Datasources in the ACF administrator 109 | 2. Enter the datasource name under Add New Data Source 110 | 3. Select `other` for the datasource driver 111 | 4. Click Add 112 | 5. Enter `org.sqlite.JDBC` for the Driver Class 113 | 6. Use `org.sqlite.JDBC` for the Driver Name 114 | 7. Etner the JDBC URL: `jdbc:sqlite:` 115 | 8. Click Submit 116 | 117 | 118 | ## Full Docs 119 | 120 | You can browse the full documentation at https://qb.ortusbooks.com 121 | 122 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb", 3 | "version":"12.1.0", 4 | "author":"Eric Peterson", 5 | "homepage":"https://github.com/coldbox-modules/qb", 6 | "documentation":"https://github.com/coldbox-modules/qb", 7 | "location":"forgeboxStorage", 8 | "scripts":{ 9 | "generateAPIDocs":"touch .tmp && rm .tmp --recurse --force && mkdir .tmp --cd && mkdir apidocs && cd .. && docbox generate mapping=qb excludes=test|ModuleConfig strategy-outputDir=.tmp/apidocs strategy-projectTitle=qb", 10 | "commitAPIDocs":"run-script generateAPIDocs && !git add docs/apidocs/* && !git commit -m 'Updated API Docs'", 11 | "format":"cfformat run models/**/*.cfc,tests/resources/**/*.cfc,tests/specs/**/*.cfc --overwrite", 12 | "format:check":"cfformat check models/**/*.cfc,tests/resources/**/*.cfc,tests/specs/**/*.cfc", 13 | "bx-modules:install":"install bx-compat-cfml@be,bx-esapi" 14 | }, 15 | "repository":{ 16 | "type":"git", 17 | "URL":"https://github.com/coldbox-modules/qb" 18 | }, 19 | "bugs":"https://github.com/coldbox-modules/qb/issues", 20 | "slug":"qb", 21 | "shortDescription":"A query builder for the rest of us", 22 | "type":"modules", 23 | "keywords":[ 24 | "ORM", 25 | "query", 26 | "SQL" 27 | ], 28 | "private":false, 29 | "projectURL":"https://github.com/coldbox-modules/qb", 30 | "license":[ 31 | { 32 | "type":"MIT", 33 | "URL":"https://github.com/coldbox-modules/qb/LICENSE" 34 | } 35 | ], 36 | "dependencies":{ 37 | "cbpaginator":"^2.4.0" 38 | }, 39 | "devDependencies":{ 40 | "testbox":"be" 41 | }, 42 | "installPaths":{ 43 | "testbox":"testbox/", 44 | "cbpaginator":"modules/cbpaginator/" 45 | }, 46 | "ignore":[ 47 | "**/.*", 48 | "test", 49 | "tests", 50 | "docs/**/*.*", 51 | "server.json" 52 | ], 53 | "testbox":{ 54 | "runner":"http://localhost:60299/tests/runner.cfm", 55 | "verbose":false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /models/Grammars/AutoDiscover.cfc: -------------------------------------------------------------------------------- 1 | component singleton { 2 | 3 | property name="wirebox" inject="wirebox"; 4 | property name="grammar"; 5 | 6 | function autoDiscoverGrammar() { 7 | cfdbinfo( type = "Version", name = "local.dbInfo" ); 8 | 9 | switch ( dbInfo.DATABASE_PRODUCTNAME ) { 10 | case "MySQL": 11 | case "MariaDB": 12 | return wirebox.getInstance( "MySQLGrammar@qb" ); 13 | case "Derby": 14 | return wirebox.getInstance( "DerbyGrammar@qb" ); 15 | case "PostgreSQL": 16 | return wirebox.getInstance( "PostgresGrammar@qb" ); 17 | case "Microsoft SQL Server": 18 | return wirebox.getInstance( "SQLServerGrammar@qb" ); 19 | case "Oracle": 20 | return wirebox.getInstance( "OracleGrammar@qb" ); 21 | case "SQLite": 22 | return wirebox.getInstance( "SQLiteGrammar@qb" ); 23 | default: 24 | return wirebox.getInstance( "BaseGrammar@qb" ); 25 | } 26 | } 27 | 28 | function onMissingMethod( missingMethodName, missingMethodArguments ) { 29 | if ( !structKeyExists( variables, "grammar" ) ) { 30 | variables.grammar = autoDiscoverGrammar(); 31 | } 32 | return invoke( variables.grammar, missingMethodName, missingMethodArguments ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /models/Grammars/MySQLGrammar.cfc: -------------------------------------------------------------------------------- 1 | component extends="qb.models.Grammars.BaseGrammar" singleton { 2 | 3 | private string function orderByRandom() { 4 | return "RAND()"; 5 | } 6 | 7 | /** 8 | * Parses and wraps a value from the Builder for use in a sql statement. 9 | * 10 | * @table The value to parse and wrap. 11 | * 12 | * @return string 13 | */ 14 | function wrapValue( required any value ) { 15 | if ( !variables.shouldWrapValues ) { 16 | return arguments.value; 17 | } 18 | 19 | if ( value == "*" ) { 20 | return value; 21 | } 22 | 23 | arguments.value = reReplace( arguments.value, """", "", "all" ); 24 | 25 | return "`#value#`"; 26 | } 27 | 28 | /** 29 | * Parses and wraps a value from the Builder for use in a sql statement. 30 | * 31 | * @table The value to parse and wrap. 32 | * 33 | * @return string 34 | */ 35 | public string function wrapAlias( required any value ) { 36 | return "`#value#`"; 37 | } 38 | 39 | function compileRenameTable( blueprint, commandParameters ) { 40 | try { 41 | var originalShouldWrapValues = getShouldWrapValues(); 42 | if ( !isNull( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ) ) { 43 | setShouldWrapValues( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ); 44 | } 45 | 46 | return concatenate( [ 47 | "RENAME TABLE", 48 | wrapTable( blueprint.getTable() ), 49 | "TO", 50 | wrapTable( commandParameters.to ) 51 | ] ); 52 | } finally { 53 | if ( !isNull( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ) ) { 54 | setShouldWrapValues( originalShouldWrapValues ); 55 | } 56 | } 57 | } 58 | 59 | function compileDropForeignKey( blueprint, commandParameters ) { 60 | try { 61 | var originalShouldWrapValues = getShouldWrapValues(); 62 | if ( !isNull( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ) ) { 63 | setShouldWrapValues( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ); 64 | } 65 | 66 | return "ALTER TABLE #wrapTable( blueprint.getTable() )# DROP FOREIGN KEY #wrapValue( commandParameters.name )#"; 67 | } finally { 68 | if ( !isNull( arguments.blueprint.getSchemaBuilder().getShouldWrapValues() ) ) { 69 | setShouldWrapValues( originalShouldWrapValues ); 70 | } 71 | } 72 | } 73 | 74 | function compileDropAllObjects( required struct options, string schema = "", required SchemaBuilder sb ) { 75 | try { 76 | var originalShouldWrapValues = getShouldWrapValues(); 77 | if ( !isNull( arguments.sb.getShouldWrapValues() ) ) { 78 | setShouldWrapValues( arguments.sb.getShouldWrapValues() ); 79 | } 80 | 81 | var tables = getAllTableNames( options ); 82 | var tableList = arrayToList( 83 | arrayMap( tables, function( table ) { 84 | return wrapTable( table ); 85 | } ), 86 | ", " 87 | ); 88 | 89 | return arrayFilter( 90 | [ 91 | compileDisableForeignKeyConstraints(), 92 | arrayIsEmpty( tables ) ? "" : "DROP TABLE #tableList#", 93 | compileEnableForeignKeyConstraints() 94 | ], 95 | function( sql ) { 96 | return sql != ""; 97 | } 98 | ); 99 | } finally { 100 | if ( !isNull( arguments.sb.getShouldWrapValues() ) ) { 101 | setShouldWrapValues( originalShouldWrapValues ); 102 | } 103 | } 104 | } 105 | 106 | function getAllTableNames( options ) { 107 | var tablesQuery = runQuery( 108 | "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'", 109 | {}, 110 | options, 111 | "query" 112 | ); 113 | var columnName = arrayToList( 114 | arrayFilter( tablesQuery.getColumnNames(), function( columnName ) { 115 | return columnName != "Table_type"; 116 | } ) 117 | ); 118 | var tables = []; 119 | for ( var table in tablesQuery ) { 120 | arrayAppend( tables, table[ columnName ] ); 121 | } 122 | return tables; 123 | } 124 | 125 | /** 126 | * Compile a Builder's query into an insert string. 127 | * 128 | * @query The Builder instance. 129 | * @columns The array of columns into which to insert. 130 | * @values The array of values to insert. 131 | * 132 | * @return string 133 | */ 134 | public string function compileInsert( required QueryBuilder query, required array columns, required array values ) { 135 | if ( !query.getReturning().isEmpty() ) { 136 | throw( type = "UnsupportedOperation", message = "This grammar does not support a RETURNING clause" ); 137 | } 138 | return super.compileInsert( argumentCollection = arguments ); 139 | } 140 | 141 | /** 142 | * Compile a Builder's query into an insert string ignoring duplicate key values. 143 | * 144 | * @qb The Builder instance. 145 | * @columns The array of columns into which to insert. 146 | * @target The array of key columns to match. 147 | * @values The array of values to insert. 148 | * 149 | * @return string 150 | */ 151 | public string function compileInsertIgnore( 152 | required QueryBuilder qb, 153 | required array columns, 154 | required array target, 155 | required array values 156 | ) { 157 | return replace( 158 | compileInsert( arguments.qb, arguments.columns, arguments.values ), 159 | "INSERT", 160 | "INSERT IGNORE", 161 | "one" 162 | ); 163 | } 164 | 165 | /** 166 | * Compile a Builder's query into an insert using string. 167 | * 168 | * @query The Builder instance. 169 | * @columns The array of columns into which to insert. 170 | * @source The source builder object to insert from. 171 | * 172 | * @return string 173 | */ 174 | public string function compileInsertUsing( 175 | required any query, 176 | required array columns, 177 | required QueryBuilder source 178 | ) { 179 | try { 180 | var originalShouldWrapValues = getShouldWrapValues(); 181 | if ( !isNull( arguments.query.getShouldWrapValues() ) ) { 182 | setShouldWrapValues( arguments.query.getShouldWrapValues() ); 183 | } 184 | 185 | var columnsString = arguments.columns 186 | .map( function( column ) { 187 | return wrapColumn( column.formatted ); 188 | } ) 189 | .toList( ", " ); 190 | 191 | var cteClause = query.getCommonTables().isEmpty() ? "" : " #compileCommonTables( query, query.getCommonTables() )#"; 192 | 193 | return "INSERT INTO #wrapTable( arguments.query.getTableName() )# (#columnsString#)#cteClause# #compileSelect( arguments.source )#"; 194 | } finally { 195 | if ( !isNull( arguments.query.getShouldWrapValues() ) ) { 196 | setShouldWrapValues( originalShouldWrapValues ); 197 | } 198 | } 199 | } 200 | 201 | /** 202 | * Compile a Builder's query into a delete string. 203 | * 204 | * @query The Builder instance. 205 | * 206 | * @return string 207 | */ 208 | public string function compileDelete( required QueryBuilder query ) { 209 | if ( !arguments.query.getReturning().isEmpty() ) { 210 | throw( 211 | type = "UnsupportedOperation", 212 | message = "This grammar does not support DELETE actions with a RETURNING clause." 213 | ); 214 | } 215 | 216 | try { 217 | var originalShouldWrapValues = getShouldWrapValues(); 218 | if ( !isNull( arguments.query.getShouldWrapValues() ) ) { 219 | setShouldWrapValues( arguments.query.getShouldWrapValues() ); 220 | } 221 | 222 | var hasJoins = !arguments.query.getJoins().isEmpty(); 223 | 224 | return trim( 225 | arrayToList( 226 | arrayFilter( 227 | [ 228 | "DELETE", 229 | hasJoins ? wrapTable( query.getTableName() ) : "", 230 | "FROM", 231 | wrapTable( query.getTableName() ), 232 | hasJoins ? compileJoins( query, query.getJoins() ) : "", 233 | compileWheres( query, query.getWheres() ) 234 | ], 235 | function( sql ) { 236 | return sql != ""; 237 | } 238 | ), 239 | " " 240 | ) 241 | ); 242 | } finally { 243 | if ( !isNull( arguments.query.getShouldWrapValues() ) ) { 244 | setShouldWrapValues( originalShouldWrapValues ); 245 | } 246 | } 247 | } 248 | 249 | public string function compileUpsert( 250 | required QueryBuilder qb, 251 | required array insertColumns, 252 | required array values, 253 | required array updateColumns, 254 | required any updates, 255 | required array target, 256 | QueryBuilder source, 257 | any deleteUnmatched = false 258 | ) { 259 | if ( !isBoolean( arguments.deleteUnmatched ) || arguments.deleteUnmatched ) { 260 | throw( type = "UnsupportedOperation", message = "This grammar does not support DELETE in a upsert clause" ); 261 | } 262 | 263 | try { 264 | var originalShouldWrapValues = getShouldWrapValues(); 265 | if ( !isNull( arguments.qb.getShouldWrapValues() ) ) { 266 | setShouldWrapValues( arguments.qb.getShouldWrapValues() ); 267 | } 268 | 269 | var insertString = isNull( arguments.source ) ? this.compileInsert( 270 | arguments.qb, 271 | arguments.insertColumns, 272 | arguments.values 273 | ) : this.compileInsertUsing( arguments.qb, arguments.insertColumns, arguments.source ); 274 | var updateString = ""; 275 | if ( isArray( arguments.updates ) ) { 276 | updateString = arguments.updateColumns 277 | .map( function( column ) { 278 | return "#wrapValue( column.formatted )# = VALUES(#wrapValue( column.formatted )#)"; 279 | } ) 280 | .toList( ", " ); 281 | } else { 282 | updateString = arguments.updateColumns 283 | .map( function( column ) { 284 | var value = updates[ column.original ]; 285 | return "#wrapValue( column.formatted )# = #getUtils().isExpression( value ) ? value.getSQL() : "?"#"; 286 | } ) 287 | .toList( ", " ); 288 | } 289 | return insertString & " ON DUPLICATE KEY UPDATE #updateString#"; 290 | } finally { 291 | if ( !isNull( arguments.qb.getShouldWrapValues() ) ) { 292 | setShouldWrapValues( originalShouldWrapValues ); 293 | } 294 | } 295 | } 296 | 297 | function compileDisableForeignKeyConstraints() { 298 | return "SET FOREIGN_KEY_CHECKS=0"; 299 | } 300 | 301 | function compileEnableForeignKeyConstraints() { 302 | return "SET FOREIGN_KEY_CHECKS=1"; 303 | } 304 | 305 | function generateDefault( column ) { 306 | if ( 307 | column.getDefaultValue() == "" && 308 | column.getType().findNoCase( "TIMESTAMP" ) > 0 309 | ) { 310 | if ( column.getIsNullable() ) { 311 | return "NULL DEFAULT NULL"; 312 | } else { 313 | column.withCurrent(); 314 | } 315 | } 316 | return super.generateDefault( column ); 317 | } 318 | 319 | function wrapDefaultType( column ) { 320 | switch ( column.getType() ) { 321 | case "boolean": 322 | return column.getDefaultValue() ? 1 : 0; 323 | case "char": 324 | case "string": 325 | return "'#column.getDefaultValue()#'"; 326 | default: 327 | return column.getDefaultValue(); 328 | } 329 | } 330 | 331 | function typeChar( column ) { 332 | return "NCHAR(#column.getLength()#)"; 333 | } 334 | 335 | function typeGUID( column ) { 336 | return "CHAR(#column.getLength()#)"; 337 | } 338 | 339 | function typeUUID( column ) { 340 | return typeGUID( column ); 341 | } 342 | 343 | function typeJson( column ) { 344 | return "JSON"; 345 | } 346 | 347 | function typeJsonb( column ) { 348 | return "JSON"; 349 | } 350 | 351 | function typeLineString( column ) { 352 | return "LINESTRING"; 353 | } 354 | 355 | function typePoint( column ) { 356 | return "POINT"; 357 | } 358 | 359 | function typePolygon( column ) { 360 | return "POLYGON"; 361 | } 362 | 363 | function typeLongText( column ) { 364 | return "LONGTEXT"; 365 | } 366 | 367 | function typeMediumText( column ) { 368 | return "MEDIUMTEXT"; 369 | } 370 | 371 | } 372 | -------------------------------------------------------------------------------- /models/Query/Expression.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Expression is a simple wrapper around text that should not 3 | * be parsed or evaluated or modified in any way. 4 | * Expressions are included as-is in a sql statement. 5 | */ 6 | component displayname="Expression" accessors="true" { 7 | 8 | /** 9 | * The raw sql value 10 | */ 11 | property name="sql" type="string" default=""; 12 | property name="bindings" type="array"; 13 | 14 | this.isExpression = true; 15 | 16 | /** 17 | * Create a new Expression wrapping up some sql. 18 | * 19 | * @sql The sql string to wrap up. 20 | * 21 | * @return qb.models.Query.Expression 22 | */ 23 | public Expression function init( required string sql, array bindings = [] ) { 24 | variables.sql = arguments.sql; 25 | variables.bindings = arguments.bindings; 26 | return this; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /models/Query/JoinClause.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a Join clause in a sql statement 3 | */ 4 | component displayname="JoinClause" accessors="true" extends="qb.models.Query.QueryBuilder" { 5 | 6 | /** 7 | * A reference to the parent query to which this join clause belongs. 8 | */ 9 | property name="parentQuery" type="qb.models.Query.QueryBuilder"; 10 | 11 | /** 12 | * The join type of the join clause. 13 | */ 14 | property name="type" type="string"; 15 | 16 | /** 17 | * The table to join. 18 | */ 19 | property name="table" type="any"; 20 | 21 | /** 22 | * In the {cross,outer}Apply case, the already-toSql'd string of the table expr source. 23 | * e.g. will be a string like "select 1 from foo where x = ?" 24 | */ 25 | property name="lateralRawExpression" type="string"; 26 | 27 | /** 28 | * Valid join types for join clauses. 29 | */ 30 | variables.types = [ 31 | "inner", 32 | "full", 33 | "full outer", 34 | "cross", 35 | "left", 36 | "left outer", 37 | "right", 38 | "right outer", 39 | "outer apply", 40 | "cross apply", 41 | "lateral" 42 | ]; 43 | 44 | /** 45 | * Creates a basic join clause. 46 | * 47 | * @parentQuery A reference to the query to which this join clause belongs. 48 | * @type The join type of this join clause. 49 | * @table The table to join. 50 | * @crossApplySqlStringWithBindParams The already-`toSql`'d table expression for the {cross,outer}Apply case 51 | * 52 | * @return qb.models.Query.JoinClause 53 | */ 54 | public JoinClause function init( 55 | required QueryBuilder parentQuery, 56 | required string type, 57 | required any table, 58 | string lateralRawExpression 59 | ) { 60 | var typeIsValid = false; 61 | for ( var validType in variables.types ) { 62 | if ( validType == arguments.type ) { 63 | typeIsValid = true; 64 | } 65 | } 66 | if ( !typeIsValid ) { 67 | throw( type = "InvalidSQLType", message = "[#type#] is not a valid sql join type" ); 68 | } 69 | 70 | variables.parentQuery = arguments.parentQuery; 71 | variables.type = arguments.type; 72 | variables.table = arguments.table; 73 | variables.lateralRawExpression = isNull( arguments.lateralRawExpression ) 74 | ? "" 75 | : arguments.lateralRawExpression; 76 | 77 | super.init( parentQuery.getGrammar(), parentQuery.getUtils() ); 78 | 79 | return this; 80 | } 81 | 82 | /** 83 | * Add a column condition to the join statement. 84 | * 85 | * @first The name of the first column with which to join. A closure can be passed to create nested join statements. 86 | * @operator The join operator to use. 87 | * @second The name of the second column with which to join. 88 | * @combinator The combinator to use between joins. 89 | * 90 | * @return qb.models.Query.JoinClause 91 | */ 92 | public JoinClause function on( 93 | required first, 94 | operator, 95 | second, 96 | combinator = "and" 97 | ) { 98 | if ( isClosure( first ) || isCustomFunction( first ) ) { 99 | return whereNested( first, combinator ); 100 | } 101 | 102 | return whereColumn( argumentCollection = arguments ); 103 | } 104 | 105 | /** 106 | * Add an or column condition to the join statement. 107 | * 108 | * @first The name of the first column with which to join. A closure can be passed to create nested join statements. 109 | * @operator The join operator to use. 110 | * @second The name of the second column with which to join. 111 | * 112 | * @return qb.models.Query.JoinClause 113 | */ 114 | public JoinClause function orOn( required first, operator, second ) { 115 | arguments.combinator = "or"; 116 | return on( argumentCollection = arguments ); 117 | } 118 | 119 | /** 120 | * Returns a new Join Clause based off of the current join clause. 121 | * 122 | * @return qb.models.Builder.JoinClause 123 | */ 124 | public QueryBuilder function newQuery() { 125 | return new qb.models.Query.JoinClause( parentQuery = getParentQuery(), type = getType(), table = getTable() ); 126 | } 127 | 128 | /** 129 | * Returns whether the object is a JoinClause. 130 | * This exists because isInstanceOf is super slow! 131 | * 132 | * @returns boolean 133 | */ 134 | public boolean function isJoin() { 135 | return true; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /models/SQLCommenter/ColdBoxSQLCommenter.cfc: -------------------------------------------------------------------------------- 1 | component extends="SQLCommenter" singleton accessors="true" { 2 | 3 | /** 4 | * All the qb module settings so we can inspect the sqlCommenter settings. 5 | */ 6 | property name="settings" inject="box:moduleSettings:qb"; 7 | 8 | /** 9 | * WireBox Injector 10 | */ 11 | property name="wirebox" inject="wirebox"; 12 | 13 | /** 14 | * An array of configured Commenter components. 15 | * These are used to fetch the comments for each SQL query. 16 | */ 17 | property name="commenters" type="array"; 18 | 19 | /** 20 | * Set up the commenters array with configured Commenter components. 21 | */ 22 | function onDIComplete() { 23 | variables.commenters = variables.settings.sqlCommenter.commenters.map( ( commenterInfo ) => { 24 | param commenterInfo.properties = {}; 25 | if ( !commenterInfo.keyExists( "class" ) ) { 26 | throw( "A commenter must have a class pointing to a WireBox mapping" ); 27 | } 28 | 29 | return variables.wirebox.getInstance( commenterInfo.class ).setProperties( commenterInfo.properties ); 30 | } ); 31 | } 32 | 33 | /** 34 | * Gathers comments from the configured commenters and appends it to the SQL query. 35 | * 36 | * @sql The SQL string to add the comment to. 37 | * @datasource The datasource that will execute the query. 38 | * If null, the default datasource is going to be used. 39 | * @bindings An array of bindings for the query. 40 | * 41 | * @return Commented SQL string 42 | */ 43 | public string function appendSqlComments( required string sql, string datasource, array bindings = [] ) { 44 | if ( !settings.sqlCommenter.enabled ) { 45 | return arguments.sql; 46 | } 47 | 48 | var comments = variables.commenters.reduce( ( acc, commenter ) => { 49 | acc.append( 50 | commenter.getComments( 51 | sql = sql, 52 | datasource = isNull( datasource ) ? javacast( "null", "" ) : datasource, 53 | bindings = bindings 54 | ), 55 | true 56 | ); 57 | return acc; 58 | }, {} ); 59 | 60 | return appendCommentsToSQL( arguments.sql, comments ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /models/SQLCommenter/Commenters/BindingsCommenter.cfc: -------------------------------------------------------------------------------- 1 | component singleton accessors="true" { 2 | 3 | property name="queryUtil" inject="QueryUtils@qb"; 4 | property name="properties"; 5 | 6 | /** 7 | * Returns a struct of key/value comment pairs to append to the SQL. 8 | * 9 | * @sql The SQL to append the comments to. This is provided if you need to 10 | * inspect the SQL to make any decisions about what comments to return. 11 | * @datasource The datasource that will execute the query. If null, the default datasource will be used. 12 | * This can be used to make decisions about what comments to return. 13 | */ 14 | public struct function getComments( required string sql, string datasource, array bindings = [] ) { 15 | return { "bindings": serializeBindings( bindings = bindings, delimiter = ";" ) }; 16 | } 17 | 18 | private string function serializeBindings( required array bindings, string delimiter = ";" ) { 19 | return serializeJSON( 20 | bindings.map( ( binding ) => { 21 | return limitString( 22 | str = isSimpleValue( binding ) ? binding : variables.queryUtil.castAsSqlType( 23 | value = binding.null ? javacast( "null", "" ) : binding.value, 24 | sqltype = binding.cfsqltype 25 | ), 26 | limit = 100 27 | ); 28 | } ) 29 | ); 30 | } 31 | 32 | private string function limitString( required string str, required numeric limit, string end = "..." ) { 33 | if ( len( arguments.str ) <= arguments.limit ) { 34 | return arguments.str; 35 | } 36 | 37 | return left( arguments.str, arguments.limit ) & arguments.end; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /models/SQLCommenter/Commenters/DBInfoCommenter.cfc: -------------------------------------------------------------------------------- 1 | component singleton accessors="true" { 2 | 3 | property name="properties"; 4 | property name="driverVersionByDatasource"; 5 | 6 | /** 7 | * Returns a struct of key/value comment pairs to append to the SQL. 8 | * 9 | * @sql The SQL to append the comments to. This is provided if you need to 10 | * inspect the SQL to make any decisions about what comments to return. 11 | * @datasource The datasource that will execute the query. If null, the default datasource will be used. 12 | * This can be used to make decisions about what comments to return. 13 | */ 14 | public struct function getComments( required string sql, string datasource, array bindings = [] ) { 15 | param variables.driverVersionByDatasource = {}; 16 | var driverVersion = "UNKNOWN"; 17 | if ( isNull( arguments.datasource ) ) { 18 | if ( !variables.driverVersionByDatasource.keyExists( "__DEFAULT__" ) ) { 19 | cfdbinfo( type = "version", name = "local.dbInfo" ); 20 | variables.driverVersionByDatasource[ "__DEFAULT__" ] = local.dbInfo.DRIVER_VERSION; 21 | } 22 | driverVersion = variables.driverVersionByDatasource[ "__DEFAULT__" ]; 23 | } else { 24 | if ( !variables.driverVersionByDatasource.keyExists( arguments.datasource ) ) { 25 | cfdbinfo( type = "version", name = "local.dbInfo", datasource = arguments.datasource ); 26 | variables.driverVersionByDatasource[ arguments.datasource ] = local.dbInfo.DRIVER_VERSION; 27 | } 28 | driverVersion = variables.driverVersionByDatasource[ arguments.datasource ]; 29 | } 30 | return { "dbDriver": driverVersion }; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /models/SQLCommenter/Commenters/FrameworkCommenter.cfc: -------------------------------------------------------------------------------- 1 | component singleton accessors="true" { 2 | 3 | property name="wirebox" inject="wirebox"; 4 | property name="properties"; 5 | 6 | /** 7 | * Returns a struct of key/value comment pairs to append to the SQL. 8 | * 9 | * @sql The SQL to append the comments to. This is provided if you need to 10 | * inspect the SQL to make any decisions about what comments to return. 11 | * @datasource The datasource that will execute the query. If null, the default datasource will be used. 12 | * This can be used to make decisions about what comments to return. 13 | */ 14 | public struct function getComments( required string sql, string datasource, array bindings = [] ) { 15 | param variables.coldboxVersion = variables.wirebox.getInstance( "coldbox:coldboxSetting:version" ); 16 | return { "version": "coldbox-#variables.coldboxVersion#" }; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /models/SQLCommenter/Commenters/ICommenter.cfc: -------------------------------------------------------------------------------- 1 | interface displayName="ICommenter" { 2 | 3 | /** 4 | * Returns a struct of key/value comment pairs to append to the SQL. 5 | * 6 | * @sql The SQL to append the comments to. This is provided if you need to 7 | * inspect the SQL to make any decisions about what comments to return. 8 | * @datasource The datasource that will execute the query. If null, the default datasource will be used. 9 | * This can be used to make decisions about what comments to return. 10 | * @bindings An array of bindings for the query 11 | */ 12 | public struct function getComments( required string sql, string datasource, array bindings ); 13 | 14 | // You can use `accessors="true"` with a `property name="properties";` to implement these methods. 15 | public ICommenter function setProperties( required struct properties ); 16 | public struct function getProperties(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /models/SQLCommenter/Commenters/RouteInfoCommenter.cfc: -------------------------------------------------------------------------------- 1 | component singleton accessors="true" { 2 | 3 | property name="wirebox" inject="wirebox"; 4 | property name="properties"; 5 | 6 | /** 7 | * Returns a struct of key/value comment pairs to append to the SQL. 8 | * 9 | * @sql The SQL to append the comments to. This is provided if you need to 10 | * inspect the SQL to make any decisions about what comments to return. 11 | * @datasource The datasource that will execute the query. If null, the default datasource will be used. 12 | * This can be used to make decisions about what comments to return. 13 | */ 14 | public struct function getComments( required string sql, string datasource, array bindings = [] ) { 15 | var event = wirebox.getInstance( "coldbox:requestContext" ); 16 | return { 17 | "event": event.getCurrentEvent(), 18 | "handler": event.getCurrentHandler(), 19 | "action": event.getCurrentAction(), 20 | "route": event.getCurrentRoutedURL() 21 | }; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /models/SQLCommenter/NullSQLCommenter.cfc: -------------------------------------------------------------------------------- 1 | component extends="SQLCommenter" singleton { 2 | 3 | /** 4 | * Returns the SQL unchanged for the NullSQLCommenter. 5 | * 6 | * @sql The SQL string to add the comment to. 7 | * @datasource The datasource that will execute the query. 8 | * If null, the default datasource is going to be used. 9 | * 10 | * @return Commented SQL string 11 | */ 12 | public string function appendSqlComments( required string sql, string datasource, array bindings = [] ) { 13 | return arguments.sql; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/SQLCommenter/SQLCommenter.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @doc_abstract true 3 | */ 4 | component singleton { 5 | 6 | /** 7 | * Gathers comments from the configured commenters and appends it to the SQL query. 8 | * @doc_abstract true 9 | * 10 | * @sql The SQL string to add the comment to. 11 | * @datasource The datasource that will execute the query. 12 | * If null, the default datasource is going to be used. 13 | * 14 | * @return Commented SQL string 15 | */ 16 | public string function appendSqlComments( required string sql, string datasource, array bindings = [] ) { 17 | throw( "appendSqlComments is an abstract method and must be implemented in a subclass." ); 18 | } 19 | 20 | /** 21 | * Serializes and appends a struct of key/value comment pairs to the provided SQL string. 22 | * 23 | * @sql The SQL string to add the serialized comments to. 24 | * @comments The key/value pairs to serialize and append. 25 | * 26 | * @return Commented SQL string 27 | */ 28 | public string function appendCommentsToSQL( required string sql, required struct comments ) { 29 | // DO NOT mutate a statement with an already present comment 30 | if ( containsSQLComment( arguments.sql ) ) { 31 | return arguments.sql; 32 | } 33 | 34 | var serializedComments = []; 35 | for ( var key in arguments.comments ) { 36 | serializedComments.append( serializeComment( key, arguments.comments[ key ] ) ); 37 | } 38 | 39 | arraySort( serializedComments, "textnocase" ) 40 | 41 | return arguments.sql & " /*#arrayToList( serializedComments )#*/"; 42 | } 43 | 44 | /** 45 | * Parses a commented SQL string into the SQL and a struct of the key/value pair comments. 46 | * 47 | * @sql The commented SQL string to parse. 48 | * 49 | * @return { "sql": string, "comments": struct } 50 | */ 51 | public struct function parseCommentedSQL( required string sql ) { 52 | var commentStartPosition = find( "/*", arguments.sql ) - 1; 53 | return { 54 | "sql": left( arguments.sql, commentStartPosition - 1 ), 55 | "comments": parseCommentString( 56 | mid( arguments.sql, commentStartPosition + 1, len( arguments.sql ) - commentStartPosition ) 57 | ) 58 | }; 59 | } 60 | 61 | /** 62 | * Parses a comment string into a struct. 63 | * 64 | * @commentString The comment string to parse into a struct. 65 | * 66 | * @return A struct of key/value pairs from the comment. 67 | */ 68 | public struct function parseCommentString( required string commentString ) { 69 | arguments.commentString = trim( arguments.commentString ); 70 | arguments.commentString = replace( arguments.commentString, "/*", "" ); 71 | arguments.commentString = replace( arguments.commentString, "*/", "" ); 72 | return listToArray( arguments.commentString ).reduce( ( acc, serializedKeyValuePair ) => { 73 | var key = decodeFromURL( unescapeMetaCharacters( listFirst( serializedKeyValuePair, "=" ) ) ); 74 | var value = decodeFromURL( 75 | unescapeMetaCharacters( unescapeSQL( listLast( serializedKeyValuePair, "=" ) ) ) 76 | ); 77 | acc[ key ] = value; 78 | return acc; 79 | }, {} ); 80 | } 81 | 82 | /** 83 | * Returns true if the SQL already contains a comment. 84 | * 85 | * @sql The SQL to check for a comment. 86 | * 87 | * @return True if the SQL already contains a comment. 88 | */ 89 | private boolean function containsSQLComment( required string sql ) { 90 | return find( "--", arguments.sql ) > 0 || find( "/*", arguments.sql ) > 0; 91 | } 92 | 93 | /** 94 | * Serializes a key/value pair for use in a comment string. 95 | * 96 | * @key The key to serialize. 97 | * @value The value to serialize. 98 | * 99 | * @return A serialized string for the key/value pair. 100 | */ 101 | private string function serializeComment( required string key, required string value ) { 102 | return serializeKey( arguments.key ) & "=" & serializeValue( arguments.value ); 103 | } 104 | 105 | /** 106 | * Serializes a key for use in a comment string. 107 | * 108 | * @key The key to serialize. 109 | * 110 | * @return The serialized key. 111 | */ 112 | private string function serializeKey( required string key ) { 113 | return escapeMetaCharacters( encodeForURL( arguments.key ) ); 114 | } 115 | 116 | /** 117 | * Serializes a value for use in a comment string. 118 | * 119 | * @value The value to serialize. 120 | * 121 | * @return The serialized value. 122 | */ 123 | private string function serializeValue( required string value ) { 124 | return escapeSQL( 125 | escapeMetaCharacters( 126 | replace( 127 | encodeForURL( arguments.value ), 128 | "+", 129 | "%20", 130 | "all" 131 | ) 132 | ) 133 | ); 134 | } 135 | 136 | /** 137 | * Escapes meta characters such as single quotes in the passed in string. 138 | * 139 | * @str The string to escape meta characters. 140 | * 141 | * @return The string with meta characters escaped. 142 | */ 143 | private string function escapeMetaCharacters( required string str ) { 144 | return replace( arguments.str, "'", "\'", "all" ); 145 | } 146 | 147 | /** 148 | * Unescapes meta characters such as single quotes in the passed in string. 149 | * 150 | * @str The string to unescape meta characters. 151 | * 152 | * @return The string without meta characters escaped. 153 | */ 154 | private string function unescapeMetaCharacters( required string str ) { 155 | return replace( arguments.str, "\'", "'", "all" ); 156 | } 157 | 158 | /** 159 | * Escapes a string for SQL by surrounding it in single quotes. 160 | * 161 | * @str The string to escape for SQL. 162 | * 163 | * @returns The string escaped for SQL. 164 | */ 165 | private string function escapeSQL( required string str ) { 166 | return "'" & arguments.str & "'"; 167 | } 168 | 169 | /** 170 | * Unescapes a string for SQL by removing the surrounding single quotes. 171 | * 172 | * @str The string to unescape for SQL. 173 | * 174 | * @returns The string unescaped for SQL. 175 | */ 176 | private string function unescapeSQL( required string str ) { 177 | return mid( arguments.str, 2, len( arguments.str ) - 2 ); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /models/Schema/Column.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a column in a create or alter sql schema. 3 | */ 4 | component accessors="true" { 5 | 6 | /** 7 | * Reference to the owning blueprint. 8 | * This allows methods to add commands to the schema builder as needed. 9 | */ 10 | property name="blueprint"; 11 | 12 | /** 13 | * The column name. 14 | */ 15 | property name="name"; 16 | 17 | /** 18 | * The schema builder type. 19 | */ 20 | property name="type"; 21 | 22 | /** 23 | * The column length. 24 | */ 25 | property name="length" default="255"; 26 | 27 | /** 28 | * The precision for the column. 29 | */ 30 | property name="precision"; 31 | 32 | /** 33 | * Whether the column value can be null. 34 | */ 35 | property name="isNullable" default="false"; 36 | 37 | /** 38 | * Whether the column value is only unsigned. 39 | */ 40 | property name="isUnsigned" default="false"; 41 | 42 | /** 43 | * Whether the column is auto incremented. 44 | */ 45 | property name="autoIncrement" default="false"; 46 | 47 | /* 48 | * The default value for the column. 49 | */ 50 | property name="defaultValue" default=""; 51 | 52 | /** 53 | * A comment for the column. 54 | */ 55 | property name="commentValue" default=""; 56 | 57 | /** 58 | * Whether the column is unique. 59 | */ 60 | property name="isUnique" default="false"; 61 | 62 | /** 63 | * The possible values for the column. 64 | * Used mainly by the ENUM type. 65 | */ 66 | property name="values"; 67 | 68 | /** 69 | * The computed column type, if any. Defaults to `none`. 70 | */ 71 | property name="computedType" default="none"; 72 | 73 | /** 74 | * The definition of the computed column, if any. 75 | */ 76 | property name="computedDefinition"; 77 | 78 | /** 79 | * Create a new column representation. 80 | * 81 | * @blueprint The blueprint object creating this column. 82 | * 83 | * @returns The Column instance. 84 | */ 85 | public Column function init( required Blueprint blueprint ) { 86 | setBlueprint( arguments.blueprint ); 87 | variables.values = []; 88 | variables.computedType = "none"; 89 | variables.computedDefinition = ""; 90 | return this; 91 | } 92 | 93 | public Column function populate( struct args = {} ) { 94 | for ( var arg in arguments.args ) { 95 | if ( 96 | ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) && 97 | !isNull( arguments.args[ arg ] ) 98 | ) { 99 | invoke( this, "set#arg#", [ arguments.args[ arg ] ] ); 100 | } 101 | } 102 | return this; 103 | } 104 | 105 | /** 106 | * Attach a comment to the column. 107 | * 108 | * @comment The comment text. 109 | * 110 | * @returns The Column instance. 111 | */ 112 | public Column function comment( required string comment ) { 113 | setCommentValue( arguments.comment ); 114 | return this; 115 | } 116 | 117 | /** 118 | * Sets a default value for the column. 119 | * 120 | * @value The default value. 121 | * 122 | * @returns The Column instance. 123 | */ 124 | public Column function default( required string value ) { 125 | setDefaultValue( arguments.value ); 126 | return this; 127 | } 128 | 129 | /** 130 | * Sets the column to allow null values. 131 | * 132 | * @returns The Column instance. 133 | */ 134 | public Column function nullable() { 135 | setIsNullable( true ); 136 | return this; 137 | } 138 | 139 | /** 140 | * Adds the column as a primary key for the table. 141 | * 142 | * @indexName Optional. The name to use for the primary key constraint. 143 | * If omitted, the indexName is derived from the table name and column name. 144 | * 145 | * @returns The TableIndex instance created. 146 | */ 147 | public TableIndex function primaryKey( string indexName ) { 148 | param arguments.indexName = "pk_#getBlueprint().getTable()#_#getName()#"; 149 | return getBlueprint().appendIndex( type = "primary", columns = getName(), name = arguments.indexName ); 150 | } 151 | 152 | /** 153 | * Creates a foreign key constraint for the column. 154 | * Additional configuration of the constraint is done by 155 | * calling methods on the returned TableIndex instance. 156 | * 157 | * @column The column name referenced on the related table. 158 | * 159 | * @returns The TableIndex instance created. 160 | */ 161 | public TableIndex function references( required string column ) { 162 | return getBlueprint().appendIndex( 163 | type = "foreign", 164 | columns = [ column ], 165 | foreignKey = [ getName() ], 166 | name = "fk_#getBlueprint().getTable()#_#getName()#" 167 | ); 168 | } 169 | 170 | /** 171 | * Sets the column as unsigned. 172 | * 173 | * @returns The Column instance. 174 | */ 175 | public Column function unsigned() { 176 | setIsUnsigned( true ); 177 | return this; 178 | } 179 | 180 | /** 181 | * Sets the column to have the UNIQUE constraint. 182 | * 183 | * @returns The Column instance. 184 | */ 185 | public Column function unique() { 186 | setIsUnique( true ); 187 | return this; 188 | } 189 | 190 | /** 191 | * @returns true if the object is a column 192 | */ 193 | public boolean function isColumn() { 194 | return true; 195 | } 196 | 197 | /** 198 | * Sets the default equal to CURRENT_TIMESTAMP 199 | * 200 | * @returns Column 201 | */ 202 | public Column function withCurrent( numeric precision ) { 203 | setDefaultValue( "CURRENT_TIMESTAMP#isNull( arguments.precision ) ? "" : "(#arguments.precision#)"#" ); 204 | return this; 205 | } 206 | 207 | /** 208 | * Marks the column as a stored computed column. 209 | * 210 | * @expression The SQL used to define the computed column. 211 | * 212 | * @returns Column 213 | */ 214 | public Column function storedAs( required string expression ) { 215 | variables.computedType = "stored"; 216 | variables.computedDefinition = arguments.expression; 217 | return this; 218 | } 219 | 220 | /** 221 | * Marks the column as a virtual computed column. 222 | * 223 | * @expression The SQL used to define the computed column. 224 | * 225 | * @returns Column 226 | */ 227 | public Column function virtualAs( required string expression ) { 228 | variables.computedType = "virtual"; 229 | variables.computedDefinition = arguments.expression; 230 | return this; 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /models/Schema/SchemaCommand.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="type"; 4 | property name="parameters"; 5 | 6 | function init( required string type, struct parameters = {} ) { 7 | setType( type ); 8 | setParameters( parameters ); 9 | return this; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /models/Schema/TableIndex.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an index or constraint in the schema. 3 | */ 4 | component accessors="true" { 5 | 6 | /** 7 | * The constraint type. 8 | */ 9 | property name="type"; 10 | 11 | /** 12 | * The constraint name. 13 | */ 14 | property name="name"; 15 | 16 | /** 17 | * The foreign key column 18 | * For example, `country_id` referencing `countries`.`id`. 19 | */ 20 | property name="foreignKey"; 21 | 22 | /** 23 | * The column or columns that make up the constraint. 24 | */ 25 | property name="columns"; 26 | 27 | /** 28 | * The table the foreign key is referencing. 29 | * For example, `countries` for a `country_id` column. 30 | */ 31 | property name="table"; 32 | 33 | /** 34 | * The strategy for updating foreign keys when the parent key is updated. 35 | * Available values are: 36 | * RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT 37 | */ 38 | property name="onUpdateAction" default="NO ACTION"; 39 | 40 | /** 41 | * The strategy for updating foreign keys when the parent key is deleted. 42 | * Available values are: 43 | * RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT 44 | */ 45 | property name="onDeleteAction" default="NO ACTION"; 46 | 47 | /** 48 | * Create a new TableIndex instance. 49 | * 50 | * @returns A TableIndex instance. 51 | */ 52 | public TableIndex function init() { 53 | variables.columns = []; 54 | return this; 55 | } 56 | 57 | public TableIndex function populate( struct args = {} ) { 58 | for ( var arg in arguments.args ) { 59 | if ( 60 | ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) && 61 | !isNull( arguments.args[ arg ] ) 62 | ) { 63 | invoke( this, "set#arg#", [ arguments.args[ arg ] ] ); 64 | } 65 | } 66 | return this; 67 | } 68 | 69 | /** 70 | * Set the referencing column for a foreign key relationship. 71 | * For example, `id` for a `country_id` column. 72 | * 73 | * @columns A column or array of columns that represents the foreign key reference. 74 | * 75 | * @returns The TableIndex instance. 76 | */ 77 | public TableIndex function references( required any columns ) { 78 | setColumns( arrayWrap( arguments.columns ) ); 79 | return this; 80 | } 81 | 82 | /** 83 | * Sets the referencing table for a foreign key relationship. 84 | * For example, `countries` for a `country_id` column. 85 | * 86 | * @table The referencing table name. 87 | * 88 | * @returns The TableIndex instance. 89 | */ 90 | public TableIndex function onTable( required string table ) { 91 | setTable( arguments.table ); 92 | return this; 93 | } 94 | 95 | /** 96 | * Set the strategy for updating foreign keys when the parent key is updated. 97 | * 98 | * @option The strategy to use. Available values are: 99 | * RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT 100 | * 101 | * @returns The TableIndex instance. 102 | */ 103 | public TableIndex function onUpdate( required string option ) { 104 | setOnUpdateAction( arguments.option ); 105 | return this; 106 | } 107 | 108 | /** 109 | * Set the strategy for updating foreign keys when the parent key is deleted. 110 | * 111 | * @option The strategy to use. Available values are: 112 | * RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT 113 | * 114 | * @returns The TableIndex instance. 115 | */ 116 | public TableIndex function onDelete( required string option ) { 117 | setOnDeleteAction( arguments.option ); 118 | return this; 119 | } 120 | 121 | /** 122 | * Set the column or columns that make up the constraint. 123 | * 124 | * @columns A single column or an array of columns that make up the constraint. 125 | * 126 | * @returns The TableIndex instance. 127 | */ 128 | public TableIndex function setColumns( required any columns ) { 129 | variables.columns = arrayWrap( arguments.columns ); 130 | return this; 131 | } 132 | 133 | private array function arrayWrap( required any value ) { 134 | return isArray( arguments.value ) ? arguments.value : [ arguments.value ]; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /server-adobe@2021.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-adobe@2021", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2021", 5 | "cfengine":"adobe@2021" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk11_jre" 18 | }, 19 | "openBrowser":"false", 20 | "scripts":{ 21 | "onServerInstall":"cfpm install zip,debugger" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server-adobe@2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-adobe@2023", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2023", 5 | "cfengine":"adobe@2023" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024" 17 | }, 18 | "openBrowser":"false", 19 | "scripts" : { 20 | "onServerInstall":"cfpm install zip,debugger" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server-adobe@2025.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-adobe@2025", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2025", 5 | "cfengine":"adobe@2025" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre" 18 | }, 19 | "openBrowser":"false", 20 | "scripts":{ 21 | "onServerInstall":"cfpm install zip,debugger" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server-adobe@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-adobe@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobeBE", 5 | "cfengine":"adobe@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024" 17 | }, 18 | "openBrowser":"false", 19 | "scripts" : { 20 | "onServerInstall":"cfpm install zip,debugger" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server-boxlang-cfml@1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-boxlang-cfml@1", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlangCFML", 5 | "cfengine":"boxlang@^1.0.0" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "JVM":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser":"false", 21 | "env":{}, 22 | "scripts":{ 23 | "onServerInitialInstall":"install bx-compat-cfml,bx-esapi --noSave" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server-boxlang@1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-boxlang@1", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlang", 5 | "cfengine":"boxlang@^1.0.0" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "JVM":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser":"false", 21 | "env":{}, 22 | "scripts":{ 23 | "onServerInitialInstall":"install bx-esapi --noSave" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server-boxlang@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-boxlang@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlangBE", 5 | "cfengine":"boxlang@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "JVM":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser":"false", 21 | "env":{}, 22 | "scripts":{ 23 | "onServerInitialInstall":"install bx-esapi --noSave" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server-lucee@5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-lucee@5", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee5", 5 | "cfengine":"lucee@5" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /server-lucee@6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-lucee@6", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee6", 5 | "cfengine":"lucee@6" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /server-lucee@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"qb-lucee@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/luceeBE", 5 | "cfengine":"lucee@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.enableNullSupport = shouldEnableFullNullSupport(); 4 | this.timezone = "UTC"; 5 | 6 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 7 | this.mappings[ "/qb" ] = expandPath( "/" ); 8 | this.mappings[ "/cbpaginator" ] = expandPath( "/modules/cbpaginator" ); 9 | this.mappings[ "/testbox" ] = this.mappings[ "/qb" ] & "/testbox"; 10 | 11 | function shouldEnableFullNullSupport() { 12 | var system = createObject( "java", "java.lang.System" ); 13 | var value = system.getEnv( "FULL_NULL" ); 14 | return isNull( value ) ? false : !!value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | // No cf debugging 3 | cfsetting( showdebugoutput="false" ); 4 | // GLOBAL VARIABLES 5 | ASSETS_DIR = expandPath( "/testbox/system/reports/assets" ); 6 | TESTBOX_VERSION = new testBox.system.TestBox().getVersion(); 7 | // TEST LOCATIONS -> UPDATE AS YOU SEE FIT 8 | rootMapping = "/tests/specs"; 9 | 10 | // Local Variables 11 | rootPath = expandPath( rootMapping ); 12 | targetPath = rootPath; 13 | 14 | // Incoming Navigation 15 | param name="url.path" default=""; 16 | if( len( url.path ) ){ 17 | targetPath = getCanonicalPath( rootpath & "/" & url.path ); 18 | // Avoid traversals, reset to root 19 | if( !findNoCase( rootpath, targetPath ) ){ 20 | targetPath = rootpath; 21 | } 22 | } 23 | 24 | // Get the actual execution path 25 | executePath = rootMapping & ( len( url.path ) ? "/#url.path#" : "/" ); 26 | // Execute an incoming path 27 | if( !isNull( url.action ) ){ 28 | if( directoryExists( targetPath ) ){ 29 | writeOutput( "#new testbox.system.TestBox( directory=executePath ).run()#" ); 30 | } else { 31 | writeOutput( "

Invalid Directory: #encodeForHTML( targetPath )#

" ); 32 | } 33 | abort; 34 | } 35 | 36 | // Get the tests to navigate 37 | qResults = directoryList( targetPath, false, "query", "", "name" ); 38 | 39 | // Calculate the back navigation path 40 | if( len( url.path ) ){ 41 | backPath = url.path.listToArray( "/\" ); 42 | backPath.pop(); 43 | backPath = backPath.toList( "/" ); 44 | } 45 |
46 | 47 | 48 | 49 | 50 | 51 | TestBox Browser 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 |
70 | v#TESTBOX_VERSION# 71 |
72 | 76 | 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 |

Availble Test Runners:

89 |

90 | Below is a listing of the runners matching the "runner*.(cfm|bxm)" pattern. 91 |

92 | 93 | 94 | 95 | 99 | class="btn btn-success btn-sm my-1 mx-1" 100 | 101 | class="btn btn-info btn-sm my-1 mx-1" 102 | 103 | > 104 | #runners.name# 105 | 106 | 107 |
108 |
109 | 110 | 111 |
112 |
113 |
114 | 115 |

TestBox Test Browser:

116 |

117 | Below is a listing of the files and folders starting from your root #rootMapping#. You can click on individual tests in order to execute them 118 | or click on the Run All button on your left and it will execute a directory runner from the visible folder. 119 |

120 | 121 |
122 | #targetPath.replace( rootPath, "" )# 123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 |
131 |
132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 146 | 150 | &##x271A; #qResults.name# 151 | 152 |
153 | 154 | 159 | #qResults.name# 160 | 161 |
162 | 165 | 167 | data-bx="true" 168 | class="btn btn-success btn-sm my-1" 169 | 170 | data-bx="false" 171 | class="btn btn-info btn-sm my-1" 172 |
173 | href="#executePath & "/" & qResults.name#?method=runRemote" 174 | target="_blank" 175 | > 176 | #qResults.name# 177 | 178 |
179 | 180 | 181 |
182 |
183 |
184 |
185 |
186 |
187 | 188 | 189 | 190 |
-------------------------------------------------------------------------------- /tests/profiling/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | this.mappings[ "/profiling" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 3 | this.mappings[ "/qb" ] = expandPath( "/" ); 4 | 5 | this.datasources[ "coolblog" ] = { 6 | class = "org.gjt.mm.mysql.Driver", 7 | connectionString = "jdbc:mysql://localhost:3306/coolblog?useUnicode=true&characterEncoding=UTF-8&useLegacyDatetimeCode=true", 8 | username = "root", 9 | password = "encrypted:58a6d180adc640d2364bb235a4003f49b9f133e14626fb0d" 10 | }; 11 | 12 | this.datasource = "coolblog"; 13 | } -------------------------------------------------------------------------------- /tests/profiling/resources/Profiler.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function beforeAll() { 4 | addMatchers( { 5 | toAverageLessThan = variables.toAverageLessThan 6 | } ); 7 | } 8 | 9 | function getAverageExecutionTime( callback, iterations = 1 ) { 10 | var threads = []; 11 | for ( var i = 1; i <= iterations; i++ ) { 12 | var threadName = "profiler-#createUUID()#"; 13 | arrayAppend( threads, threadName ); 14 | thread action="run" name="#threadName#" callback="#callback#" { 15 | try { 16 | attributes.callback(); 17 | } 18 | catch ( any e ) { 19 | thread.exception = e; 20 | } 21 | } 22 | } 23 | 24 | thread action="join" name="#threads.toList()#"; 25 | 26 | var times = []; 27 | for ( var threadName in threads ) { 28 | var threadInstance = cfthread[ threadName ]; 29 | 30 | if ( structKeyExists( threadInstance, "exception" ) ) { 31 | throw( argumentCollection = threadInstance.exception ); 32 | } 33 | 34 | arrayAppend( times, threadInstance.elapsedTime ); 35 | } 36 | 37 | return arrayAvg( times ); 38 | } 39 | 40 | function toAverageLessThan( expectation, args = {} ) { 41 | param args.ms = 0; 42 | param args.iterations = 1; 43 | param args.dumpOutTimes = false; 44 | 45 | args.ms = args [ 1 ]; 46 | 47 | var threads = []; 48 | for ( var i = 1; i <= args.iterations; i++ ) { 49 | var threadName = "profiler-#createUUID()#"; 50 | arrayAppend( threads, threadName ); 51 | thread action="run" name="#threadName#" expectation="#expectation#" { 52 | try { 53 | attributes.expectation.actual(); 54 | } 55 | catch ( any e ) { 56 | thread.exception = e; 57 | } 58 | } 59 | } 60 | 61 | thread action="join" name="#threads.toList()#"; 62 | 63 | var times = []; 64 | for ( var threadName in threads ) { 65 | var threadInstance = cfthread[ threadName ]; 66 | 67 | if ( structKeyExists( threadInstance, "exception" ) ) { 68 | expectation.message = "An exception was thrown: #serializeJSON( threadInstance.exception )#"; 69 | return false; 70 | } 71 | 72 | arrayAppend( times, threadInstance.elapsedTime ); 73 | } 74 | if ( args.dumpOutTimes ) { 75 | writeDump( var = times ); 76 | } 77 | var averageTime = arrayAvg( times ); 78 | 79 | expectation.message = "Average elapsed time [#averageTime#] is greater than allowed time [#args.ms#]."; 80 | return averageTime < args.ms; 81 | } 82 | 83 | function newMySQLBuilder() { 84 | return new models.Query.Builder( 85 | new models.Query.Grammars.MySQLGrammar() 86 | ); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /tests/profiling/runner-profiler.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/profiling/specs/qbProfile.cfc: -------------------------------------------------------------------------------- 1 | component extends="profiling.resources.Profiler" { 2 | 3 | function run() { 4 | describe( "qb profiling", function() { 5 | it( "simple query", function() { 6 | // var baseline = getAverageExecutionTime( function() { 7 | // queryExecute( "SELECT * FROM entries", {}, {} ); 8 | // }, 10 ); 9 | 10 | expect( function() { 11 | newMySQLBuilder() 12 | .from( "entries" ) 13 | .get(); 14 | } ).toAverageLessThan( ms = 50, iterations = 10 ); 15 | } ); 16 | 17 | it( "selecting columns", function() { 18 | // var baseline = getAverageExecutionTime( function() { 19 | // queryExecute( "SELECT title, entryBody FROM entries", {}, {} ); 20 | // }, 10 ); 21 | 22 | expect( function() { 23 | newMySQLBuilder() 24 | .select( [ "title", "entryBody" ] ) 25 | .from( "entries" ) 26 | .get(); 27 | } ).toAverageLessThan( ms = 50, iterations = 10 ); 28 | } ); 29 | 30 | it( "simple where", function() { 31 | // var baseline = getAverageExecutionTime( function() { 32 | // queryExecute( 33 | // "SELECT * FROM entries WHERE entry_id = ?", 34 | // [ "402881882814615e01282b14964d0016" ], 35 | // {} 36 | // ); 37 | // }, 10 ); 38 | 39 | expect( function() { 40 | newMySQLBuilder() 41 | .from( "entries" ) 42 | .where( "entry_id", "402881882814615e01282b14964d0016" ) 43 | .get(); 44 | } ).toAverageLessThan( ms = 50, iterations = 10 ); 45 | } ); 46 | 47 | it( "simple join", function() { 48 | var baseline = getAverageExecutionTime( function() { 49 | queryExecute( 50 | " 51 | SELECT 52 | e.entry_id, 53 | e.title, 54 | e.entryBody, 55 | e.postedDate 56 | FROM entries e 57 | JOIN entry_categories ec 58 | ON e.entry_id = ec.FKentry_id 59 | JOIN categories c 60 | ON c.category_id = ec.FKcategory_id 61 | WHERE c.category = ? 62 | ", 63 | [ "Presentations" ], 64 | {} 65 | ); 66 | }, 10 ); 67 | 68 | expect( function() { 69 | newMySQLBuilder() 70 | .select( [ 71 | "entry_id", 72 | "title", 73 | "entryBody", 74 | "postedDate" 75 | ] ) 76 | .from( "entries e" ) 77 | .join( "entry_categories ec", function( j ) { 78 | j.on( "e.entry_id", "ec.FKentry_id" ); 79 | } ) 80 | .join( "categories c", function( j ) { 81 | j.on( "c.category_id", "ec.FKcategory_id" ); 82 | } ) 83 | .where( "c.category", "Presentations" ) 84 | .get(); 85 | } ).toAverageLessThan( ms = 50, iterations = 10 ); 86 | } ); 87 | } ); 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /tests/runner.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderAliasSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "builder alias", () => { 5 | describe( "columns", () => { 6 | it( "it renames aliases in the select clause", () => { 7 | var qb = new qb.models.Query.QueryBuilder(); 8 | qb.from( "users AS u" ).select( [ "u.id", "u.name" ] ); 9 | expect( qb.getColumns() ).toBe( [ "u.id", "u.name" ] ); 10 | qb.withAlias( "u1" ); 11 | expect( qb.getColumns() ).toBe( [ "u1.id", "u1.name" ] ); 12 | } ); 13 | 14 | it( "renames the base table name if used in column declarations", () => { 15 | var qb = new qb.models.Query.QueryBuilder(); 16 | qb.from( "users" ).select( [ "users.id", "users.name" ] ); 17 | expect( qb.getColumns() ).toBe( [ "users.id", "users.name" ] ); 18 | qb.withAlias( "u" ); 19 | expect( qb.getColumns() ).toBe( [ "u.id", "u.name" ] ); 20 | } ); 21 | 22 | it( "renames aliases with multiple periods", () => { 23 | var qb = new qb.models.Query.QueryBuilder(); 24 | qb.from( "MyServer.dbo.users" ).select( [ "MyServer.dbo.users.id", "MyServer.dbo.users.name" ] ); 25 | expect( qb.getColumns() ).toBe( [ "MyServer.dbo.users.id", "MyServer.dbo.users.name" ] ); 26 | qb.withAlias( "u1" ); 27 | expect( qb.getColumns() ).toBe( [ "u1.id", "u1.name" ] ); 28 | } ); 29 | 30 | it( "renames aliases with schema shortcut periods", () => { 31 | var qb = new qb.models.Query.QueryBuilder(); 32 | qb.from( "MyServer..users" ).select( [ "MyServer..users.id", "MyServer..users.name" ] ); 33 | expect( qb.getColumns() ).toBe( [ "MyServer..users.id", "MyServer..users.name" ] ); 34 | qb.withAlias( "u1" ); 35 | expect( qb.getColumns() ).toBe( [ "u1.id", "u1.name" ] ); 36 | } ); 37 | } ); 38 | 39 | describe( "wheres", () => { 40 | it( "renames the columns used in where basic clauses", () => { 41 | var qb = new qb.models.Query.QueryBuilder(); 42 | qb.from( "users" ).where( "users.isActive", 1 ); 43 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""isActive"" = ?" ); 44 | qb.withAlias( "u" ); 45 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""isActive"" = ?" ); 46 | } ); 47 | 48 | it( "renames the columns used in where column clauses", () => { 49 | var qb = new qb.models.Query.QueryBuilder(); 50 | qb.from( "users" ).whereColumn( "users.createdDate", "users.modifiedDate" ); 51 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""createdDate"" = ""users"".""modifiedDate""" ); 52 | qb.withAlias( "u" ); 53 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""createdDate"" = ""u"".""modifiedDate""" ); 54 | } ); 55 | 56 | it( "renames the columns used in where sub clauses", () => { 57 | var qb = new qb.models.Query.QueryBuilder(); 58 | 59 | qb.from( "users u1" ) 60 | .where( "u1.email", "foo" ) 61 | .orWhere( 62 | "u1.id", 63 | "=", 64 | function( q ) { 65 | q.select( q.raw( "MAX(id)" ) ) 66 | .from( "users u2" ) 67 | .where( "u2.email", "bar" ) 68 | .whereColumn( "u1.email", "<>", "u2.email" ); 69 | } 70 | ); 71 | 72 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u1"" WHERE ""u1"".""email"" = ? OR ""u1"".""id"" = (SELECT MAX(id) FROM ""users"" AS ""u2"" WHERE ""u2"".""email"" = ? AND ""u1"".""email"" <> ""u2"".""email"")" ); 73 | 74 | qb.withAlias( "u3" ); 75 | 76 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u3"" WHERE ""u3"".""email"" = ? OR ""u3"".""id"" = (SELECT MAX(id) FROM ""users"" AS ""u2"" WHERE ""u2"".""email"" = ? AND ""u3"".""email"" <> ""u2"".""email"")" ); 77 | } ); 78 | 79 | it( "renames the columns used in where in clauses", () => { 80 | var qb = new qb.models.Query.QueryBuilder(); 81 | qb.from( "users" ).whereIn( "users.id", [ 1, 2, 3 ] ); 82 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""id"" IN (?, ?, ?)" ); 83 | qb.withAlias( "u" ); 84 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""id"" IN (?, ?, ?)" ); 85 | } ); 86 | 87 | it( "renames the columns used in where not in clauses", () => { 88 | var qb = new qb.models.Query.QueryBuilder(); 89 | qb.from( "users" ).whereNotIn( "users.id", [ 1, 2, 3 ] ); 90 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""id"" NOT IN (?, ?, ?)" ); 91 | qb.withAlias( "u" ); 92 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""id"" NOT IN (?, ?, ?)" ); 93 | } ); 94 | 95 | it( "renames the columns used in where exists clauses", () => { 96 | var qb = new qb.models.Query.QueryBuilder(); 97 | qb.from( "users" ) 98 | .whereExists( ( q ) => { 99 | return q 100 | .selectRaw( 1 ) 101 | .from( "logins" ) 102 | .whereColumn( "logins.userId", "users.id" ) 103 | .andWhere( "logins.createdDate", ">=", "2024-03-15 00:00:00" ); 104 | } ); 105 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE EXISTS (SELECT 1 FROM ""logins"" WHERE ""logins"".""userId"" = ""users"".""id"" AND ""logins"".""createdDate"" >= ?)" ); 106 | qb.withAlias( "u" ); 107 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE EXISTS (SELECT 1 FROM ""logins"" WHERE ""logins"".""userId"" = ""u"".""id"" AND ""logins"".""createdDate"" >= ?)" ); 108 | } ); 109 | 110 | it( "renames the columns used in where not exists clauses", () => { 111 | var qb = new qb.models.Query.QueryBuilder(); 112 | qb.from( "users" ) 113 | .whereNotExists( ( q ) => { 114 | return q 115 | .selectRaw( 1 ) 116 | .from( "logins" ) 117 | .whereColumn( "logins.userId", "users.id" ) 118 | .andWhere( "logins.createdDate", ">=", "2024-03-15 00:00:00" ); 119 | } ); 120 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE NOT EXISTS (SELECT 1 FROM ""logins"" WHERE ""logins"".""userId"" = ""users"".""id"" AND ""logins"".""createdDate"" >= ?)" ); 121 | qb.withAlias( "u" ); 122 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE NOT EXISTS (SELECT 1 FROM ""logins"" WHERE ""logins"".""userId"" = ""u"".""id"" AND ""logins"".""createdDate"" >= ?)" ); 123 | } ); 124 | 125 | it( "renames the columns used in nested where clauses", () => { 126 | var qb = new qb.models.Query.QueryBuilder(); 127 | qb.from( "users" ) 128 | .where( ( q ) => { 129 | q.where( "users.isActive", 1 ); 130 | q.andWhere( "users.isConfirmed", 1 ); 131 | } ); 132 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE (""users"".""isActive"" = ? AND ""users"".""isConfirmed"" = ?)" ); 133 | qb.withAlias( "u" ); 134 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE (""u"".""isActive"" = ? AND ""u"".""isConfirmed"" = ?)" ); 135 | } ); 136 | 137 | it( "renames the columns used in where null clauses", () => { 138 | var qb = new qb.models.Query.QueryBuilder(); 139 | qb.from( "users" ).whereNull( "users.canceledDate" ); 140 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""canceledDate"" IS NULL" ); 141 | qb.withAlias( "u" ); 142 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""canceledDate"" IS NULL" ); 143 | } ); 144 | 145 | it( "renames the columns used in where not null clauses", () => { 146 | var qb = new qb.models.Query.QueryBuilder(); 147 | qb.from( "users" ).whereNotNull( "users.canceledDate" ); 148 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""canceledDate"" IS NOT NULL" ); 149 | qb.withAlias( "u" ); 150 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""canceledDate"" IS NOT NULL" ); 151 | } ); 152 | 153 | it( "renames the columns used in where null sub clauses", () => { 154 | var qb = new qb.models.Query.QueryBuilder(); 155 | qb.from( "users" ) 156 | .whereNull( function( q ) { 157 | q.selectRaw( "MAX(created_date)" ) 158 | .from( "logins" ) 159 | .whereColumn( "logins.user_id", "users.id" ); 160 | } ); 161 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE (SELECT MAX(created_date) FROM ""logins"" WHERE ""logins"".""user_id"" = ""users"".""id"") IS NULL" ); 162 | qb.withAlias( "u" ); 163 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE (SELECT MAX(created_date) FROM ""logins"" WHERE ""logins"".""user_id"" = ""u"".""id"") IS NULL" ); 164 | } ); 165 | 166 | it( "renames the columns used in where not null sub clauses", () => { 167 | var qb = new qb.models.Query.QueryBuilder(); 168 | qb.from( "users" ) 169 | .whereNotNull( function( q ) { 170 | q.selectRaw( "MAX(created_date)" ) 171 | .from( "logins" ) 172 | .whereColumn( "logins.user_id", "users.id" ); 173 | } ); 174 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE (SELECT MAX(created_date) FROM ""logins"" WHERE ""logins"".""user_id"" = ""users"".""id"") IS NOT NULL" ); 175 | qb.withAlias( "u" ); 176 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE (SELECT MAX(created_date) FROM ""logins"" WHERE ""logins"".""user_id"" = ""u"".""id"") IS NOT NULL" ); 177 | } ); 178 | 179 | it( "renames the columns used in where between clauses", () => { 180 | var qb = new qb.models.Query.QueryBuilder(); 181 | qb.from( "users" ) 182 | .whereBetween( "users.lastLoginDate", "2024-02-15 00:00:00", "2024-03-14 23:59:59" ); 183 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""lastLoginDate"" BETWEEN ? AND ?" ); 184 | qb.withAlias( "u" ); 185 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""lastLoginDate"" BETWEEN ? AND ?" ); 186 | } ); 187 | 188 | it( "renames the columns used in where not between clauses", () => { 189 | var qb = new qb.models.Query.QueryBuilder(); 190 | qb.from( "users" ) 191 | .whereNotBetween( "users.lastLoginDate", "2024-02-15 00:00:00", "2024-03-14 23:59:59" ); 192 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""users"".""lastLoginDate"" NOT BETWEEN ? AND ?" ); 193 | qb.withAlias( "u" ); 194 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" WHERE ""u"".""lastLoginDate"" NOT BETWEEN ? AND ?" ); 195 | } ); 196 | } ); 197 | 198 | describe( "joins", () => { 199 | it( "renames the columns used in join clauses", () => { 200 | var qb = new qb.models.Query.QueryBuilder(); 201 | qb.from( "users" ) 202 | .join( "contacts", "users.id", "contacts.id" ) 203 | .join( "addresses AS a", "a.contact_id", "contacts.id" ) 204 | .leftJoin( "logins", ( j ) => { 205 | j.on( "logins.user_id", "users.id" ); 206 | } ); 207 | 208 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" INNER JOIN ""contacts"" ON ""users"".""id"" = ""contacts"".""id"" INNER JOIN ""addresses"" AS ""a"" ON ""a"".""contact_id"" = ""contacts"".""id"" LEFT JOIN ""logins"" ON ""logins"".""user_id"" = ""users"".""id""" ); 209 | qb.withAlias( "u" ); 210 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" INNER JOIN ""contacts"" ON ""u"".""id"" = ""contacts"".""id"" INNER JOIN ""addresses"" AS ""a"" ON ""a"".""contact_id"" = ""contacts"".""id"" LEFT JOIN ""logins"" ON ""logins"".""user_id"" = ""u"".""id""" ); 211 | } ); 212 | } ); 213 | 214 | describe( "groups", () => { 215 | it( "renames the columns used in group clauses", () => { 216 | var qb = new qb.models.Query.QueryBuilder(); 217 | qb.from( "logins" ) 218 | .select( "userId" ) 219 | .selectRaw( "MAX(createdDate) AS lastLoginDate" ) 220 | .groupBy( "logins.userId" ); 221 | 222 | expect( qb.toSQL() ).toBe( "SELECT ""userId"", MAX(createdDate) AS lastLoginDate FROM ""logins"" GROUP BY ""logins"".""userId""" ); 223 | qb.withAlias( "l" ); 224 | expect( qb.toSQL() ).toBe( "SELECT ""userId"", MAX(createdDate) AS lastLoginDate FROM ""logins"" AS ""l"" GROUP BY ""l"".""userId""" ); 225 | } ); 226 | } ); 227 | 228 | describe( "orders", () => { 229 | it( "renames the columns used in orderBy clauses", () => { 230 | var qb = new qb.models.Query.QueryBuilder(); 231 | qb.from( "users" ).orderByDesc( "users.lastLoginDate" ); 232 | 233 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" ORDER BY ""users"".""lastLoginDate"" DESC" ); 234 | qb.withAlias( "u" ); 235 | expect( qb.toSQL() ).toBe( "SELECT * FROM ""users"" AS ""u"" ORDER BY ""u"".""lastLoginDate"" DESC" ); 236 | } ); 237 | } ); 238 | } ); 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderColumnCallbackSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "column callback spec", function() { 5 | beforeEach( function() { 6 | variables.query = new qb.models.Query.QueryBuilder(); 7 | getMockBox().prepareMock( query ); 8 | query.$property( propertyName = "utils", mock = new qb.models.Query.QueryUtils() ); 9 | } ); 10 | 11 | it( "does nothing by default", function() { 12 | query.from( "users" ).where( "firstName", "=", "firstName" ); 13 | expect( query.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""firstName"" = ?" ); 14 | expect( query.getBindings()[ 1 ].value ).toBe( "firstName" ); 15 | } ); 16 | 17 | it( "can set a column formatter to be called for each column used", function() { 18 | query.setColumnFormatter( function( column ) { 19 | return reverse( column ); 20 | } ); 21 | query.from( "users" ).where( "firstName", "=", "firstName" ); 22 | expect( query.toSQL() ).toBe( "SELECT * FROM ""users"" WHERE ""emaNtsrif"" = ?" ); 23 | expect( query.getBindings()[ 1 ].value ).toBe( "firstName" ); 24 | } ); 25 | } ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderGetSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "get methods", function() { 5 | beforeEach( function() { 6 | variables.qb = new qb.models.Query.QueryBuilder(); 7 | getMockBox().prepareMock( qb ); 8 | 9 | var utils = new qb.models.Query.QueryUtils(); 10 | qb.$property( propertyName = "utils", mock = utils ); 11 | 12 | var mockWirebox = getMockBox().createStub(); 13 | var mockJoinClause = getMockBox().prepareMock( new qb.models.Query.JoinClause( qb, "inner", "second" ) ); 14 | mockJoinClause.$property( propertyName = "utils", mock = utils ); 15 | mockWirebox 16 | .$( "getInstance" ) 17 | .$args( 18 | name = "JoinClause@Quick", 19 | initArguments = { parentQuery: qb, type: "inner", table: "second" } 20 | ) 21 | .$results( mockJoinClause ); 22 | qb.$property( propertyName = "wirebox", mock = mockWirebox ); 23 | } ); 24 | 25 | it( "retreives bindings in a flat array", function() { 26 | qb.join( "second", function( join ) { 27 | join.where( "second.locale", "=", "en-US" ); 28 | } ) 29 | .where( "first.quantity", ">=", 10 ); 30 | 31 | var bindings = qb.getBindings(); 32 | expect( bindings ).toBeArray(); 33 | expect( arrayLen( bindings ) ).toBe( 2, "2 bindings should exist" ); 34 | var binding = bindings[ 1 ]; 35 | expect( binding.value ).toBe( "en-US" ); 36 | expect( binding.cfsqltype ).toBe( "varchar" ); 37 | var binding = bindings[ 2 ]; 38 | expect( binding.value ).toBe( 10 ); 39 | expect( binding.cfsqltype ).toBe( "INTEGER" ); 40 | } ); 41 | 42 | it( "retreives a map of bindings", function() { 43 | qb.join( "second", function( join ) { 44 | join.where( "second.locale", "=", "en-US" ); 45 | } ) 46 | .where( "first.quantity", ">=", "10" ); 47 | 48 | var bindings = qb.getRawBindings(); 49 | 50 | expect( bindings ).toBeStruct(); 51 | } ); 52 | } ); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderJoinSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "join methods", function() { 5 | beforeEach( function() { 6 | variables.query = new qb.models.Query.QueryBuilder(); 7 | getMockBox().prepareMock( query ); 8 | variables.utils = new qb.models.Query.QueryUtils(); 9 | query.$property( propertyName = "utils", mock = utils ); 10 | var mockJoinClause = getMockBox().prepareMock( 11 | new qb.models.Query.JoinClause( query, "inner", "second" ) 12 | ); 13 | mockJoinClause.$property( propertyName = "utils", mock = utils ); 14 | } ); 15 | 16 | it( "does a simple inner join", function() { 17 | var mockJoinClause = getMockBox().prepareMock( 18 | new qb.models.Query.JoinClause( query, "inner", "second" ) 19 | ); 20 | mockJoinClause.$property( propertyName = "utils", mock = utils ); 21 | 22 | query.join( 23 | "second", 24 | "first.id", 25 | "=", 26 | "second.first_id" 27 | ); 28 | 29 | var joins = query.getJoins(); 30 | expect( arrayLen( joins ) ).toBe( 1, "Only one join should exist" ); 31 | 32 | var join = joins[ 1 ]; 33 | expect( join ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 34 | expect( join.getType() ).toBe( "inner" ); 35 | expect( join.getTable() ).toBe( "second" ); 36 | 37 | var clauses = join.getWheres(); 38 | expect( arrayLen( clauses ) ).toBe( 1, "Only one join clause should exist" ); 39 | 40 | var clause = clauses[ 1 ]; 41 | expect( clause ).toBeStruct(); 42 | expect( clause.first ).toBe( "first.id", "First column should be [first.id]" ); 43 | expect( clause.operator ).toBe( "=", "Operator should be [=]" ); 44 | expect( clause.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 45 | expect( clause.combinator ).toBe( "and" ); 46 | } ); 47 | 48 | it( "does a left join", function() { 49 | var mockJoinClause = getMockBox().prepareMock( 50 | new qb.models.Query.JoinClause( query, "left", "second" ) 51 | ); 52 | mockJoinClause.$property( propertyName = "utils", mock = utils ); 53 | 54 | query.leftJoin( 55 | "second", 56 | "first.id", 57 | "=", 58 | "second.first_id" 59 | ); 60 | 61 | var joins = query.getJoins(); 62 | expect( arrayLen( joins ) ).toBe( 1, "Only one join should exist" ); 63 | 64 | var join = joins[ 1 ]; 65 | expect( join ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 66 | expect( join.getType() ).toBe( "left" ); 67 | expect( join.getTable() ).toBe( "second" ); 68 | 69 | var clauses = join.getWheres(); 70 | expect( arrayLen( clauses ) ).toBe( 1, "Only one join clause should exist" ); 71 | 72 | var clause = clauses[ 1 ]; 73 | expect( clause ).toBeStruct(); 74 | expect( clause.first ).toBe( "first.id", "First column should be [first.id]" ); 75 | expect( clause.operator ).toBe( "=", "Operator should be [=]" ); 76 | expect( clause.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 77 | expect( clause.combinator ).toBe( "and" ); 78 | } ); 79 | 80 | it( "does a right join", function() { 81 | var mockJoinClause = getMockBox().prepareMock( 82 | new qb.models.Query.JoinClause( query, "right", "second" ) 83 | ); 84 | mockJoinClause.$property( propertyName = "utils", mock = utils ); 85 | 86 | query.rightJoin( 87 | "second", 88 | "first.id", 89 | "=", 90 | "second.first_id" 91 | ); 92 | 93 | var joins = query.getJoins(); 94 | expect( arrayLen( joins ) ).toBe( 1, "Only one join should exist" ); 95 | 96 | var join = joins[ 1 ]; 97 | expect( join ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 98 | expect( join.getType() ).toBe( "right" ); 99 | expect( join.getTable() ).toBe( "second" ); 100 | 101 | var clauses = join.getWheres(); 102 | expect( arrayLen( clauses ) ).toBe( 1, "Only one join clause should exist" ); 103 | 104 | var clause = clauses[ 1 ]; 105 | expect( clause ).toBeStruct(); 106 | expect( clause.first ).toBe( "first.id", "First column should be [first.id]" ); 107 | expect( clause.operator ).toBe( "=", "Operator should be [=]" ); 108 | expect( clause.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 109 | expect( clause.combinator ).toBe( "and" ); 110 | } ); 111 | 112 | it( "can use a callback to specify advanced join clauses", function() { 113 | query.join( "second", function( join ) { 114 | join.on( "first.id", "=", "second.first_id" ).on( "first.locale", "=", "second.locale" ); 115 | } ); 116 | 117 | var joins = query.getJoins(); 118 | expect( arrayLen( joins ) ).toBe( 1, "Only one join should exist" ); 119 | 120 | var join = joins[ 1 ]; 121 | expect( join ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 122 | expect( join.getType() ).toBe( "inner" ); 123 | expect( join.getTable() ).toBe( "second" ); 124 | 125 | var clauses = join.getWheres(); 126 | expect( arrayLen( clauses ) ).toBe( 2, "Two join clauses should exist" ); 127 | 128 | var clauseOne = clauses[ 1 ]; 129 | expect( clauseOne ).toBeStruct(); 130 | expect( clauseOne.first ).toBe( "first.id", "First column should be [first.id]" ); 131 | expect( clauseOne.operator ).toBe( "=", "Operator should be [=]" ); 132 | expect( clauseOne.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 133 | expect( clauseOne.combinator ).toBe( "and" ); 134 | 135 | var clauseTwo = clauses[ 2 ]; 136 | expect( clauseTwo ).toBeStruct(); 137 | expect( clauseTwo.first ).toBe( "first.locale", "First column should be [first.locale]" ); 138 | expect( clauseTwo.operator ).toBe( "=", "Operator should be [=]" ); 139 | expect( clauseTwo.second ).toBe( "second.locale", "First column should be [second.locale]" ); 140 | expect( clauseTwo.combinator ).toBe( "and" ); 141 | } ); 142 | 143 | it( "can pass the callback as the second parameter when using positional parameters", function() { 144 | query.join( "second", function( join ) { 145 | join.on( "first.id", "=", "second.first_id" ).on( "first.locale", "=", "second.locale" ); 146 | } ); 147 | 148 | var joins = query.getJoins(); 149 | expect( arrayLen( joins ) ).toBe( 1, "Only one join should exist" ); 150 | 151 | var join = joins[ 1 ]; 152 | expect( join ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 153 | expect( join.getType() ).toBe( "inner" ); 154 | expect( join.getTable() ).toBe( "second" ); 155 | 156 | var clauses = join.getWheres(); 157 | expect( arrayLen( clauses ) ).toBe( 2, "Two join clauses should exist" ); 158 | 159 | var clauseOne = clauses[ 1 ]; 160 | expect( clauseOne ).toBeStruct(); 161 | expect( clauseOne.first ).toBe( "first.id", "First column should be [first.id]" ); 162 | expect( clauseOne.operator ).toBe( "=", "Operator should be [=]" ); 163 | expect( clauseOne.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 164 | expect( clauseOne.combinator ).toBe( "and" ); 165 | 166 | var clauseTwo = clauses[ 2 ]; 167 | expect( clauseTwo ).toBeStruct(); 168 | expect( clauseTwo.first ).toBe( "first.locale", "First column should be [first.locale]" ); 169 | expect( clauseTwo.operator ).toBe( "=", "Operator should be [=]" ); 170 | expect( clauseTwo.second ).toBe( "second.locale", "First column should be [second.locale]" ); 171 | expect( clauseTwo.combinator ).toBe( "and" ); 172 | } ); 173 | 174 | it( "adds the join bindings to the builder bindings", function() { 175 | query.join( "second", function( join ) { 176 | join.where( "second.locale", "=", "en-US" ); 177 | } ); 178 | 179 | var bindings = query.getRawBindings().join; 180 | expect( bindings ).toBeArray(); 181 | expect( arrayLen( bindings ) ).toBe( 1, "1 binding should exist" ); 182 | var binding = bindings[ 1 ]; 183 | expect( binding.value ).toBe( "en-US" ); 184 | expect( binding.cfsqltype ).toBe( "varchar" ); 185 | } ); 186 | } ); 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderSelectSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "select methods", function() { 5 | beforeEach( function() { 6 | variables.mockGrammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ); 7 | variables.query = new qb.models.Query.QueryBuilder( variables.mockGrammar ); 8 | } ); 9 | 10 | describe( "select()", function() { 11 | it( "defaults to all columns", function() { 12 | expect( query.getColumns() ).toBe( [ "*" ] ); 13 | } ); 14 | 15 | it( "can specify a single column from a query", function() { 16 | query.select( "::some_column::" ); 17 | expect( query.getColumns() ).toBe( [ "::some_column::" ] ); 18 | } ); 19 | 20 | describe( "can specify multiple columns in a query", function() { 21 | it( "using a list", function() { 22 | query.select( "::some_column::, ::another_column::" ); 23 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::" ] ); 24 | } ); 25 | 26 | it( "trims a list before splitting it", function() { 27 | query.select( 28 | " 29 | ::some_column::, ::another_column:: 30 | ,::third_column:: 31 | " 32 | ); 33 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::", "::third_column::" ] ); 34 | } ); 35 | 36 | it( "using an array", function() { 37 | query.select( [ "::some_column::", "::another_column::" ] ); 38 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::" ] ); 39 | } ); 40 | } ); 41 | } ); 42 | 43 | describe( "addSelect()", function() { 44 | beforeEach( function() { 45 | query.select( "::some_column::" ); 46 | expect( query.getColumns() ).toBe( [ "::some_column::" ] ); 47 | } ); 48 | 49 | it( "can add a single column to an existing query", function() { 50 | query.addSelect( "::another_column::" ); 51 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::" ] ); 52 | } ); 53 | 54 | describe( "can add multiple columns to an existing query", function() { 55 | it( "using a list", function() { 56 | query.addSelect( "::another_column::, ::yet_another_column::" ); 57 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::", "::yet_another_column::" ] ); 58 | } ); 59 | 60 | it( "using an array", function() { 61 | query.addSelect( [ "::another_column::", "::yet_another_column::" ] ); 62 | expect( query.getColumns() ).toBe( [ "::some_column::", "::another_column::", "::yet_another_column::" ] ); 63 | } ); 64 | } ); 65 | } ); 66 | 67 | describe( "distinct()", function() { 68 | it( "sets the distinct flag", function() { 69 | expect( query.getDistinct() ).toBe( false, "Queries are not distinct by default" ); 70 | 71 | query.distinct(); 72 | 73 | expect( query.getDistinct() ).toBe( true, "Distinct should be set to true" ); 74 | } ); 75 | } ); 76 | } ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/BuilderWhereSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "where methods", function() { 5 | beforeEach( function() { 6 | variables.query = new qb.models.Query.QueryBuilder(); 7 | getMockBox().prepareMock( query ); 8 | query.$property( propertyName = "utils", mock = new qb.models.Query.QueryUtils() ); 9 | } ); 10 | 11 | it( "defaults to empty", function() { 12 | expect( query.getWheres() ).toBeEmpty( "Default `wheres` should be empty." ); 13 | } ); 14 | 15 | describe( "where", function() { 16 | it( "specifices a where clause", function() { 17 | query.where( "::some column::", "=", "::some value::" ); 18 | expect( query.getWheres() ).toBe( [ 19 | { 20 | column: "::some column::", 21 | operator: "=", 22 | value: "::some value::", 23 | combinator: "and", 24 | type: "basic" 25 | } 26 | ] ); 27 | } ); 28 | 29 | it( "only infers the = when only two arguments", function() { 30 | query.where( "::some column::", "::some value::" ); 31 | expect( query.getWheres() ).toBe( [ 32 | { 33 | column: "::some column::", 34 | operator: "=", 35 | value: "::some value::", 36 | combinator: "and", 37 | type: "basic" 38 | } 39 | ] ); 40 | } ); 41 | 42 | it( "can be specify the boolean combinator", function() { 43 | query 44 | .where( "::some column::", "=", "::some value::" ) 45 | .where( 46 | "::another column::", 47 | "=", 48 | "::another value::", 49 | "or" 50 | ); 51 | expect( query.getWheres() ).toBe( [ 52 | { 53 | column: "::some column::", 54 | operator: "=", 55 | value: "::some value::", 56 | combinator: "and", 57 | type: "basic" 58 | }, 59 | { 60 | column: "::another column::", 61 | operator: "=", 62 | value: "::another value::", 63 | combinator: "or", 64 | type: "basic" 65 | } 66 | ] ); 67 | } ); 68 | 69 | describe( "specialized where methods", function() { 70 | it( "has a whereIn shortcut", function() { 71 | query.whereIn( "::some column::", [ "::value one::", "::value two::" ] ); 72 | 73 | var wheres = query.getWheres(); 74 | expect( wheres ).toBeArray(); 75 | expect( arrayLen( wheres ) ).toBe( 1, "1 where clause should exist" ); 76 | var where = wheres[ 1 ]; 77 | expect( where.column ).toBe( "::some column::" ); 78 | expect( where.values ).toBe( [ "::value one::", "::value two::" ] ); 79 | expect( where.combinator ).toBe( "and" ); 80 | expect( where.type ).toBe( "in" ); 81 | } ); 82 | 83 | it( "has a whereNotIn shortcut", function() { 84 | query.whereNotIn( "::some column::", [ "::value one::", "::value two::" ] ); 85 | 86 | var wheres = query.getWheres(); 87 | expect( wheres ).toBeArray(); 88 | expect( arrayLen( wheres ) ).toBe( 1, "1 where clause should exist" ); 89 | var where = wheres[ 1 ]; 90 | expect( where.column ).toBe( "::some column::" ); 91 | expect( where.values ).toBe( [ "::value one::", "::value two::" ] ); 92 | expect( where.combinator ).toBe( "and" ); 93 | expect( where.type ).toBe( "notIn" ); 94 | } ); 95 | 96 | it( "has a orWhere shortcut", function() { 97 | query.orWhere( "::some column::", "<>", "::some value::" ); 98 | 99 | var wheres = query.getWheres(); 100 | expect( wheres ).toBeArray(); 101 | expect( arrayLen( wheres ) ).toBe( 1, "1 where clause should exist" ); 102 | var where = wheres[ 1 ]; 103 | expect( where.column ).toBe( "::some column::" ); 104 | expect( where.operator ).toBe( "<>" ); 105 | expect( where.value ).toBe( "::some value::" ); 106 | expect( where.combinator ).toBe( "or" ); 107 | expect( where.type ).toBe( "basic" ); 108 | } ); 109 | } ); 110 | 111 | describe( "bindings", function() { 112 | it( "adds the bindings for where statements received", function() { 113 | query.where( "::some column::", "=", "::some value::" ); 114 | 115 | var bindings = query.getRawBindings().where; 116 | expect( bindings ).toBeArray(); 117 | expect( arrayLen( bindings ) ).toBe( 1, "1 binding should exist" ); 118 | var binding = bindings[ 1 ]; 119 | expect( binding.value ).toBe( "::some value::" ); 120 | expect( binding.cfsqltype ).toBe( "varchar" ); 121 | } ); 122 | } ); 123 | 124 | describe( "dynamic where statements", function() { 125 | it( "translates whereColumn in to where(""column""", function() { 126 | query.whereSomeColumn( "::some value::" ); 127 | 128 | expect( query.getWheres() ).toBe( [ 129 | { 130 | column: "somecolumn", 131 | operator: "=", 132 | value: "::some value::", 133 | combinator: "and", 134 | type: "basic" 135 | } 136 | ] ); 137 | } ); 138 | 139 | it( "also translates orWhereColumn in to orWhere(""column""", function() { 140 | query.orWhereSomeColumn( "::some value::" ); 141 | 142 | expect( query.getWheres() ).toBe( [ 143 | { 144 | column: "somecolumn", 145 | operator: "=", 146 | value: "::some value::", 147 | combinator: "or", 148 | type: "basic" 149 | } 150 | ] ); 151 | } ); 152 | 153 | it( "returns the query instance to continue chaining", function() { 154 | var q = query.whereSomeColumn( "::some value::" ); 155 | expect( q ).toBeInstanceOf( "QueryBuilder" ); 156 | } ); 157 | } ); 158 | 159 | describe( "operators", function() { 160 | it( "throws an exception on illegal operators", function() { 161 | expect( function() { 162 | query.where( "::some column::", "::invalid operator::", "::some value::" ); 163 | } ).toThrow( type = "InvalidSQLType", regex = "Illegal operator" ); 164 | } ); 165 | } ); 166 | } ); 167 | } ); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/ControlFlowSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "control flow", function() { 5 | describe( "when", function() { 6 | it( "executes the callback when the condition is true", function() { 7 | testCase( function( builder ) { 8 | builder 9 | .from( "users" ) 10 | .when( true, function( query ) { 11 | query.where( "id", "=", 1 ); 12 | } ) 13 | .where( "email", "foo" ); 14 | }, { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? AND ""email"" = ?", bindings: [ 1, "foo" ] } ); 15 | } ); 16 | 17 | it( "does not execute the callback when the condition is false", function() { 18 | testCase( function( builder ) { 19 | builder 20 | .from( "users" ) 21 | .when( false, function( query ) { 22 | query.where( "id", "=", 1 ); 23 | } ) 24 | .where( "email", "foo" ); 25 | }, { sql: "SELECT * FROM ""users"" WHERE ""email"" = ?", bindings: [ "foo" ] } ); 26 | } ); 27 | 28 | it( "executes the default callback when the condition is false", function() { 29 | testCase( function( builder ) { 30 | builder 31 | .select( "*" ) 32 | .from( "users" ) 33 | .when( 34 | false, 35 | function( query ) { 36 | query.where( "id", "=", 1 ); 37 | }, 38 | function( query ) { 39 | query.where( "id", "=", 2 ); 40 | } 41 | ) 42 | .where( "email", "foo" ); 43 | }, { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? AND ""email"" = ?", bindings: [ 2, "foo" ] } ); 44 | } ); 45 | 46 | it( "does not execute the default callback when the condition is true", function() { 47 | testCase( function( builder ) { 48 | builder 49 | .select( "*" ) 50 | .from( "users" ) 51 | .when( 52 | true, 53 | function( query ) { 54 | query.where( "id", "=", 1 ); 55 | }, 56 | function( query ) { 57 | query.where( "id", "=", 2 ); 58 | } 59 | ) 60 | .where( "email", "foo" ); 61 | }, { sql: "SELECT * FROM ""users"" WHERE ""id"" = ? AND ""email"" = ?", bindings: [ 1, "foo" ] } ); 62 | } ); 63 | 64 | it( "wraps the wheres if an OR combinator is used inside the callback", function() { 65 | testCase( 66 | function( builder ) { 67 | builder 68 | .select( "*" ) 69 | .from( "users" ) 70 | .where( "email", "foo" ) 71 | .when( true, function( query ) { 72 | query.where( "id", "=", 1 ).orWhere( "id", "=", 2 ); 73 | } ); 74 | }, 75 | { 76 | sql: "SELECT * FROM ""users"" WHERE ""email"" = ? AND (""id"" = ? OR ""id"" = ?)", 77 | bindings: [ "foo", 1, 2 ] 78 | } 79 | ); 80 | } ); 81 | 82 | it( "can skip the wrapping of wheres if an OR combinator is used inside the callback", function() { 83 | testCase( 84 | function( builder ) { 85 | builder 86 | .select( "*" ) 87 | .from( "users" ) 88 | .where( "email", "foo" ) 89 | .when( 90 | condition = true, 91 | onTrue = function( query ) { 92 | query.where( "id", "=", 1 ).orWhere( "id", "=", 2 ); 93 | }, 94 | withoutScoping = true 95 | ); 96 | }, 97 | { 98 | sql: "SELECT * FROM ""users"" WHERE ""email"" = ? AND ""id"" = ? OR ""id"" = ?", 99 | bindings: [ "foo", 1, 2 ] 100 | } 101 | ); 102 | } ); 103 | 104 | it( "does not double wrap the wheres if an the wheres are already wrapped inside the callback", function() { 105 | testCase( 106 | function( builder ) { 107 | builder 108 | .select( "*" ) 109 | .from( "users" ) 110 | .where( "email", "foo" ) 111 | .when( true, function( query ) { 112 | query.where( function( q2 ) { 113 | q2.where( "id", "=", 1 ).orWhere( "id", "=", 2 ); 114 | } ); 115 | } ); 116 | }, 117 | { 118 | sql: "SELECT * FROM ""users"" WHERE ""email"" = ? AND (""id"" = ? OR ""id"" = ?)", 119 | bindings: [ "foo", 1, 2 ] 120 | } 121 | ); 122 | } ); 123 | } ); 124 | 125 | describe( "tap", function() { 126 | it( "runs a callback that gets passed the query. without modifying the query", function() { 127 | variables.count = 0; 128 | testCase( function( builder ) { 129 | builder 130 | .from( "users" ) 131 | .tap( function( q ) { 132 | count++; 133 | } ) 134 | .where( "id", 1 ) 135 | .tap( function( q ) { 136 | count++; 137 | } ) 138 | .tap( function( q ) { 139 | count++; 140 | // attempts to modify the query should not work 141 | return q.where( "foo", "bar" ); 142 | } ); 143 | }, { sql: "SELECT * FROM ""users"" WHERE ""id"" = ?", bindings: [ 1 ] } ); 144 | 145 | expect( count ).toBe( 3, "Three different tap functions should have been called." ); 146 | } ); 147 | } ); 148 | } ); 149 | } 150 | 151 | private function testCase( callback, expected ) { 152 | var builder = getBuilder(); 153 | local.sql = callback( builder ); 154 | if ( !isNull( local.sql ) ) { 155 | if ( !isSimpleValue( local.sql ) ) { 156 | local.sql = local.sql.toSQL(); 157 | } 158 | } else { 159 | local.sql = builder.toSQL(); 160 | } 161 | if ( isSimpleValue( expected ) ) { 162 | expected = { sql: expected, bindings: [] }; 163 | } 164 | expect( local.sql ).toBeWithCase( expected.sql ); 165 | expect( getTestBindings( builder ) ).toBe( expected.bindings ); 166 | } 167 | 168 | private function getBuilder() { 169 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 170 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 171 | return builder; 172 | } 173 | 174 | private array function getTestBindings( builder ) { 175 | return builder 176 | .getBindings() 177 | .map( function( binding ) { 178 | return binding.value; 179 | } ); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/JoinClauseSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "join clause", function() { 5 | beforeEach( function() { 6 | variables.query = prepareMock( new qb.models.Query.QueryBuilder() ); 7 | } ); 8 | describe( "initialization", function() { 9 | it( "requires a parentQuery, type, and a table", function() { 10 | expect( function() { 11 | new qb.models.Query.JoinClause(); 12 | } ).toThrow(); 13 | expect( function() { 14 | new qb.models.Query.JoinClause( "inner" ); 15 | } ).toThrow(); 16 | expect( function() { 17 | new qb.models.Query.JoinClause( "inner", "sometable" ); 18 | } ).toThrow(); 19 | } ); 20 | 21 | it( "validates the type is a valid sql join type", function() { 22 | expect( function() { 23 | new qb.models.Query.JoinClause( query, "gibberish", "sometable" ); 24 | } ).toThrow(); 25 | expect( function() { 26 | new qb.models.Query.JoinClause( query, "left typo", "sometable" ); 27 | } ).toThrow(); 28 | expect( function() { 29 | new qb.models.Query.JoinClause( query, "left", "sometable" ); 30 | } ).notToThrow(); 31 | expect( function() { 32 | new qb.models.Query.JoinClause( query, "left outer", "sometable" ); 33 | } ).notToThrow(); 34 | } ); 35 | } ); 36 | 37 | describe( "adding join conditions", function() { 38 | beforeEach( function() { 39 | variables.join = new qb.models.Query.JoinClause( query, "inner", "second" ); 40 | getMockBox().prepareMock( join ); 41 | join.$property( propertyName = "utils", mock = new qb.models.Query.QueryUtils() ); 42 | } ); 43 | 44 | afterEach( function() { 45 | structDelete( variables, "join" ); 46 | } ); 47 | 48 | describe( "on()", function() { 49 | it( "can add a single join condition", function() { 50 | join.on( 51 | "first.id", 52 | "=", 53 | "second.first_id", 54 | "and", 55 | false 56 | ); 57 | 58 | var clauses = join.getWheres(); 59 | expect( arrayLen( clauses ) ).toBe( 1, "Only one clause should exist in the join statement" ); 60 | 61 | var clause = clauses[ 1 ]; 62 | expect( clause.first ).toBe( "first.id", "First column should be [first.id]" ); 63 | expect( clause.operator ).toBe( "=", "Operator should be [=]" ); 64 | expect( clause.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 65 | expect( clause.combinator ).toBe( "and" ); 66 | } ); 67 | 68 | it( "defaults to ""and"" for the combinator and ""false"" for the where flag", function() { 69 | join.on( "first.id", "=", "second.first_id" ); 70 | 71 | var clauses = join.getWheres(); 72 | expect( arrayLen( clauses ) ).toBe( 1, "Only one clause should exist in the join statement" ); 73 | 74 | var clause = clauses[ 1 ]; 75 | expect( clause.first ).toBe( "first.id", "First column should be [first.id]" ); 76 | expect( clause.operator ).toBe( "=", "Operator should be [=]" ); 77 | expect( clause.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 78 | expect( clause.combinator ).toBe( "and" ); 79 | } ); 80 | 81 | it( "can add multiple join clauses", function() { 82 | join.on( "first.id", "=", "second.first_id" ); 83 | join.on( "first.locale", "=", "second.locale" ); 84 | 85 | var clauses = join.getWheres(); 86 | expect( arrayLen( clauses ) ).toBe( 2, "Two clauses should exist in the join statement" ); 87 | 88 | var clauseOne = clauses[ 1 ]; 89 | expect( clauseOne.first ).toBe( "first.id", "First column should be [first.id]" ); 90 | expect( clauseOne.operator ).toBe( "=", "Operator should be [=]" ); 91 | expect( clauseOne.second ).toBe( "second.first_id", "First column should be [second.first_id]" ); 92 | expect( clauseOne.combinator ).toBe( "and" ); 93 | 94 | var clauseTwo = clauses[ 2 ]; 95 | expect( clauseTwo.first ).toBe( "first.locale", "First column should be [first.locale]" ); 96 | expect( clauseTwo.operator ).toBe( "=", "Operator should be [=]" ); 97 | expect( clauseTwo.second ).toBe( "second.locale", "First column should be [second.locale]" ); 98 | expect( clauseTwo.combinator ).toBe( "and" ); 99 | } ); 100 | 101 | it( "validates that the operator is a valid sql operator", function() { 102 | expect( function() { 103 | join.on( "first.id", "==", "second.first_id" ); 104 | } ).toThrow(); 105 | 106 | expect( function() { 107 | join.on( "first.id", "<>", "second.first_id" ); 108 | } ).notToThrow(); 109 | } ); 110 | } ); 111 | 112 | describe( "orOn()", function() { 113 | it( "can add a single join condition", function() { 114 | join.orOn( "first.another_value", ">=", "second.another_value" ); 115 | 116 | var clauses = join.getWheres(); 117 | expect( arrayLen( clauses ) ).toBe( 1, "Only one clause should exist in the join statement" ); 118 | 119 | var clause = clauses[ 1 ]; 120 | expect( clause.first ).toBe( 121 | "first.another_value", 122 | "First column should be [first.another_value]" 123 | ); 124 | expect( clause.operator ).toBe( ">=", "Operator should be [>=]" ); 125 | expect( clause.second ).toBe( 126 | "second.another_value", 127 | "First column should be [second.another_value]" 128 | ); 129 | expect( clause.combinator ).toBe( "or" ); 130 | } ); 131 | } ); 132 | 133 | describe( "where()", function() { 134 | it( "adds a where statement to a join clause", function() { 135 | join.where( "second.locale", "=", "en-US" ); 136 | 137 | var clauses = join.getWheres(); 138 | expect( arrayLen( clauses ) ).toBe( 1, "Only one clause should exist in the join statement" ); 139 | 140 | var clause = clauses[ 1 ]; 141 | expect( clause.column ).toBe( "second.locale", "First column should be [second.locale]" ); 142 | expect( clause.operator ).toBe( "=", "Operator should be [>=]" ); 143 | expect( clause.combinator ).toBe( "and" ); 144 | } ); 145 | 146 | it( "can use the shortcut where statement when the operator is equals (=)", function() { 147 | join.where( "second.locale", "en-US" ); 148 | 149 | var clauses = join.getWheres(); 150 | expect( arrayLen( clauses ) ).toBe( 1, "Only one clause should exist in the join statement" ); 151 | 152 | var clause = clauses[ 1 ]; 153 | expect( clause.column ).toBe( "second.locale", "First column should be [second.locale]" ); 154 | expect( clause.operator ).toBe( "=", "Operator should be [>=]" ); 155 | expect( clause.combinator ).toBe( "and" ); 156 | } ); 157 | 158 | it( "adds the where value to the bindings", function() { 159 | join.where( "second.locale", "=", "en-US" ); 160 | 161 | var bindings = join.getBindings(); 162 | expect( arrayLen( bindings ) ).toBe( 1, "Only one clause should exist in the join statement" ); 163 | 164 | var binding = bindings[ 1 ]; 165 | expect( binding.value ).toBe( "en-US" ); 166 | expect( binding.cfsqltype ).toBe( "varchar" ); 167 | } ); 168 | } ); 169 | 170 | describe( "newQuery()", function() { 171 | it( "creates a new JoinClause instance", function() { 172 | expect( join.newQuery() ).toBeInstanceOf( "qb.models.Query.JoinClause" ); 173 | } ); 174 | 175 | it( "binds the type", function() { 176 | var newJoin = join.newQuery(); 177 | expect( newJoin.getType() ).toBe( join.getType() ); 178 | } ); 179 | 180 | it( "binds the table", function() { 181 | var newJoin = join.newQuery(); 182 | expect( newJoin.getTable() ).toBe( join.getTable() ); 183 | } ); 184 | } ); 185 | 186 | describe( "getMementoForComparison", function() { 187 | beforeEach( function() { 188 | variables.qb = new qb.models.Query.QueryBuilder( preventDuplicateJoins = true ).from( 189 | new qb.models.Query.QueryBuilder( preventDuplicateJoins = true ) 190 | .select( "FK_otherTable" ) 191 | .from( "second_table" ) 192 | ); 193 | 194 | variables.otherQb = new qb.models.Query.QueryBuilder( preventDuplicateJoins = true ).from( 195 | "third_table" 196 | ); 197 | 198 | variables.joinOther = new qb.models.Query.JoinClause( qb, "inner", otherQb ); 199 | } ); 200 | 201 | afterEach( function() { 202 | structDelete( variables, "qb" ); 203 | structDelete( variables, "otherQb" ); 204 | } ); 205 | 206 | it( "can produce a memento for a table with a QB object as a FROM", function() { 207 | expect( qb.getMementoForComparison().from ).toBe( 208 | "SELECT ""FK_otherTable"" FROM ""second_table""" 209 | ); 210 | } ); 211 | 212 | it( "can produce a memento for a joinClause", function() { 213 | expect( joinOther.getMementoForComparison().table ).toBe( "SELECT * FROM ""third_table""" ); 214 | } ); 215 | } ); 216 | 217 | describe( "preventDuplicateJoins", function() { 218 | beforeEach( function() { 219 | variables.qb = new qb.models.Query.QueryBuilder( preventDuplicateJoins = true ); 220 | variables.joinOther = new qb.models.Query.JoinClause( query, "inner", "second" ); 221 | getMockBox().prepareMock( joinOther ); 222 | } ); 223 | 224 | afterEach( function() { 225 | structDelete( variables, "joinOther" ); 226 | structDelete( variables, "qb" ); 227 | } ); 228 | 229 | it( "can match two identical, simple joins", function() { 230 | expect( variables.join.isEqualTo( variables.joinOther ) ).toBeTrue(); 231 | } ); 232 | 233 | it( "can tell that an inner join does not match a left join", function() { 234 | variables.joinOther.setType( "left" ); 235 | expect( variables.join.isEqualTo( variables.joinOther ) ).toBeFalse(); 236 | } ); 237 | 238 | it( "can tell that the same kind of join on two different tables do not match", function() { 239 | variables.joinOther.setTable( "third" ); 240 | expect( variables.join.isEqualTo( variables.joinOther ) ).toBeFalse(); 241 | } ); 242 | 243 | it( "can tell that two joins on the same table with different conditions do not match", function() { 244 | join.on( "first.id", "=", "second.first_id" ); 245 | joinOther.on( "first.locale", "=", "second.locale" ); 246 | expect( variables.join.isEqualTo( variables.joinOther ) ).toBeFalse(); 247 | } ); 248 | 249 | it( "will prevent an identical join from being added when preventDuplicateJoins is true", function() { 250 | variables.qb.join( variables.join ); 251 | variables.qb.join( variables.joinOther ); 252 | expect( variables.qb.getJoins().len() ).toBe( 1 ); 253 | } ); 254 | 255 | it( "will prevent an identical join from being added using the closure syntax when preventDuplicateJoins is true", function() { 256 | variables.qb.join( "secondTable", function( j ) { 257 | j.on( "secondTable.id", "firstTable.secondId" ); 258 | } ); 259 | variables.qb.join( "secondTable", function( j ) { 260 | j.on( "secondTable.id", "firstTable.secondId" ); 261 | } ); 262 | expect( variables.qb.getJoins().len() ).toBe( 1 ); 263 | } ); 264 | 265 | it( "will allow an identical join from being added when preventDuplicateJoins is false", function() { 266 | variables.qb.setPreventDuplicateJoins( false ); 267 | variables.qb.join( variables.join ); 268 | variables.qb.join( variables.joinOther ); 269 | expect( variables.qb.getJoins().len() ).toBe( 2 ); 270 | } ); 271 | } ); 272 | } ); 273 | } ); 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/PaginationSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "pagination", function() { 5 | it( "returns the default pagination object", function() { 6 | var builder = getBuilder(); 7 | var expectedResults = []; 8 | for ( var i = 1; i <= 25; i++ ) { 9 | expectedResults.append( { "id": i } ); 10 | } 11 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 12 | builder.$( "count", 45 ); 13 | builder.$( "runQuery", expectedQuery ); 14 | 15 | var results = builder.from( "users" ).paginate(); 16 | 17 | expect( results ).toBe( { 18 | "pagination": { 19 | "maxRows": 25, 20 | "offset": 0, 21 | "page": 1, 22 | "totalPages": 2, 23 | "totalRecords": 45 24 | }, 25 | "results": expectedResults 26 | } ); 27 | } ); 28 | 29 | it( "can paginate a group by query", function() { 30 | var builder = getBuilder(); 31 | var expectedResults = []; 32 | for ( var i = 1; i <= 25; i++ ) { 33 | expectedResults.append( { "id": i } ); 34 | } 35 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 36 | builder.$( "runQuery", expectedQuery ); 37 | 38 | var nestedBuilder = getBuilder(); 39 | nestedBuilder.$( "count", 15 ); 40 | builder.$( "newQuery", nestedBuilder ); 41 | 42 | var results = builder 43 | .from( "users" ) 44 | .groupBy( "lastName" ) 45 | .paginate(); 46 | 47 | expect( results ).toBe( { 48 | "pagination": { 49 | "maxRows": 25, 50 | "offset": 0, 51 | "page": 1, 52 | "totalPages": 1, 53 | "totalRecords": 15 54 | }, 55 | "results": expectedResults 56 | } ); 57 | } ); 58 | 59 | it( "can get results for subsequent pages", function() { 60 | var builder = getBuilder(); 61 | var expectedResults = []; 62 | for ( var i = 26; i <= 45; i++ ) { 63 | expectedResults.append( { "id": i } ); 64 | } 65 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 66 | builder.$( "count", 45 ); 67 | builder.$( "runQuery", expectedQuery ); 68 | 69 | var results = builder.from( "users" ).paginate( page = 2 ); 70 | 71 | expect( results ).toBe( { 72 | "pagination": { 73 | "maxRows": 25, 74 | "offset": 25, 75 | "page": 2, 76 | "totalPages": 2, 77 | "totalRecords": 45 78 | }, 79 | "results": expectedResults 80 | } ); 81 | } ); 82 | 83 | it( "can provide a custom amount per page", function() { 84 | var builder = getBuilder(); 85 | var expectedResults = []; 86 | for ( var i = 1; i <= 10; i++ ) { 87 | expectedResults.append( { "id": i } ); 88 | } 89 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 90 | builder.$( "count", 45 ); 91 | builder.$( "runQuery", expectedQuery ); 92 | 93 | var results = builder.from( "users" ).paginate( page = 1, maxRows = 10 ); 94 | 95 | expect( results ).toBe( { 96 | "pagination": { 97 | "maxRows": 10, 98 | "offset": 0, 99 | "page": 1, 100 | "totalPages": 5, 101 | "totalRecords": 45 102 | }, 103 | "results": expectedResults 104 | } ); 105 | } ); 106 | 107 | it( "can does not limit the query when the maxrows passes the override check", function() { 108 | var builder = getBuilder(); 109 | builder.setShouldMaxRowsOverrideToAll( function( maxrows ) { 110 | return maxrows <= 0; 111 | } ); 112 | 113 | var expectedResults = []; 114 | for ( var i = 1; i <= 45; i++ ) { 115 | expectedResults.append( { "id": i } ); 116 | } 117 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 118 | builder.$( "count", 45 ); 119 | builder.$( "runQuery", expectedQuery ); 120 | 121 | var results = builder.from( "users" ).paginate( page = 1, maxRows = 0 ); 122 | 123 | expect( results.pagination ).toBe( { 124 | "maxRows": 0, 125 | "offset": 0, 126 | "page": 1, 127 | "totalPages": 0, 128 | "totalRecords": 45 129 | } ); 130 | expect( results.results ).toBe( expectedResults ); 131 | } ); 132 | 133 | it( "can handle empty datasets", function() { 134 | var builder = getBuilder(); 135 | var expectedResults = []; 136 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 137 | builder.$( "count", 0 ); 138 | builder.$( "runQuery", expectedQuery ); 139 | 140 | var results = builder.from( "users" ).paginate( page = 1, maxRows = 0 ); 141 | 142 | expect( results.pagination ).toBe( { 143 | "maxRows": 0, 144 | "offset": 0, 145 | "page": 1, 146 | "totalPages": 0, 147 | "totalRecords": 0 148 | } ); 149 | expect( results.results ).toBe( expectedResults ); 150 | } ); 151 | 152 | it( "can provide a custom paginator shell", function() { 153 | var builder = getBuilder(); 154 | builder.setPaginationCollector( { 155 | "generateWithResults": function( totalRecords, results, page, maxRows ) { 156 | return { 157 | "total": totalRecords, 158 | "pageNumber": page, 159 | "limit": maxRows, 160 | "data": results 161 | }; 162 | } 163 | } ); 164 | var expectedResults = []; 165 | for ( var i = 1; i <= 25; i++ ) { 166 | expectedResults.append( { "id": i } ); 167 | } 168 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 169 | builder.$( "count", 45 ); 170 | builder.$( "runQuery", expectedQuery ); 171 | 172 | var results = builder.from( "users" ).paginate(); 173 | 174 | expect( results ).toBe( { 175 | "total": 45, 176 | "pageNumber": 1, 177 | "limit": 25, 178 | "data": expectedResults 179 | } ); 180 | } ); 181 | } ); 182 | 183 | describe( "simple pagination", function() { 184 | it( "returns the default pagination object", function() { 185 | var builder = getBuilder(); 186 | var expectedResults = []; 187 | for ( var i = 1; i <= 26; i++ ) { 188 | expectedResults.append( { "id": i } ); 189 | } 190 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 191 | builder.$( "runQuery", expectedQuery ); 192 | 193 | var results = builder.from( "users" ).simplePaginate(); 194 | 195 | expect( results ).toBe( { 196 | "pagination": { 197 | "maxRows": 25, 198 | "offset": 0, 199 | "page": 1, 200 | "hasMore": true 201 | }, 202 | "results": expectedResults.slice( 1, 25 ) 203 | } ); 204 | } ); 205 | 206 | it( "can get results for subsequent pages", function() { 207 | var builder = getBuilder(); 208 | var expectedResults = []; 209 | for ( var i = 26; i <= 45; i++ ) { 210 | expectedResults.append( { "id": i } ); 211 | } 212 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 213 | builder.$( "runQuery", expectedQuery ); 214 | 215 | var results = builder.from( "users" ).simplePaginate( page = 2 ); 216 | 217 | expect( results ).toBe( { 218 | "pagination": { 219 | "maxRows": 25, 220 | "offset": 25, 221 | "page": 2, 222 | "hasMore": false 223 | }, 224 | "results": expectedResults 225 | } ); 226 | } ); 227 | 228 | it( "can provide a custom amount per page", function() { 229 | var builder = getBuilder(); 230 | var expectedResults = []; 231 | for ( var i = 1; i <= 11; i++ ) { 232 | expectedResults.append( { "id": i } ); 233 | } 234 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 235 | builder.$( "runQuery", expectedQuery ); 236 | 237 | var results = builder.from( "users" ).simplePaginate( page = 1, maxRows = 10 ); 238 | 239 | expect( results ).toBe( { 240 | "pagination": { 241 | "maxRows": 10, 242 | "offset": 0, 243 | "page": 1, 244 | "hasMore": true 245 | }, 246 | "results": expectedResults.slice( 1, 10 ) 247 | } ); 248 | } ); 249 | 250 | it( "can provide a custom paginator shell", function() { 251 | var builder = getBuilder(); 252 | builder.setPaginationCollector( { 253 | "generateWithResults": function( totalRecords, results, page, maxRows ) { 254 | return { 255 | "total": totalRecords, 256 | "pageNumber": page, 257 | "limit": maxRows, 258 | "data": results 259 | }; 260 | }, 261 | "generateSimpleWithResults": function( results, page, maxRows ) { 262 | return { 263 | "next": results.len() > maxRows, 264 | "pageNumber": page, 265 | "limit": maxRows, 266 | "data": results 267 | }; 268 | } 269 | } ); 270 | var expectedResults = []; 271 | for ( var i = 1; i <= 20; i++ ) { 272 | expectedResults.append( { "id": i } ); 273 | } 274 | var expectedQuery = queryNew( "id", "integer", expectedResults ); 275 | builder.$( "runQuery", expectedQuery ); 276 | 277 | var results = builder.from( "users" ).simplePaginate(); 278 | 279 | expect( results ).toBe( { 280 | "next": false, 281 | "pageNumber": 1, 282 | "limit": 25, 283 | "data": expectedResults 284 | } ); 285 | } ); 286 | } ); 287 | } 288 | 289 | private function getBuilder() { 290 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 291 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 292 | return builder; 293 | } 294 | 295 | private array function getTestBindings( builder ) { 296 | return builder 297 | .getBindings() 298 | .map( function( binding ) { 299 | return binding.value; 300 | } ); 301 | } 302 | 303 | private boolean function supportsNativeReturnType() { 304 | return server.keyExists( "lucee" ) || listFirst( server.coldfusion.productversion ) >= 2021; 305 | } 306 | 307 | } 308 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/PretendSpec.cfc: -------------------------------------------------------------------------------- 1 | component displayname="PretendSpec" extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "pretend", function() { 5 | it( "can pretend to run queries instead of actually running them", function() { 6 | var qb = new qb.models.Query.QueryBuilder(); 7 | expect( function() { 8 | qb.newQuery() 9 | .select( "*" ) 10 | .from( "users" ) 11 | .get(); 12 | } ).toThrow(); 13 | 14 | expect( function() { 15 | qb.newQuery() 16 | .pretend() 17 | .select( "*" ) 18 | .from( "users" ) 19 | .get(); 20 | } ).notToThrow(); 21 | } ); 22 | 23 | it( "can pretend to run schema commands instead of actually running them", function() { 24 | var sb = new qb.models.Schema.SchemaBuilder(); 25 | expect( function() { 26 | sb.create( "users", function( t ) { 27 | t.increments( "id" ); 28 | t.string( "name" ); 29 | t.datetime( "createdDate" ); 30 | } ); 31 | } ).toThrow(); 32 | 33 | expect( function() { 34 | sb.pretend() 35 | .create( "users", function( t ) { 36 | t.increments( "id" ); 37 | t.string( "name" ); 38 | t.datetime( "createdDate" ); 39 | } ); 40 | } ).notToThrow(); 41 | } ); 42 | } ); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/QueryDebuggingSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "query debugging", function() { 5 | it( "can output the configured sql with placeholders for the bindings", function() { 6 | var query = getBuilder() 7 | .from( "users" ) 8 | .join( "logins", function( j ) { 9 | j.on( "users.id", "logins.user_id" ).where( "logins.created_date", ">", "01 Jun 2019" ); 10 | } ) 11 | .whereIn( "users.type", [ "admin", "manager" ] ) 12 | .whereNotNull( "active" ) 13 | .orderBy( "logins.created_date", "desc" ); 14 | 15 | expect( query.toSQL() ).toBe( 16 | "SELECT * FROM ""users"" INNER JOIN ""logins"" ON ""users"".""id"" = ""logins"".""user_id"" AND ""logins"".""created_date"" > ? WHERE ""users"".""type"" IN (?, ?) AND ""active"" IS NOT NULL ORDER BY ""logins"".""created_date"" DESC" 17 | ); 18 | } ); 19 | 20 | // TODO: Update with BoxLang specific path 21 | xit( "can output the configured sql with the bindings substituted in", function() { 22 | var query = getBuilder() 23 | .from( "users" ) 24 | .join( "logins", function( j ) { 25 | j.on( "users.id", "logins.user_id" ) 26 | .where( "logins.created_date", ">", parseDateTime( "2019-06-01" ) ); 27 | } ) 28 | .whereIn( "users.type", [ "admin", "manager" ] ) 29 | .whereNotNull( "active" ) 30 | .orderBy( "logins.created_date", "desc" ); 31 | 32 | if ( isLucee() ) { 33 | expect( query.toSQL( showBindings = true ) ).toBe( 34 | "SELECT * FROM ""users"" INNER JOIN ""logins"" ON ""users"".""id"" = ""logins"".""user_id"" AND ""logins"".""created_date"" > {""value"":""2019-06-01T00:00:00-06:00"",""cfsqltype"":""TIMESTAMP"",""null"":false} WHERE ""users"".""type"" IN ({""value"":""admin"",""cfsqltype"":""VARCHAR"",""null"":false}, {""value"":""manager"",""cfsqltype"":""VARCHAR"",""null"":false}) AND ""active"" IS NOT NULL ORDER BY ""logins"".""created_date"" DESC" 35 | ); 36 | } else { 37 | expect( query.toSQL( showBindings = true ) ).toBe( 38 | "SELECT * FROM ""users"" INNER JOIN ""logins"" ON ""users"".""id"" = ""logins"".""user_id"" AND ""logins"".""created_date"" > {""value"":""2019-06-01T00:00:00-06:00"",""cfsqltype"":""TIMESTAMP"",""null"":false} WHERE ""users"".""type"" IN ({""value"":""admin"",""cfsqltype"":""VARCHAR"",""null"":false}, {""value"":""manager"",""cfsqltype"":""VARCHAR"",""null"":false}) AND ""active"" IS NOT NULL ORDER BY ""logins"".""created_date"" DESC" 39 | ); 40 | } 41 | } ); 42 | 43 | it( "provides a useful error message when calling `from` with a closure", function() { 44 | expect( function() { 45 | getBuilder().from( function( q ) { 46 | q.from( "whatever" ).where( "active", 1 ); 47 | } ); 48 | } ).toThrow( type = "QBInvalidFrom" ); 49 | } ); 50 | } ); 51 | } 52 | 53 | private function getBuilder() { 54 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 55 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 56 | return builder; 57 | } 58 | 59 | private boolean function isLucee() { 60 | return server.keyExists( "lucee" ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/QueryDefaultsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "query defaults", function() { 5 | it( "can configure default options", function() { 6 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 7 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 8 | builder.setDefaultOptions( { "datasource": "foo" } ); 9 | var expectedQuery = queryNew( "id", "integer", [ { id: 1 } ] ); 10 | grammar.$( "runQuery", expectedQuery ); 11 | builder 12 | .select( "id" ) 13 | .from( "users" ) 14 | .get(); 15 | expect( grammar.$once( "runQuery" ) ).toBeTrue( "runQuery should have been called once." ); 16 | var options = grammar.$callLog().runQuery[ 1 ][ 3 ]; 17 | expect( options ).toBeStruct(); 18 | expect( options ).toHaveKey( "datasource" ); 19 | expect( options.datasource ).toBe( "foo" ); 20 | } ); 21 | 22 | it( "can override default options at the call site", function() { 23 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 24 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 25 | builder.setDefaultOptions( { "datasource": "foo" } ); 26 | var expectedQuery = queryNew( "id", "integer", [ { id: 1 } ] ); 27 | grammar.$( "runQuery", expectedQuery ); 28 | builder 29 | .select( "id" ) 30 | .from( "users" ) 31 | .get( options = { "datasource": "bar" } ); 32 | expect( grammar.$once( "runQuery" ) ).toBeTrue( "runQuery should have been called once." ); 33 | var options = grammar.$callLog().runQuery[ 1 ][ 3 ]; 34 | expect( options ).toBeStruct(); 35 | expect( options ).toHaveKey( "datasource" ); 36 | expect( options.datasource ).toBe( "bar" ); 37 | } ); 38 | } ); 39 | } 40 | 41 | private function getBuilder() { 42 | var grammar = getMockBox().createMock( "qb.models.Grammars.BaseGrammar" ).init(); 43 | var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); 44 | return builder; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/QueryLogSpec.cfc: -------------------------------------------------------------------------------- 1 | component displayname="QueryLogSpec" extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "queryLog", function() { 5 | it( "tracks the queries it executes", function() { 6 | var qb = new qb.models.Query.QueryBuilder(); 7 | qb.pretend() 8 | .select( "*" ) 9 | .from( "users" ) 10 | .get(); 11 | 12 | expect( qb.getQueryLog() ).toBeArray(); 13 | expect( qb.getQueryLog() ).toHaveLength( 1 ); 14 | expect( qb.getQueryLog()[ 1 ] ).toBeStruct(); 15 | 16 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "sql" ); 17 | expect( qb.getQueryLog()[ 1 ].sql ).toBeString().toBe( "SELECT * FROM ""users""" ); 18 | 19 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "bindings" ); 20 | expect( qb.getQueryLog()[ 1 ].bindings ).toBeArray().toBeEmpty(); 21 | 22 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "options" ); 23 | expect( qb.getQueryLog()[ 1 ].options ).toBeStruct(); 24 | 25 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "returnObject" ); 26 | expect( qb.getQueryLog()[ 1 ].returnObject ).toBeString().toBe( "query" ); 27 | 28 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "pretend" ); 29 | expect( qb.getQueryLog()[ 1 ].pretend ).toBeBoolean().toBeTrue(); 30 | 31 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "result" ); 32 | expect( qb.getQueryLog()[ 1 ].result ).toBeStruct().toBeEmpty(); 33 | 34 | expect( qb.getQueryLog()[ 1 ] ).toHaveKey( "executionTime" ); 35 | expect( qb.getQueryLog()[ 1 ].executionTime ).toBeNumeric().toBe( 0 ); 36 | } ); 37 | 38 | it( "tracks the queries it executes for schema builder", function() { 39 | var schema = new qb.models.Schema.SchemaBuilder(); 40 | schema 41 | .pretend() 42 | .create( "users", function( t ) { 43 | t.increments( "id" ); 44 | t.string( "name" ); 45 | t.datetime( "createdDate" ); 46 | } ); 47 | 48 | expect( schema.getQueryLog() ).toBeArray(); 49 | expect( schema.getQueryLog() ).toHaveLength( 1 ); 50 | expect( schema.getQueryLog()[ 1 ] ).toBeStruct(); 51 | 52 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "sql" ); 53 | expect( schema.getQueryLog()[ 1 ].sql ) 54 | .toBeString() 55 | .toBe( "CREATE TABLE ""users"" (""id"" INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, ""name"" VARCHAR(255) NOT NULL, ""createdDate"" DATETIME NOT NULL, CONSTRAINT ""pk_users_id"" PRIMARY KEY (""id""))" ); 56 | 57 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "bindings" ); 58 | expect( schema.getQueryLog()[ 1 ].bindings ).toBeArray().toBeEmpty(); 59 | 60 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "options" ); 61 | expect( schema.getQueryLog()[ 1 ].options ).toBeStruct(); 62 | 63 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "returnObject" ); 64 | expect( schema.getQueryLog()[ 1 ].returnObject ).toBeString().toBe( "result" ); 65 | 66 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "pretend" ); 67 | expect( schema.getQueryLog()[ 1 ].pretend ).toBeBoolean().toBeTrue(); 68 | 69 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "result" ); 70 | expect( schema.getQueryLog()[ 1 ].result ).toBeStruct().toBeEmpty(); 71 | 72 | expect( schema.getQueryLog()[ 1 ] ).toHaveKey( "executionTime" ); 73 | expect( schema.getQueryLog()[ 1 ].executionTime ).toBeNumeric().toBe( 0 ); 74 | } ); 75 | } ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tests/specs/Query/Abstract/QueryUtilsSpec.cfc: -------------------------------------------------------------------------------- 1 | component displayname="QueryUtilsSpec" extends="testbox.system.BaseSpec" { 2 | 3 | function beforeAll() { 4 | variables.utils = new qb.models.Query.QueryUtils(); 5 | variables.mockGrammar = createMock( "qb.models.Grammars.BaseGrammar" ); 6 | variables.mockBuilder = new qb.models.Query.QueryBuilder( 7 | grammar = variables.mockGrammar, 8 | utils = variables.utils 9 | ); 10 | } 11 | 12 | function run() { 13 | describe( "inferSqlType()", function() { 14 | it( "maintains the passed in cfsqltype if provided", () => { 15 | var binding = utils.extractBinding( { "value": 1, "cfsqltype": "BIT" }, variables.mockGrammar ); 16 | expect( binding.cfsqltype ).toBe( "BIT" ); 17 | expect( binding.sqltype ).toBe( "BIT" ); 18 | } ); 19 | 20 | it( "maintains the passed in sqltype if provided", () => { 21 | var binding = utils.extractBinding( { "value": 1, "sqltype": "BIT" }, variables.mockGrammar ); 22 | expect( binding.cfsqltype ).toBe( "BIT" ); 23 | expect( binding.sqltype ).toBe( "BIT" ); 24 | } ); 25 | 26 | it( "strings", function() { 27 | expect( utils.inferSqlType( "a string", variables.mockGrammar ) ).toBe( "VARCHAR" ); 28 | } ); 29 | 30 | describe( "numbers", function() { 31 | it( "integers", function() { 32 | expect( utils.inferSqlType( 100, variables.mockGrammar ) ).toBe( "INTEGER" ); 33 | } ); 34 | 35 | it( "decimals", function() { 36 | expect( utils.inferSqlType( 4.50, variables.mockGrammar ) ).toBe( "DECIMAL" ); 37 | } ); 38 | 39 | it( "really long decimals", function() { 40 | expect( utils.inferSqlType( 19482.279999997998, variables.mockGrammar ) ).toBe( "DECIMAL" ); 41 | } ); 42 | } ); 43 | 44 | it( "dates", function() { 45 | expect( utils.inferSqlType( now(), variables.mockGrammar ) ).toBe( "TIMESTAMP" ); 46 | } ); 47 | 48 | it( "empty strings as null", () => { 49 | var bindingA = utils.extractBinding( "", variables.mockGrammar ); 50 | expect( bindingA.null ).toBeFalse(); 51 | variables.utils.setConvertEmptyStringsToNull( true ); 52 | var bindingB = utils.extractBinding( "", variables.mockGrammar ); 53 | expect( bindingB.null ).toBeTrue(); 54 | } ); 55 | 56 | it( "null", function() { 57 | expect( utils.inferSqlType( javacast( "null", "" ), variables.mockGrammar ) ).toBe( "VARCHAR" ); 58 | expect( utils.extractBinding( javacast( "null", "" ), variables.mockGrammar ) ).toBe( { 59 | "null": true, 60 | "cfsqltype": "VARCHAR", 61 | "sqltype": "VARCHAR", 62 | "value": "" 63 | } ); 64 | makePublic( utils, "checkIsActuallyNumeric", "publicCheckIsActuallyNumeric" ); 65 | expect( utils.publicCheckIsActuallyNumeric( javacast( "null", "" ) ) ).toBe( false ); 66 | makePublic( utils, "isFloatingPoint", "publicIsFloatingPoint" ); 67 | expect( 68 | utils.publicIsFloatingPoint( { "value": javacast( "null", "" ), "cfsqltype": "DECIMAL", "null": true } ) 69 | ).toBe( false ); 70 | makePublic( utils, "checkIsActuallyDate", "publicCheckIsActuallyDate" ); 71 | expect( utils.publicCheckIsActuallyDate( javacast( "null", "" ) ) ).toBe( false ); 72 | makePublic( utils, "calculateNumberOfDecimalDigits", "publicCalculateNumberOfDecimalDigits" ); 73 | expect( 74 | utils.publicCalculateNumberOfDecimalDigits( { "value": javacast( "null", "" ), "cfsqltype": "DECIMAL", "null": true } ) 75 | ).toBe( 0 ); 76 | } ); 77 | 78 | describe( "boolean", () => { 79 | it( "infers boolean types correctly", () => { 80 | makePublic( utils, "checkIsActuallyBoolean", "publicCheckIsActuallyBoolean" ); 81 | expect( utils.publicCheckIsActuallyBoolean( true ) ).toBeTrue(); 82 | expect( utils.publicCheckIsActuallyBoolean( "true" ) ).toBeFalse(); 83 | expect( utils.publicCheckIsActuallyBoolean( false ) ).toBeTrue(); 84 | expect( utils.publicCheckIsActuallyBoolean( "false" ) ).toBeFalse(); 85 | } ); 86 | 87 | describe( "extracting boolean params", () => { 88 | afterEach( () => variables.mockGrammar.$reset() ); 89 | 90 | it( "without boolean support in the grammar", () => { 91 | expect( utils.inferSqlType( true, variables.mockGrammar ) ).toBe( "TINYINT" ); 92 | expect( utils.inferSqlType( "true", variables.mockGrammar ) ).toBe( "VARCHAR" ); 93 | expect( utils.inferSqlType( false, variables.mockGrammar ) ).toBe( "TINYINT" ); 94 | expect( utils.inferSqlType( "false", variables.mockGrammar ) ).toBe( "VARCHAR" ); 95 | 96 | expect( utils.extractBinding( true, variables.mockGrammar ) ).toBe( { 97 | "list": false, 98 | "null": false, 99 | "cfsqltype": "TINYINT", 100 | "sqltype": "TINYINT", 101 | "value": 1 102 | } ); 103 | expect( utils.extractBinding( "true", variables.mockGrammar ) ).toBe( { 104 | "list": false, 105 | "null": false, 106 | "cfsqltype": "VARCHAR", 107 | "sqltype": "VARCHAR", 108 | "value": "true" 109 | } ); 110 | expect( utils.extractBinding( false, variables.mockGrammar ) ).toBe( { 111 | "list": false, 112 | "null": false, 113 | "cfsqltype": "TINYINT", 114 | "sqltype": "TINYINT", 115 | "value": 0 116 | } ); 117 | expect( utils.extractBinding( "false", variables.mockGrammar ) ).toBe( { 118 | "list": false, 119 | "null": false, 120 | "cfsqltype": "VARCHAR", 121 | "sqltype": "VARCHAR", 122 | "value": "false" 123 | } ); 124 | } ); 125 | 126 | it( "with boolean support in the grammar", () => { 127 | variables.mockGrammar.$( "getBooleanSqlType", "OTHER" ); 128 | variables.mockGrammar 129 | .$( "convertToBooleanType" ) 130 | .$callback( ( any value ) => { 131 | return { 132 | "value": isNull( value ) ? javacast( "null", "" ) : !!value, 133 | "cfsqltype": "OTHER", 134 | "sqltype": "OTHER" 135 | }; 136 | } ); 137 | 138 | expect( utils.extractBinding( true, variables.mockGrammar ) ).toBe( { 139 | "list": false, 140 | "null": false, 141 | "cfsqltype": "OTHER", 142 | "sqltype": "OTHER", 143 | "value": true 144 | } ); 145 | expect( utils.extractBinding( "true", variables.mockGrammar ) ).toBe( { 146 | "list": false, 147 | "null": false, 148 | "cfsqltype": "VARCHAR", 149 | "sqltype": "VARCHAR", 150 | "value": "true" 151 | } ); 152 | expect( utils.extractBinding( false, variables.mockGrammar ) ).toBe( { 153 | "list": false, 154 | "null": false, 155 | "cfsqltype": "OTHER", 156 | "sqltype": "OTHER", 157 | "value": false 158 | } ); 159 | expect( utils.extractBinding( "false", variables.mockGrammar ) ).toBe( { 160 | "list": false, 161 | "null": false, 162 | "cfsqltype": "VARCHAR", 163 | "sqltype": "VARCHAR", 164 | "value": "false" 165 | } ); 166 | } ); 167 | } ); 168 | } ); 169 | 170 | describe( "it infers the sql type from the members of an array", function() { 171 | it( "if all the members of the array are the same", function() { 172 | expect( utils.inferSqlType( [ 1, 2 ], variables.mockGrammar ) ).toBe( "INTEGER" ); 173 | } ); 174 | 175 | it( "but defaults to VARCHAR if they are different", function() { 176 | expect( 177 | utils.inferSqlType( 178 | [ 179 | 1, 180 | 2, 181 | 3, 182 | dateFormat( "05/01/2016", "MM/DD/YYYY" ) 183 | ], 184 | variables.mockGrammar 185 | ) 186 | ).toBe( "VARCHAR" ); 187 | } ); 188 | } ); 189 | } ); 190 | 191 | describe( "extractBinding()", function() { 192 | it( "includes sensible defaults", function() { 193 | var datetime = parseDateTime( "05/10/2016" ); 194 | var binding = utils.extractBinding( datetime, variables.mockGrammar ); 195 | 196 | expect( binding ).toBeStruct(); 197 | expect( binding.value ).toBe( dateTimeFormat( datetime, "yyyy-mm-dd'T'HH:nn:ss.SSSXXX" ) ); 198 | expect( binding.cfsqltype ).toBe( "TIMESTAMP" ); 199 | expect( binding.sqltype ).toBe( "TIMESTAMP" ); 200 | expect( binding.list ).toBe( false ); 201 | expect( binding.null ).toBe( false ); 202 | } ); 203 | 204 | it( "automatically sets a scale if needed", function() { 205 | var binding = utils.extractBinding( 206 | { "value": 3.14159, "cfsqltype": "DECIMAL" }, 207 | variables.mockGrammar 208 | ); 209 | 210 | expect( binding ).toBeStruct(); 211 | expect( binding.value ).toBe( 3.14159 ); 212 | expect( binding.cfsqltype ).toBe( "DECIMAL" ); 213 | expect( binding.sqltype ).toBe( "DECIMAL" ); 214 | expect( binding ).toHaveKey( "scale" ); 215 | expect( binding.scale ).toBe( 5 ); 216 | expect( binding.list ).toBe( false ); 217 | expect( binding.null ).toBe( false ); 218 | } ); 219 | 220 | it( "does not set a scale for integers", function() { 221 | var binding = utils.extractBinding( 222 | { "value": 3.14159, "cfsqltype": "INTEGER" }, 223 | variables.mockGrammar 224 | ); 225 | 226 | expect( binding ).toBeStruct(); 227 | expect( binding.value ).toBe( 3.14159 ); 228 | expect( binding.cfsqltype ).toBe( "INTEGER" ); 229 | expect( binding.sqltype ).toBe( "INTEGER" ); 230 | expect( binding ).notToHaveKey( "scale" ); 231 | expect( binding.list ).toBe( false ); 232 | expect( binding.null ).toBe( false ); 233 | } ); 234 | 235 | it( "uses a passed in scale if provided", function() { 236 | var binding = utils.extractBinding( 237 | { "value": 3.14159, "cfsqltype": "DECIMAL", "scale": 2 }, 238 | variables.mockGrammar 239 | ); 240 | 241 | expect( binding ).toBeStruct(); 242 | expect( binding.value ).toBe( 3.14159 ); 243 | expect( binding.cfsqltype ).toBe( "DECIMAL" ); 244 | expect( binding.sqltype ).toBe( "DECIMAL" ); 245 | expect( binding ).toHaveKey( "scale" ); 246 | expect( binding.scale ).toBe( 2 ); 247 | expect( binding.list ).toBe( false ); 248 | expect( binding.null ).toBe( false ); 249 | } ); 250 | 251 | it( "checks that structs that are passed look like query param structs", () => { 252 | expect( () => { 253 | var binding = utils.extractBinding( 254 | { 255 | "foo": "bar", 256 | "value": "something", 257 | "null": true, 258 | "enabled": true 259 | }, 260 | variables.mockGrammar 261 | ); 262 | } ).toThrow( 263 | type = "QBInvalidQueryParam", 264 | regex = "Invalid keys detected in your query param struct: \[enabled, foo\]\. Usually this happens when you meant to serialize the struct to JSON first\." 265 | ); 266 | } ); 267 | } ); 268 | 269 | describe( "queryToArrayOfStructs()", function() { 270 | it( "converts a query to an array of structs", function() { 271 | var data = [ 272 | { id: 1, name: "foo", age: 24 }, 273 | { id: 2, name: "bar", age: 32 }, 274 | { id: 3, name: "baz", age: 41 } 275 | ]; 276 | var q = queryNew( "id,name,age", "integer,varchar,integer", data ); 277 | expect( q ).toBeQuery(); 278 | expect( q.recordCount ).toBe( 3 ); 279 | 280 | var result = utils.queryToArrayOfStructs( q ); 281 | 282 | expect( result ).toBeArray(); 283 | expect( result ).toHaveLength( 3 ); 284 | expect( result ).toBe( data ); 285 | } ); 286 | } ); 287 | 288 | describe( "queryRemoveColumns()", function() { 289 | it( "returns the query with specified columns removed", function() { 290 | var data = [ 291 | { id: 1, name: "foo", age: 24 }, 292 | { id: 2, name: "bar", age: 32 }, 293 | { id: 3, name: "baz", age: 41 } 294 | ]; 295 | var q = queryNew( "id,name,age", "integer,varchar,integer", data ); 296 | var result = utils.queryRemoveColumns( q, "age,name" ); 297 | 298 | expect( result ).toBeQuery(); 299 | expect( result.recordCount ).toBe( 3 ); 300 | expect( result.columnList ).toBe( "id" ); 301 | } ); 302 | 303 | it( "returns the query with specified columns removed when no rows exist in query", function() { 304 | var data = []; 305 | var q = queryNew( "id,name,age", "integer,varchar,integer", data ); 306 | var result = utils.queryRemoveColumns( q, "age,name" ); 307 | 308 | expect( result ).toBeQuery(); 309 | expect( result.recordCount ).toBe( 0 ); 310 | expect( result.columnList ).toBe( "id" ); 311 | } ); 312 | } ); 313 | 314 | describe( "clone()", function() { 315 | it( "clones the query preserving the grammar and avoiding duplicate()", function() { 316 | var queryOne = new qb.models.Query.QueryBuilder(); 317 | queryOne 318 | .from( "foo" ) 319 | .select( [ "one", "two" ] ) 320 | .where( "bar", "baz" ); 321 | var queryTwo = queryOne.clone(); 322 | expect( queryTwo.getTableName() ).toBe( "foo" ); 323 | expect( queryTwo.getColumns() ).toBe( [ "one", "two" ] ); 324 | expect( queryTwo.getWheres() ).toBe( [ 325 | { 326 | column: "bar", 327 | combinator: "and", 328 | operator: "=", 329 | value: "baz", 330 | type: "basic" 331 | } 332 | ] ); 333 | expect( queryTwo.getRawBindings().where ).toBe( [ 334 | { 335 | value: "baz", 336 | cfsqltype: "varchar", 337 | sqltype: "varchar", 338 | null: false, 339 | list: false 340 | } 341 | ] ); 342 | queryTwo.from( "another" ); 343 | expect( queryOne.getTableName() ).toBe( "foo" ); 344 | } ); 345 | 346 | it( "has the exact same sql as the original query", function() { 347 | var queryOne = new qb.models.Query.QueryBuilder(); 348 | queryOne 349 | .from( "foo" ) 350 | .select( [ "one", "two" ] ) 351 | .where( "bar", "baz" ) 352 | .join( "qux", "qux.fooId", "=", "foo.id" ) 353 | .groupBy( [ "foo.one", "foo.two", "foo.bar" ] ) 354 | .having( "foo.one", ">", 1 ) 355 | .withAlias( "f" ) 356 | .orderByDesc( "qux.blah" ); 357 | var queryTwo = queryOne.clone(); 358 | expect( queryTwo.toSql( showBindings = "inline" ) ).toBe( queryOne.toSql( showBindings = "inline" ) ); 359 | } ); 360 | } ); 361 | } 362 | 363 | } 364 | -------------------------------------------------------------------------------- /tests/specs/SQLCommenterSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function beforeAll() { 4 | variables.sqlCommenter = new qb.models.SQLCommenter.SQLCommenter(); 5 | } 6 | 7 | function run() { 8 | describe( "SQLCommenter", () => { 9 | describe( "containsSQLComment", () => { 10 | it( "can detect if a SQL statement has a comment in it", function() { 11 | makePublic( variables.sqlCommenter, "containsSQLComment", "containsSQLCommentPublic" ); 12 | expect( 13 | variables.sqlCommenter.containsSQLCommentPublic( "SELECT * /* should not use star */ FROM users" ) 14 | ).toBeTrue(); 15 | expect( variables.sqlCommenter.containsSQLCommentPublic( "SELECT * FROM users" ) ).toBeFalse(); 16 | } ); 17 | } ); 18 | 19 | describe( "serializeValue", () => { 20 | it( "can serialize values in a key value pair", function() { 21 | makePublic( variables.sqlCommenter, "serializeValue", "serializeValuePublic" ); 22 | expect( variables.sqlCommenter.serializeValuePublic( "DROP TABLE FOO" ) ).toBe( "'DROP%20TABLE%20FOO'" ); 23 | expect( variables.sqlCommenter.serializeValuePublic( "/param first" ) ).toBe( "'%2Fparam%20first'" ); 24 | expect( variables.sqlCommenter.serializeValuePublic( "1234" ) ).toBe( "'1234'" ); 25 | } ); 26 | } ); 27 | 28 | describe( "serializeComment", () => { 29 | it( "can serialize key value pair comments", function() { 30 | makePublic( variables.sqlCommenter, "serializeComment", "serializeCommentPublic" ); 31 | expect( variables.sqlCommenter.serializeCommentPublic( "route", "/polls 1000" ) ).toBe( "route='%2Fpolls%201000'" ); 32 | expect( variables.sqlCommenter.serializeCommentPublic( "name''", """DROP TABLE USERS'""" ) ).toBe( "name%27%27='%22DROP%20TABLE%20USERS%27%22'" ); 33 | } ); 34 | } ); 35 | 36 | describe( "appendCommentsToSQL", () => { 37 | it( "serializes comments on the end of a sql query", function() { 38 | var commentedSQL = variables.sqlCommenter.appendCommentsToSQL( 39 | sql = "SELECT * FROM foo", 40 | comments = { 41 | "event": "Main.index", 42 | "framework": "coldbox-6.0.0", 43 | "handler": "Main", 44 | "action": "index", 45 | "route": "/", 46 | "dbDriver": "mysql-connector-java-8.0.25 (Revision: 08be9e9b4cba6aa115f9b27b215887af40b159e0)" 47 | } 48 | ); 49 | 50 | expect( commentedSQL ).toBeWithCase( 51 | "SELECT * FROM foo /*action='index',dbDriver='mysql-connector-java-8.0.25%20%28Revision%3A%2008be9e9b4cba6aa115f9b27b215887af40b159e0%29',event='Main.index',framework='coldbox-6.0.0',handler='Main',route='%2F'*/" 52 | ); 53 | } ); 54 | } ); 55 | 56 | describe( "parseCommentedSQL", () => { 57 | it( "can parse a commented SQL string into the sql and the comments", function() { 58 | var commentedSQL = "SELECT * FROM foo /*action='index',dbDriver='mysql-connector-java-8.0.25%20%28Revision%3A%2008be9e9b4cba6aa115f9b27b215887af40b159e0%29',event='Main.index',framework='coldbox-6.0.0',handler='Main',route='%2F'*/"; 59 | var sqlAndComments = variables.sqlCommenter.parseCommentedSQL( commentedSQL ); 60 | expect( sqlAndComments.sql ).toBeWithCase( "SELECT * FROM foo" ); 61 | expect( sqlAndComments.comments ).toBe( { 62 | "event": "Main.index", 63 | "framework": "coldbox-6.0.0", 64 | "handler": "Main", 65 | "action": "index", 66 | "route": "/", 67 | "dbDriver": "mysql-connector-java-8.0.25 (Revision: 08be9e9b4cba6aa115f9b27b215887af40b159e0)" 68 | } ); 69 | } ); 70 | } ); 71 | } ); 72 | } 73 | 74 | } 75 | --------------------------------------------------------------------------------