├── docs ├── images │ ├── deploy-button.png │ └── query-sample-1.png └── README.zh-CN.md ├── apex-query └── main │ └── default │ └── classes │ ├── Query.cls-meta.xml │ ├── QueryTest.cls-meta.xml │ ├── Query.cls │ └── QueryTest.cls ├── .prettierignore ├── .prettierrc ├── scripts ├── script.sh └── bench.apex ├── .forceignore ├── config └── project-scratch-def.json ├── package.json ├── .gitignore ├── sfdx-project.json ├── LICENSE └── README.md /docs/images/deploy-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexfarm/ApexQuery/HEAD/docs/images/deploy-button.png -------------------------------------------------------------------------------- /docs/images/query-sample-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexfarm/ApexQuery/HEAD/docs/images/query-sample-1.png -------------------------------------------------------------------------------- /apex-query/main/default/classes/Query.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /apex-query/main/default/classes/QueryTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | .sf 9 | .vscode 10 | 11 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "plugins": [ 4 | "prettier-plugin-apex", 5 | "@prettier/plugin-xml" 6 | ], 7 | "overrides": [ 8 | { 9 | "files": "*.{cls,apex}", 10 | "options": { 11 | "tabWidth": 4, 12 | "printWidth": 120 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/script.sh: -------------------------------------------------------------------------------- 1 | # force:package:create only execute for the first time 2 | # sfdx force:package:create -n ApexQuery -t Unlocked -r apex-query 3 | sfdx force:package:version:create -p ApexQuery -x -c --wait 10 --code-coverage 4 | sfdx force:package:version:list 5 | sfdx force:package:version:promote -p 04tGC000007TPn6YAG 6 | sfdx force:package:version:report -p 04tGC000007TPn6YAG -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "Apex Query", 3 | "edition": "Developer", 4 | "language": "en_US", 5 | "features": ["EnableSetPasswordInApi", "MultiCurrency", "Knowledge"], 6 | "settings": { 7 | "lightningExperienceSettings": { 8 | "enableS1DesktopEnabled": true 9 | }, 10 | "mobileSettings": { 11 | "enableS1EncryptedStoragePref2": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex-query", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Apex Query", 6 | "scripts": { 7 | "lint": "eslint **/{aura,lwc}/**/*.js", 8 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 9 | "prettier:verify": "prettier --check \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 10 | "postinstall": "husky install", 11 | "precommit": "lint-staged" 12 | }, 13 | "devDependencies": { 14 | "@prettier/plugin-xml": "^3.2.2", 15 | "husky": "^9.1.5", 16 | "lint-staged": "^15.1.0", 17 | "prettier": "^3.1.0", 18 | "prettier-plugin-apex": "^2.0.1" 19 | }, 20 | "lint-staged": { 21 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ 22 | "prettier --write" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sf/ 7 | .sfdx/ 8 | .localdevserver/ 9 | deploy-options.json 10 | 11 | # LWC VSCode autocomplete 12 | **/lwc/jsconfig.json 13 | 14 | # LWC Jest coverage reports 15 | coverage/ 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Dependency directories 25 | node_modules/ 26 | 27 | # Eslint cache 28 | .eslintcache 29 | 30 | # MacOS system files 31 | .DS_Store 32 | 33 | # Windows system files 34 | Thumbs.db 35 | ehthumbs.db 36 | [Dd]esktop.ini 37 | $RECYCLE.BIN/ 38 | 39 | # Local environment variables 40 | .env 41 | .husky 42 | .vscode 43 | package-lock.json 44 | 45 | # Python Salesforce Functions 46 | **/__pycache__/ 47 | **/.venv/ 48 | **/venv/ 49 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "apex-query", 5 | "versionName": "ver 3.0.5", 6 | "versionNumber": "3.0.5.NEXT", 7 | "default": true, 8 | "package": "ApexQuery" 9 | } 10 | ], 11 | "name": "ApexQuery", 12 | "namespace": "", 13 | "sfdcLoginUrl": "https://login.salesforce.com", 14 | "sourceApiVersion": "64.0", 15 | "packageAliases": { 16 | "ApexQuery": "0Ho2v000000PBG5CAO", 17 | "ApexQuery@1.0.0-1": "04t2v000007CfiRAAS", 18 | "ApexQuery@1.0.1-1": "04t2v000007CfibAAC", 19 | "ApexQuery@1.0.2-1": "04t2v000007Cfj0AAC", 20 | "ApexQuery@1.0.3-1": "04t2v000007Cg2pAAC", 21 | "ApexQuery@1.0.4-1": "04t2v000007Cg2zAAC", 22 | "ApexQuery@1.0.5-1": "04t2v000007Cg4CAAS", 23 | "ApexQuery@1.0.6-1": "04t2v000007Cg4MAAS", 24 | "ApexQuery@1.0.7-1": "04t2v000007Cg4RAAS", 25 | "ApexQuery@1.0.8-1": "04t2v000007OilzAAC", 26 | "ApexQuery@1.1.0-1": "04t2v000007Oim9AAC", 27 | "ApexQuery@2.0.0-1": "04t2v000007OimYAAS", 28 | "ApexQuery@3.0.0-1": "04tGC000007TLzwYAG", 29 | "ApexQuery@3.0.1-1": "04tGC000007TMKKYA4", 30 | "ApexQuery@3.0.1-2": "04tGC000007TMKPYA4", 31 | "ApexQuery@3.0.2-1": "04tGC000007TMKUYA4", 32 | "ApexQuery@3.0.3-1": "04tGC000007TMTiYAO", 33 | "ApexQuery@3.0.3-2": "04tGC000007TOQEYA4", 34 | "ApexQuery@3.0.4-1": "04tGC000007TORgYAO", 35 | "ApexQuery@3.0.5-1": "04tGC000007TPn6YAG" 36 | } 37 | } -------------------------------------------------------------------------------- /scripts/bench.apex: -------------------------------------------------------------------------------- 1 | class QueryTest extends Query { 2 | public void test() { 3 | // prettier-ignore 4 | Query parentAccount = Query.of('Account') 5 | .selectBy('Name', format(convertCurrency('AnnualRevenue')), 'BillingState') 6 | .selectParent('Parent', Query.of('Account').selectBy('Name', 'AnnualRevenue', 'BillingState')); 7 | 8 | // prettier-ignore 9 | Query q = Query.of('Account') 10 | .selectBy('Name', convertCurrency('AnnualRevenue'), 'BillingState') 11 | .selectParent('Parent', parentAccount) 12 | .selectChild('Contacts', Query.of('Contact').selectBy('Name')) 13 | .whereBy(orx() 14 | .add(andx() 15 | .add(gt('AnnualRevenue', CURRENCY('CNY', 1000))) 16 | .add(eq('BillingCountry', 'China')) 17 | .add(eq('BillingState', 'Beijing')) 18 | ) 19 | .add(andx() 20 | .add(lt('AnnualRevenue', CURRENCY('CNY', 1000))) 21 | .add(eq('BillingCountry', 'China')) 22 | .add(eq('BillingState', 'Shanghai')) 23 | ) 24 | ) 25 | // .groupBy('Name', calendarMonth('createdDate'), 'BillingState') 26 | // .havingBy(gt(SUM('AnnualRevenue'), CURRENCY('CNY', 1000))) 27 | .orderBy(orderBy('Name').ascending().nullsLast(), orderBy('AnnualRevenue').descending()) 28 | .limitx(10) 29 | .forView(); 30 | System.debug(q.buildSOQL()); 31 | System.debug(q.run()); 32 | } 33 | } 34 | 35 | Integer startCPU = Limits.getCpuTime(); 36 | for (Integer i = 0; i < 1; i++) { 37 | QueryTest test = new QueryTest(); 38 | test.test(); 39 | } 40 | Integer endCPU = Limits.getCpuTime(); 41 | System.debug(LoggingLevel.INFO, '(CPU): ' + (endCPU - startCPU)); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Jeff Jin 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Apex Query(Apex 查询构建器) 2 | 3 | ![](https://img.shields.io/badge/version-3.0.5-brightgreen.svg) ![](https://img.shields.io/badge/build-passing-brightgreen.svg) ![](https://img.shields.io/badge/coverage-99%25-brightgreen.svg) 4 | 5 | 一个用于动态 SOQL 构建的查询生成器。 6 | 7 | **支持:** 如果你觉得这个库有帮助,请考虑在朋友圈上分享,或推荐给你的朋友或同事。 8 | 9 | | 环境 | 安装链接 | 版本 | 10 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | 11 | | 正式/开发环境 | | ver 3.0.5 | 12 | | 沙盒环境 | | ver 3.0.5 | 13 | 14 | --- 15 | 16 | ### 翻译 17 | 18 | - [英语](../README.md) 19 | 20 | ### 版本 v3.0.0 21 | 22 | 2.0 版本过于复杂,难以维护和使用。3.0 版本追求简洁,虽然改进空间有限。在重构过程中,我也思考过简单字符串拼接是否足够。 23 | 24 | - **主要更新** 25 | - 性能提升约 30%。这是一个温和的提升,大约 7 vs 10 的 CPU 时间差。 26 | - 字符串现在是一等公民,强类型检查已移除。 27 | - 移除了很少用到的特性。 28 | - **新特性**: 29 | - [查询组合](#22-查询组合) 30 | - [查询链式调用](#23-查询链式调用) 31 | - [查询模板](#24-查询模板) 32 | 33 | --- 34 | 35 | ## 目录 36 | 37 | - [1. 命名规范](#1-命名规范) 38 | - [1.1 可读性](#11-可读性) 39 | - [1.2 命名冲突](#12-命名冲突) 40 | - [2. 概览](#2-概览) 41 | - [2.1 Query 类](#21-query-类) 42 | - [2.2 查询组合](#22-查询组合) 43 | - [2.3 关系查询](#23-关系查询) 44 | - [2.4 查询模板](#24-查询模板) 45 | - [2.5 查询执行](#25-查询执行) 46 | - [3. 关键字](#3-关键字) 47 | - [3.1 From 语句](#31-from-语句) 48 | - [3.2 Select 语句](#32-select-语句) 49 | - [3.3 Where 语句](#33-where-语句) 50 | - [3.4 Order By 语句](#34-order-by-语句) 51 | - [3.5 Group By 语句](#35-group-by-语句) 52 | - [3.6 其他关键字](#36-其他关键字) 53 | - [4. 过滤器](#4-过滤器) 54 | - [4.1 比较过滤器](#41-比较过滤器) 55 | - [4.2 逻辑过滤器](#42-逻辑过滤器) 56 | - [5. 函数](#5-函数) 57 | - [5.1 聚合函数](#51-聚合函数) 58 | - [5.2 日期/时间函数](#52-日期时间函数) 59 | - [5.3 其他函数](#53-其他函数) 60 | - [6. 字面量](#6-字面量) 61 | - [6.1 日期字面量](#61-日期字面量) 62 | - [6.2 货币字面量](#62-货币字面量) 63 | - [7. 许可证](#7-许可证) 64 | 65 | ## 1. 命名规范 66 | 67 | ### 1.1 可读性 68 | 69 | 以下命名规范用于提升查询的可读性: 70 | 71 | | | 描述 | 命名规范 | 理由 | 示例 | 72 | | ---------- | -------------------- | ---------- | ---------------------------------- | ------------------------------------------------------------------ | 73 | | **关键字** | SOQL 的核心结构 | camelCase | 关键字应与 SOQL 语义一一对应 | `selectBy`, `whereBy`, `groupBy`, `havingBy`, `orderBy` | 74 | | **操作符** | 逻辑和比较操作符 | 小写 | 操作符应简洁,尽量用缩写 | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `inx`, `nin` | 75 | | **函数** | 聚合、格式化、日期等 | camelCase | 驼峰风格与 Apex 方法一致,易于输入 | `count`, `max`, `toLabel`, `format`, `calendarMonth`, `fiscalYear` | 76 | | **字面量** | 仅日期和货币字面量 | UPPER_CASE | 常量风格,便于区分 | `LAST_90_DAYS()`, `LAST_N_DAYS(30)`, `CURRENCY('USD', 100)` | 77 | 78 | ### 1.2 命名冲突 79 | 80 | 为避免与现有关键字或操作符冲突,遵循以下规范: 81 | 82 | 1. SOQL 关键字采用 `By()` 格式,如 `selectBy`, `whereBy`, `groupBy`, `havingBy`, `orderBy`。 83 | 2. 有冲突的操作符采用 `x()` 格式,如 `orx()`, `andx()`, `inx()`, `likex()`。 84 | 85 | ## 2. 概览 86 | 87 | ### 2.1 Query 类 88 | 89 | 所有操作符和函数都作为 Query 类的静态方法实现。每次都用 `Query.` 前缀会很繁琐,建议继承 Query 类后直接调用。 90 | 91 | ```java 92 | public with sharing class AccountQuery extends Query { 93 | public List listAccount() { 94 | return (List) Query.of('Account') 95 | .selectBy('Name', toLabel('Industry')) 96 | .whereBy(orx() 97 | .add(andx() 98 | .add(gt('AnnualRevenue', 1000)) 99 | .add(eq('BillingState', 'Beijing'))) 100 | .add(andx() 101 | .add(lt('AnnualRevenue', 1000)) 102 | .add(eq('BillingState', 'Shanghai'))) 103 | ) 104 | .orderBy(orderField('AnnualRevenue').descending().nullsLast()) 105 | .run(); 106 | } 107 | } 108 | ``` 109 | 110 | 上述查询等价于以下 SOQL: 111 | 112 | ```sql 113 | SELECT Name, toLabel(Industry) 114 | FROM Account 115 | WHERE ((AnnualRevenue > 1000 AND BillingState = 'Beijing') 116 | OR (AnnualRevenue < 1000 AND BillingState = 'Shanghai')) 117 | ORDER BY AnnualRevenue DESC NULLS LAST 118 | ``` 119 | 120 | ### 2.2 查询组合 121 | 122 | 本库的一大优势在于支持将完整查询灵活拆分为多个片段,并可根据实际需求自由组合、调整顺序。比如,上述 SOQL 查询可以动态地分解为若干部分,然后按需组装: 123 | 124 | ```java 125 | public with sharing class AccountQuery extends Query { 126 | public List runQuery(List additionalFields, 127 | Decimal beijingRevenue, 128 | Decimal shanghaiRevenue) { 129 | 130 | Query q = baseQuery(); 131 | q.selectBy(additionalFields); 132 | /** 133 | * 不用担心 where 条件中的 andx() 或 orx() 为空或只有一个过滤器,SOQL 会自动正确生成。 134 | */ 135 | q.whereBy(orx()); 136 | q.whereBy().add(beijingRevenueGreaterThan(beijingRevenue)); 137 | q.whereBy().add(shanghaiRevenueLessThan(shanghaiRevenue)); 138 | return q.run(); 139 | } 140 | 141 | public Query baseQuery() { 142 | Query q = Query.of('Account'); 143 | q.selectBy('Name'); 144 | q.selectBy(toLabel('Industry')); 145 | return q.orderBy(orderField('AnnualRevenue').descending().nullsLast()); 146 | } 147 | 148 | public Filter beijingRevenueGreaterThan(Decimal revenue) { 149 | return andx() 150 | .add(gt('AnnualRevenue', revenue)) 151 | .add(eq('BillingState', 'Beijing')); 152 | } 153 | 154 | public Filter shanghaiRevenueLessThan(Decimal revenue) { 155 | return andx() 156 | .add(lt('AnnualRevenue', revenue)) 157 | .add(eq('BillingState', 'Shanghai')); 158 | } 159 | } 160 | ``` 161 | 162 | ### 2.3 关系查询 163 | 164 | 父子关系可通过链式调用组装,支持多级父子链(分组查询除外)。 165 | 166 | ```java 167 | public with sharing class AccountQuery extends Query { 168 | public List listAccount() { 169 | Query parentQuery = Query.of('Account') 170 | .selectBy('Name', format(convertCurrency('AnnualRevenue'))); 171 | Query childQuery = Query.of('Contact').selectBy('Name', 'Email'); 172 | 173 | return (List) Query.of('Account') 174 | .selectBy('Name', toLabel('Industry')) 175 | .selectParent('Parent', parentQuery) // 父级查询 176 | .selectChild('Contacts', childQuery) // 子级查询 177 | .run(); 178 | } 179 | } 180 | ``` 181 | 182 | 上述查询等价于以下 SOQL: 183 | 184 | ```sql 185 | SELECT Name, toLabel(Industry), 186 | Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)) -- 父级查询 187 | (SELECT Name, Email FROM Contacts) -- 子级查询 188 | FROM Account 189 | ``` 190 | 191 | 不使用链式调用,也可以这样实现: 192 | 193 | ```java 194 | public with sharing class AccountQuery extends Query { 195 | public List listAccount() { 196 | return (List) Query.of('Account') 197 | .selectBy('Name', toLabel('Industry'), 198 | 'Parent.Name', format(convertCurrency('Parent.AnnualRevenue')), 199 | '(SELECT Name, Email FROM Contacts)') 200 | .run(); 201 | } 202 | } 203 | ``` 204 | 205 | ### 2.4 查询模板 206 | 207 | 如需用不同绑定变量多次执行同一 Query,建议用如下模式。**注意**:模板需用 `var(变量名)`。 208 | 209 | ```java 210 | public with sharing class AccountQuery extends Query { 211 | public static Query accQuery { 212 | get { 213 | if (accQuery == null) { 214 | accQuery = Query.of('Account') 215 | .selectBy('Name', toLabel('Industry')) 216 | .selectChild('Contacts', Query.of('Contact') 217 | .selectBy('Name', 'Email') 218 | .whereBy(likex('Email', var('emailSuffix'))) // 变量 1 219 | ) 220 | .whereBy(andx() 221 | .add(gt('AnnualRevenue', var('revenue'))) // 变量 2 222 | .add(eq('BillingState', var('state'))) // 变量 3 223 | ); 224 | } 225 | return accQuery; 226 | } 227 | set; 228 | } 229 | 230 | public List listAccount(String state, Decimal revenue) { 231 | return (List) accQuery.run(new Map { 232 | 'revenue' => revenue, 233 | 'state' => state, 234 | 'emailSuffix' => '%gmail.com' 235 | }); 236 | } 237 | } 238 | ``` 239 | 240 | 上述查询等价于以下 SOQL: 241 | 242 | ```sql 243 | SELECT Name, toLabel(Industry) 244 | (SELECT Name, Email FROM Contacts WHERE Email LIKE :emailSuffix) 245 | FROM Account 246 | WHERE (AnnualRevenue > :revenue AND BillingState = :state) 247 | ``` 248 | 249 | ### 2.5 查询执行 250 | 251 | 默认以 `AccessLevel.SYSTEM_MODE` 执行: 252 | 253 | | | API | 带绑定变量的 API | 返回类型 | 254 | | ----- | -------------- | ------------------------- | ------------------------------------- | 255 | | **1** | `run()` | `run(bindingVars)` | `List` | 256 | | **2** | `getLocator()` | `getLocator(bindingVars)` | `Database.QueryLocator` | 257 | | **3** | `getCount()` | `getCount(bindingVars)` | `Integer`,需配合 `selectBy(count())` | 258 | 259 | 以任意 `AccessLevel` 执行,如 `AccessLevel.USER_MODE`: 260 | 261 | | | API | 带访问级别的 API | 返回类型 | 262 | | ----- | ------------------------- | -------------------------------------- | ------------------------------------- | 263 | | **1** | `run(AccessLevel)` | `run(bindingVars, AccessLevel)` | `List` | 264 | | **2** | `getLocator(AccessLevel)` | `getLocator(bindingVars, AccessLevel)` | `Database.QueryLocator` | 265 | | **3** | `getCount(AccessLevel)` | `getCount(bindingVars, AccessLevel)` | `Integer`,需配合 `selectBy(count())` | 266 | 267 | ## 3. 关键字 268 | 269 | ### 3.1 From 语句 270 | 271 | 所有查询通过 `Query.of(String objectName)` 创建。若未指定字段,默认选取 `Id` 字段。 272 | 273 | ```java 274 | Query accountQuery = Query.of('Account'); 275 | ``` 276 | 277 | 上述查询等价于以下 SOQL: 278 | 279 | ```sql 280 | SELECT Id FROM Account 281 | ``` 282 | 283 | ### 3.2 Select 语句 284 | 285 | | | API | 描述 | 286 | | ----- | ------------------------------------------------------- | --------------------- | 287 | | **1** | `selectBy(Object ... )` | 最多选 10 个字段/函数 | 288 | | **2** | `selectBy(List)` | 选取字段/函数列表 | 289 | | **3** | `selectParent(String relationshipName, Query subQuery)` | 父级查询 | 290 | | **4** | `selectChild(String relationshipName, Query subQuery)` | 子级查询 | 291 | 292 | ```java 293 | Query accountQuery = Query.of('Account') 294 | .selectBy('Name', toLabel('Industry')) 295 | .selectBy(new List { 'Owner.Name', FORMAT('CreatedDate') }) 296 | .selectParent('Parent', Query.of('Account') 297 | .selectBy('Name', format(convertCurrency('AnnualRevenue')))) 298 | .selectChild('Contacts', Query.of('Contact').selectBy('Name', 'Email')); 299 | ``` 300 | 301 | 上述查询等价于以下 SOQL: 302 | 303 | ```sql 304 | SELECT Name, toLabel(Industry), 305 | Owner.Name, FORMAT(CreatedDate) 306 | Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)) 307 | (SELECT Name, Email FROM Contacts) 308 | FROM Account 309 | ``` 310 | 311 | ### 3.3 Where 语句 312 | 313 | #### 设置根过滤器 314 | 315 | `whereBy(Filter filter)` 可接收比较表达式或逻辑表达式。 316 | 317 | ```java 318 | Query accountQuery = Query.of('Account') 319 | .selectBy('Name') 320 | .whereBy(gt('AnnualRevenue', 2000)); // #1. 比较过滤器 321 | 322 | Query accountQuery = Query.of('Account') 323 | .selectBy('Name') 324 | .whereBy(andx() // #2. 逻辑过滤器 325 | .add(gt('AnnualRevenue', 2000)) 326 | .add(lt('AnnualRevenue', 6000)) 327 | ); 328 | ``` 329 | 330 | #### 获取根过滤器 331 | 332 | 用 `whereBy()` 获取根过滤器,可后续追加分支过滤器。 333 | 334 | ```java 335 | // 类型 #1: 默认 AND 逻辑过滤器 336 | Query accountQuery = Query.of('Account').selectBy('Name') 337 | .whereBy(gt('AnnualRevenue', 2000)); 338 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 339 | 340 | // 类型 #2: 复用先有逻辑过滤器 341 | Query accountQuery = Query.of('Account').selectBy('Name') 342 | .whereBy(andx().add(gt('AnnualRevenue', 2000))); 343 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 344 | 345 | // 类型 #3: 默认 AND 逻辑过滤器 346 | Query accountQuery = Query.of('Account').selectBy('Name'); 347 | accountQuery.whereBy().add(gt('AnnualRevenue', 2000)); 348 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 349 | ``` 350 | 351 | 以上三种类型都等价于以下 SOQL:: 352 | 353 | ```sql 354 | SELECT Name FROM Account Where AnnualRevenue > 2000 AND AnnualRevenue < 6000 355 | ``` 356 | 357 | ### 3.4 Order By 语句 358 | 359 | | | API | 描述 | 360 | | ----- | ----------------------- | -------------- | 361 | | **1** | `orderBy(Object...)` | 最多 10 个字段 | 362 | | **2** | `orderBy(List)` | 字段列表 | 363 | 364 | 参数可为字符串或函数。 365 | 366 | ```java 367 | Query accountQuery = Query.of('Account') 368 | .selectBy('Name', toLabel('Industry')) 369 | .orderBy( 370 | 'BillingCountry DESC NULLS LAST', 371 | distance('ShippingAddress', Location.newInstance(37.775000, -122.41800), 'km') 372 | ) 373 | .orderBy(new List{ 'Owner.Profile.Name' }); 374 | ``` 375 | 376 | 也可用 `orderField()` 创建参数,上述查询等价于: 377 | 378 | ```java 379 | Query accountQuery = Query.of('Account') 380 | .selectBy('Name', toLabel('Industry')) 381 | .orderBy( 382 | orderField('BillingCountry').descending().nullsLast(), 383 | orderField(distance('ShippingAddress', Location.newInstance(37.775000, -122.41800), 'km')) 384 | ) 385 | .orderBy(new List{ orderField('Owner.Profile.Name') }); 386 | ``` 387 | 388 | 上述查询等价于以下 SOQL: 389 | 390 | ```sql 391 | SELECT Name, toLabel(Industry) 392 | FROM Account 393 | ORDER BY BillingCountry DESC NULLS LAST, 394 | DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), 'km'), 395 | Owner.Profile.Name 396 | ``` 397 | 398 | ### 3.5 Group By 语句 399 | 400 | | | API | 描述 | 401 | | ----- | ----------------------- | -------------- | 402 | | **1** | `groupBy(String ...)` | 最多 10 个字段 | 403 | | **2** | `groupBy(List)` | 字段列表 | 404 | 405 | ```java 406 | Query accountQuery = Query.of('Account') 407 | .selectBy(avg('AnnualRevenue')) 408 | .selectBy(sum('AnnualRevenue', 'RevenueSUM')) // 可选别名 409 | .groupBy('BillingCountry', calendarYear('CreatedDate')) 410 | .groupBy(new List{ calendarMonth('CreatedDate') }); 411 | ``` 412 | 413 | 上述查询等价于以下 SOQL: 414 | 415 | ```sql 416 | SELECT AVG(AnnualRevenue), SUM(AnnualRevenue) RevenueSUM 417 | FROM Account 418 | GROUP BY BillingCountry, CALENDAR_YEAR(CreatedDate), CALENDAR_MONTH(CreatedDate) 419 | ``` 420 | 421 | #### Having 子句 422 | 423 | 聚合结果可用 `havingBy()` 和 `orderBy()` 过滤和排序。`havingBy(Filter filter)` 用法同 `whereBy()`。 424 | 425 | ```java 426 | Query accountQuery = Query.of('Account') 427 | .selectBy(avg('AnnualRevenue'), sum('AnnualRevenue')) 428 | .groupBy('BillingCountry', 'BillingState') 429 | .rollup() 430 | .havingBy(gt(sum('AnnualRevenue'), 2000)) 431 | .orderBy(avg('AnnualRevenue'), sum('AnnualRevenue')); 432 | ``` 433 | 434 | 上述查询等价于以下 SOQL: 435 | 436 | ```sql 437 | SELECT AVG(AnnualRevenue), SUM(AnnualRevenue) 438 | FROM Account 439 | GROUP BY ROLLUP(BillingCountry, BillingState) 440 | HAVING SUM(AnnualRevenue) > 2000 441 | ORDER BY AVG(AnnualRevenue), SUM(AnnualRevenue) 442 | ``` 443 | 444 | #### Rollup 汇总 445 | 446 | 可选的 `rollup()` 或 `cube()` 方法可生成小计或总计。 447 | 448 | ```java 449 | Query accountQuery = Query.of('Account') 450 | .selectBy(AVG('AnnualRevenue'), SUM('AnnualRevenue')) 451 | .groupBy('BillingCountry', 'BillingState') 452 | .rollup(); 453 | ``` 454 | 455 | ### 3.6 其他关键字 456 | 457 | | API | 生成格式 | 458 | | ------------------- | ----------------- | 459 | | `limitx(Integer n)` | `LIMIT n` | 460 | | `offset(Integer n)` | `OFFSET n` | 461 | | `forView()` | `FOR VIEW` | 462 | | `forReference()` | `FOR REFERENCE` | 463 | | `forUpdate()` | `FOR UPDATE` | 464 | | `updateTracking()` | `UPDATE TRACKING` | 465 | | `updateViewstat()` | `UPDATE VIEWSTAT` | 466 | 467 | ## 4. 过滤器 468 | 469 | ### 4.1 比较过滤器 470 | 471 | | SOQL 操作符 | Apex Query 操作符 | 生成格式 | 472 | | ------------ | --------------------------------- | ------------------------------- | 473 | | **=** | `eq(param, value)` | `param = value` | 474 | | **!=** | `ne(param, value)` | `param != value` | 475 | | **<** | `lt(param, value)` | `param < value` | 476 | | **<=** | `lte(param, value)` | `param <= value` | 477 | | **>** | `gt(param, value)` | `param > value` | 478 | | **>=** | `gte(param, value)` | `param >= value` | 479 | | **BETWEEN** | `between(param, min, max)` | `param >= min AND param <= max` | 480 | | **LIKE** | `likex(param, String value)` | `param LIKE value` | 481 | | **NOT LIKE** | `nlike(param, String value)` | `(NOT param LIKE value)` | 482 | | **IN** | `inx(param, List values)` | `param IN :values` | 483 | | **NOT IN** | `nin(param, List values)` | `param NOT IN :values` | 484 | | **INCLUDES** | `includes(param, List)` | `param INCLUDES (:v1, :v2)` | 485 | | **EXCLUDES** | `excludes(param, List)` | `param EXCLUDES (:v1, :v2)` | 486 | 487 | 第一个参数可为: 488 | 489 | 1. 字段名,如 `AnnualRevenue`,`'Owner.Profile.Name'`。 490 | 2. 函数,如: 491 | - `toLabel()` 492 | - 日期函数 `calendarMonth('CreatedDate')` 493 | - 距离函数 `distance('ShippingAddress', Location.newInstance(37.775001, -122.41801), 'km')` 494 | - 聚合函数 `sum('AnnualRevenue')`(仅用于 having) 495 | 496 | #### 与 sObject 列表比较 497 | 498 | `inx()` 和 `nin()` 也可用于 Id 字段与 sObject 列表比较。 499 | 500 | ```java 501 | List accounts = ... ; // 其他地方查询的账户 502 | List contacts = List Query.of('Contact') 503 | .selectBy('Name', toLabel('Account.Industry')) 504 | .whereBy(inx('AccountId', accounts)) 505 | .run(); 506 | ``` 507 | 508 | 上述查询等价于以下 SOQL: 509 | 510 | ```sql 511 | SELECT Name, toLabel(Account.Industry) 512 | FROM Contact 513 | WHERE AccountId IN :accounts 514 | ``` 515 | 516 | ### 4.2 逻辑过滤器 517 | 518 | | AND | 生成格式 | 519 | | ------------------------------------- | ----------------- | 520 | | `andx().add(f1).add(f2)...` | `(f1 AND f2 ...)` | 521 | | `andx().addAll(List filters)` | `(f1 AND f2 ...)` | 522 | | **OR** | | 523 | | `orx().add(f1).add(f2)...` | `(f1 OR f2 ...)` | 524 | | `orx().addAll(List filters)` | `(f1 OR f2 ...)` | 525 | | **NOT** | | 526 | | `notx(Filter filter)` | `NOT(filter)` | 527 | 528 | 示例: 529 | 530 | ```java 531 | Query.Filter revenueGreaterThan = gt('AnnualRevenue', 1000); 532 | 533 | Query.LogicalFilter shanghaiRevenueLessThan = andx().addAll(new List { 534 | lt('AnnualRevenue', 1000), 535 | eq('BillingState', 'Shanghai') 536 | }); 537 | 538 | Query.LogicalFilter orFilter = orx() 539 | .add(andx() 540 | .add(revenueGreaterThan) 541 | .add(eq('BillingState', 'Beijing')) 542 | ) 543 | .add(shanghaiRevenueLessThan); 544 | ``` 545 | 546 | 上述查询等价于以下 SOQL: 547 | 548 | ```sql 549 | (AnnualRevenue > 1000 AND BillingState = 'Beijing') 550 | OR (AnnualRevenue < 1000 AND BillingState = 'Shanghai') 551 | ``` 552 | 553 | ## 5. 函数 554 | 555 | ### 5.1 聚合函数 556 | 557 | | 静态方法 | 生成格式 | 558 | | ----------------------------- | ----------------------------- | 559 | | `count(field)` | `COUNT(field)` | 560 | | `count(field, alias)` | `COUNT(field) alias` | 561 | | `countDistinct(field)` | `COUNT_DISTINCT(field)` | 562 | | `countDistinct(field, alias)` | `COUNT_DISTINCT(field) alias` | 563 | | `grouping(field)` | `GROUPING(field)` | 564 | | `grouping(field, alias)` | `GROUPING(field) alias` | 565 | | `sum(field)` | `SUM(field)` | 566 | | `sum(field, alias)` | `SUM(field) alias` | 567 | | `avg(field)` | `AVG(field)` | 568 | | `avg(field, alias)` | `AVG(field) alias` | 569 | | `max(field)` | `MAX(field)` | 570 | | `max(field, alias)` | `MAX(field) alias` | 571 | | `min(field)` | `MIN(field)` | 572 | | `min(field, alias)` | `MIN(field) alias` | 573 | 574 | ### 5.2 日期/时间函数 575 | 576 | 以下函数用于日期、时间和日期时间字段。 577 | 578 | ```java 579 | Query.of('Opportunity') 580 | .selectBy(calendarYear('CreatedDate'), sum('Amount')) 581 | .whereBy(gt(calendarYear('CreatedDate'), 2000)) 582 | .groupBy(calendarYear('CreatedDate')); 583 | ``` 584 | 585 | 上述查询等价于以下 SOQL: 586 | 587 | ```sql 588 | SELECT CALENDAR_YEAR(CreatedDate), SUM(Amount) 589 | FROM Opportunity 590 | WHERE CALENDAR_YEAR(CreatedDate) > 2000 591 | GROUP BY CALENDAR_YEAR(CreatedDate) 592 | ``` 593 | 594 | | 静态方法 | 描述 | 595 | | ------------------------ | ------------------------------------ | 596 | | `convertTimezone(field)` | 转换为用户时区,仅能用于日期函数内部 | 597 | | `calendarMonth(field)` | 返回日期字段的月份 | 598 | | `calendarQuarter(field)` | 返回日期字段的季度 | 599 | | `calendarYear(field)` | 返回日期字段的年份 | 600 | | `dayInMonth(field)` | 返回日期字段的天 | 601 | | `dayInWeek(field)` | 返回日期字段的星期几 | 602 | | `dayInYear(field)` | 返回日期字段的年内天数 | 603 | | `dayOnly(field)` | 返回日期时间字段的日期部分 | 604 | | `fiscalMonth(field)` | 返回日期字段的财务月份 | 605 | | `fiscalQuarter(field)` | 返回日期字段的财务季度 | 606 | | `fiscalYear(field)` | 返回日期字段的财务年份 | 607 | | `hourInDay(field)` | 返回日期时间字段的小时 | 608 | | `weekInMonth(field)` | 返回日期字段的月内周数 | 609 | | `weekInYear(field)` | 返回日期字段的年内周数 | 610 | 611 | ### 5.3 其他函数 612 | 613 | 示例: 614 | 615 | ```java 616 | Query.Filter filter = lt(distance('ShippingAddreess', 617 | Location.newInstance(37.775000, -122.41800)), 20, 'km'); 618 | ``` 619 | 620 | | 静态方法 | 生成格式 | 621 | | -------------------------------------------- | --------------------------------------------------------------- | 622 | | `toLabel(field)` | `toLabel(field)` | 623 | | `format(field)` | `FORMAT(field)` | 624 | | `convertCurrency(field)` | `convertCurrency(field)`,可嵌套于 format() | 625 | | `distance(field, Location geo, string unit)` | `DISTANCE(ShippingAddress, GEOLOCATION(37.775,-122.418), 'km')` | 626 | 627 | ## 6. 字面量 628 | 629 | ### 6.1 日期字面量 630 | 631 | 以下为 Salesforce 支持的所有日期字面量([官方文档](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm)): 632 | 633 | ```java 634 | Query.Filter filter = andx() 635 | .add(eq('LastModifiedDate', YESTERDAY())) 636 | .add(gt('CreatedDate', LAST_N_DAYS(5))) 637 | ); 638 | ``` 639 | 640 | > `YESTERDAY()`, `TODAY()`, `TOMORROW()`, `LAST_WEEK()`, `THIS_WEEK()`, `NEXT_WEEK()`, `LAST_MONTH()`, `THIS_MONTH()`, `NEXT_MONTH()`, `LAST_90_DAYS()`, `NEXT_90_DAYS()`, `THIS_QUARTER()`, `LAST_QUARTER()`, `NEXT_QUARTER()`, `THIS_YEAR()`, `LAST_YEAR()`, `NEXT_YEAR()`, `THIS_FISCAL_QUARTER()`, `LAST_FISCAL_QUARTER()`, `NEXT_FISCAL_QUARTER()`, `THIS_FISCAL_YEAR()`, `LAST_FISCAL_YEAR()`, `NEXT_FISCAL_YEAR()` 641 | > 642 | > `LAST_N_DAYS(Integer n)`, `NEXT_N_DAYS(Integer n)`, `N_DAYS_AGO(Integer n)`, `NEXT_N_WEEKS(Integer n)`, `LAST_N_WEEKS(Integer n)`, `N_WEEKS_AGO(Integer n)`, `NEXT_N_MONTHS(Integer n)`, `LAST_N_MONTHS(Integer n)`, `N_MONTHS_AGO(Integer n)`, `NEXT_N_QUARTERS(Integer n)`, `LAST_N_QUARTERS(Integer n)`, `N_QUARTERS_AGO(Integer n)`, `NEXT_N_YEARS(Integer n)`, `LAST_N_YEARS(Integer n)`, `N_YEARS_AGO(Integer n)`, `NEXT_N_FISCAL_QUARTERS(Integer n)`, `N_FISCAL_QUARTERS_AGO(Integer n)`, `NEXT_N_FISCAL_YEARS(Integer n)`, `LAST_N_FISCAL_YEARS(Integer n)`, `N_FISCAL_YEARS_AGO(Integer n)` 643 | 644 | ### 6.2 货币字面量 645 | 646 | Salesforce 支持的货币 ISO 代码见[官方文档](https://help.salesforce.com/s/articleView?language=zh_CN&id=sf.admin_supported_currencies.htm)。 647 | 648 | ```java 649 | Query.Filter filter = orx() 650 | .add(eq('AnnualRevenual', CURRENCY('USD', 2000))) 651 | .add(eq('AnnualRevenual', CURRENCY('CNY', 2000))) 652 | .add(eq('AnnualRevenual', CURRENCY('TRY', 2000))) 653 | ); 654 | ``` 655 | 656 | ## 7. 许可证 657 | 658 | Apache 2.0 659 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apex Query 2 | 3 | ![](https://img.shields.io/badge/version-3.0.5-brightgreen.svg) ![](https://img.shields.io/badge/build-passing-brightgreen.svg) ![](https://img.shields.io/badge/coverage-99%25-brightgreen.svg) 4 | 5 | A query builder for dynamic SOQL construction. 6 | 7 | **Support:** If you find this library helpful, please consider sharing it on Twitter or recommending it to your friends or colleagues. 8 | 9 | | Environment | Installation Link | Version | 10 | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | 11 | | Production, Developer | | ver 3.0.5 | 12 | | Sandbox | | ver 3.0.5 | 13 | 14 | --- 15 | 16 | ### Translations 17 | 18 | - [简体中文](docs/README.zh-CN.md) 19 | 20 | ### Release v3.0.0 21 | 22 | Version 2.0 was too complex to maintain and use. Version 3.0 aims for simplicity, though there is limited room for improvement. During the redesign, I also considered whether simple string concatenation would suffice. 23 | 24 | - **Key Updates** 25 | - Performance improved by 30%. This is a modest gain, roughly a 7 vs 10 CPU time difference. 26 | - Strings are now first-class citizens, and strong type checking has been removed. 27 | - Rarely used features have been removed. 28 | - **New Features**: 29 | - [Query Composition](#22-query-composition) 30 | - [Query Chaining](#23-query-chaining) 31 | - [Query Template](#24-query-template) 32 | 33 | --- 34 | 35 | ## Table of Contents 36 | 37 | - [1. Naming Conventions](#1-naming-conventions) 38 | - [1.1 Naming Readability](#11-naming-readability) 39 | - [1.2 Naming Confliction](#12-naming-confliction) 40 | - [2. Overview](#2-overview) 41 | - [2.1 Query Class](#21-query-class) 42 | - [2.2 Query Composition](#22-query-composition) 43 | - [2.3 Query Chaining](#23-query-chaining) 44 | - [2.4 Query Template](#24-query-template) 45 | - [2.5 Query Execution](#25-query-execution) 46 | - [3. Keywords](#3-keywords) 47 | - [3.1 From Statement](#31-from-statement) 48 | - [3.2 Select Statement](#32-select-statement) 49 | - [3.3 Where Statement](#33-where-statement) 50 | - [3.4 Order By Statement](#34-order-by-statement) 51 | - [3.5 Group By Statement](#35-group-by-statement) 52 | - [3.6 Other Keywords](#36-other-keywords) 53 | - [4. Filters](#4-filters) 54 | - [4.1 Comparison Filter](#41-comparison-filter) 55 | - [4.2 Logical Filter](#42-logical-filter) 56 | - [5. Functions](#5-functions) 57 | - [5.1 Aggregate Functions](#51-aggregate-functions) 58 | - [5.2 Date/Time Functions](#52-datetime-functions) 59 | - [5.3 Other Functions](#53-other-functions) 60 | - [6. Literals](#6-literals) 61 | - [6.1 Date Literals](#61-date-literals) 62 | - [6.2 Currency Literals](#62-currency-literals) 63 | - [7. License](#7-license) 64 | 65 | ## 1. Naming Conventions 66 | 67 | ### 1.1 Naming Readability 68 | 69 | The following naming conventions are used to improve query readability: 70 | 71 | | | Description | Naming Convention | Reasoning | Example | 72 | | ------------- | --------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | 73 | | **Keywords** | Core structures of SOQL. | camelCase | Keywords should clearly correspond to their SOQL equivalents. | `selectBy`, `whereBy`, `groupBy`, `havingBy`, `orderBy` | 74 | | **Operators** | Logical and comparison operators. | lowercase | Operators should be concise and operator-like, using abbreviations where appropriate. | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `inx`, `nin` | 75 | | **Functions** | Used for aggregation, formatting, date access, etc. | camelCase | Camel case aligns with Apex method names and is easy to type. | `count`, `max`, `toLabel`, `format`, `calendarMonth`, `fiscalYear` | 76 | | **Literals** | Only date and currency literals. | UPPER_CASE | These are constant-like values, so static constant variable naming is preferred. | `LAST_90_DAYS()`, `LAST_N_DAYS(30)`, `CURRENCY('USD', 100)` | 77 | 78 | ### 1.2 Naming Confliction 79 | 80 | To avoid conflicts with existing keywords or operators, follow these conventions: 81 | 82 | 1. Use the `By()` format for SOQL keywords, such as `selectBy`, `whereBy`, `groupBy`, `havingBy`, `orderBy`. 83 | 2. Use the `x()` format for conflicting operators only, such as `orx()`, `andx()`, `inx()`, `likex()`. 84 | 85 | ## 2. Overview 86 | 87 | ### 2.1 Query Class 88 | 89 | All operators and functions are implemented as static methods of the Query class. Referencing them with a `Query.` prefix each time can be tedious. When possible, extend the `Query` class so all static methods can be referenced directly. 90 | 91 | ```java 92 | public with sharing class AccountQuery extends Query { 93 | public List listAccount() { 94 | return (List) Query.of('Account') 95 | .selectBy('Name', toLabel('Industry')) 96 | .whereBy(orx() 97 | .add(andx() 98 | .add(gt('AnnualRevenue', 1000)) 99 | .add(eq('BillingState', 'Beijing'))) 100 | .add(andx() 101 | .add(lt('AnnualRevenue', 1000)) 102 | .add(eq('BillingState', 'Shanghai'))) 103 | ) 104 | .orderBy(orderField('AnnualRevenue').descending().nullsLast()) 105 | .run(); 106 | } 107 | } 108 | ``` 109 | 110 | Equivalent to the following SOQL: 111 | 112 | ```sql 113 | SELECT Name, toLabel(Industry) 114 | FROM Account 115 | WHERE ((AnnualRevenue > 1000 AND BillingState = 'Beijing') 116 | OR (AnnualRevenue < 1000 AND BillingState = 'Shanghai')) 117 | ORDER BY AnnualRevenue DESC NULLS LAST 118 | ``` 119 | 120 | ### 2.2 Query Composition 121 | 122 | The main advantage of this library is its flexibility: you can split a complete query into multiple segments, freely combine or reorder them as needed, and assemble the final query conditionally. For example, the SOQL above can be broken down into several dynamic components and then composed together as required: 123 | 124 | ```java 125 | public with sharing class AccountQuery extends Query { 126 | public List runQuery(List additionalFields, 127 | Decimal beijingRevenue, 128 | Decimal shanghaiRevenue) { 129 | 130 | Query q = baseQuery(); 131 | q.selectBy(additionalFields); 132 | /** 133 | * Don't worry if `andx()` or `orx()` in the where condition 134 | * have zero or only one filter; SOQL will always be built correctly. 135 | */ 136 | q.whereBy(orx()); 137 | q.whereBy().add(beijingRevenueGreaterThan(beijingRevenue)); 138 | q.whereBy().add(shanghaiRevenueLessThan(shanghaiRevenue)); 139 | return q.run(); 140 | } 141 | 142 | public Query baseQuery() { 143 | Query q = Query.of('Account'); 144 | q.selectBy('Name'); 145 | q.selectBy(toLabel('Industry')); 146 | return q.orderBy(orderField('AnnualRevenue').descending().nullsLast()); 147 | } 148 | 149 | public Filter beijingRevenueGreaterThan(Decimal revenue) { 150 | return andx() 151 | .add(gt('AnnualRevenue', revenue)) 152 | .add(eq('BillingState', 'Beijing')); 153 | } 154 | 155 | public Filter shanghaiRevenueLessThan(Decimal revenue) { 156 | return andx() 157 | .add(lt('AnnualRevenue', revenue)) 158 | .add(eq('BillingState', 'Shanghai')); 159 | } 160 | } 161 | ``` 162 | 163 | ### 2.3 Query Chaining 164 | 165 | Parent and child relationships can be assembled using query chaining. Multiple levels of parent and child chaining are supported, except for queries with a group by clause. 166 | 167 | ```java 168 | public with sharing class AccountQuery extends Query { 169 | public List listAccount() { 170 | Query parentQuery = Query.of('Account') 171 | .selectBy('Name', format(convertCurrency('AnnualRevenue'))); 172 | Query childQuery = Query.of('Contact').selectBy('Name', 'Email'); 173 | 174 | return (List) Query.of('Account') 175 | .selectBy('Name', toLabel('Industry')) 176 | .selectParent('Parent', parentQuery) // Parent Chaining 177 | .selectChild('Contacts', childQuery) // Child Chaining 178 | .run(); 179 | } 180 | } 181 | ``` 182 | 183 | Equivalent to the following SOQL: 184 | 185 | ```sql 186 | SELECT Name, toLabel(Industry), 187 | Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)) -- Parent Chaining 188 | (SELECT Name, Email FROM Contacts) -- Child Chaining 189 | FROM Account 190 | ``` 191 | 192 | Without query chaining, the following code achieves the same result: 193 | 194 | ```java 195 | public with sharing class AccountQuery extends Query { 196 | public List listAccount() { 197 | return (List) Query.of('Account') 198 | .selectBy('Name', toLabel('Industry'), 199 | 'Parent.Name', format(convertCurrency('Parent.AnnualRevenue')), 200 | '(SELECT Name, Email FROM Contacts)') 201 | .run(); 202 | } 203 | } 204 | ``` 205 | 206 | ### 2.4 Query Template 207 | 208 | When you want to run the same `Query` with different binding variables, use the following pattern. **Note**: Query templates should be built with `var(binding variable name)`. 209 | 210 | ```java 211 | public with sharing class AccountQuery extends Query { 212 | public static Query accQuery { 213 | get { 214 | if (accQuery == null) { 215 | accQuery = Query.of('Account') 216 | .selectBy('Name', toLabel('Industry')) 217 | .selectChild('Contacts', Query.of('Contact') 218 | .selectBy('Name', 'Email') 219 | .whereBy(likex('Email', var('emailSuffix'))) // var 1 220 | ) 221 | .whereBy(andx() 222 | .add(gt('AnnualRevenue', var('revenue'))) // var 2 223 | .add(eq('BillingState', var('state'))) // var 3 224 | ); 225 | } 226 | return accQuery; 227 | } 228 | set; 229 | } 230 | 231 | public List listAccount(String state, Decimal revenue) { 232 | return (List) accQuery.run(new Map { 233 | 'revenue' => revenue, 234 | 'state' => state, 235 | 'emailSuffix' => '%gmail.com' 236 | }); 237 | } 238 | } 239 | ``` 240 | 241 | Equivalent to the following SOQL: 242 | 243 | ```sql 244 | SELECT Name, toLabel(Industry) 245 | (SELECT Name, Email FROM Contacts WHERE Email LIKE :emailSuffix) 246 | FROM Account 247 | WHERE (AnnualRevenue > :revenue AND BillingState = :state) 248 | ``` 249 | 250 | ### 2.5 Query Execution 251 | 252 | Execute with the default `AccessLevel.SYSTEM_MODE`: 253 | 254 | | | API | API with Binding Variables | Return Types | 255 | | ----- | -------------- | -------------------------- | ---------------------------------------------------------- | 256 | | **1** | `run()` | `run(bindingVars)` | `List` | 257 | | **2** | `getLocator()` | `getLocator(bindingVars)` | `Database.QueryLocator` | 258 | | **3** | `getCount()` | `getCount(bindingVars)` | `Integer`, must be used together with `selectBy(count())`. | 259 | 260 | Execute with any `AccessLevel`, such as `AccessLevel.USER_MODE`: 261 | | | API | API with Access Level | Return Types | 262 | | ----- | ------------------------- | -------------------------------------- | ------------------------------------------------------- | 263 | | **1** | `run(AccessLevel)` | `run(bindingVars, AccessLevel)` | `List` | 264 | | **2** | `getLocator(AccessLevel)` | `getLocator(bindingVars, AccessLevel)` | `Database.QueryLocator` | 265 | | **3** | `getCount(AccessLevel)` | `getCount(bindingVars, AccessLevel)` | `Integer`, must be used together with `selectBy(count())`. | 266 | 267 | ## 3. Keywords 268 | 269 | ### 3.1 From Statement 270 | 271 | All queries are created with a simple call to `Query.of(String objectName)`. If no other fields are selected, a default `Id` field is used. 272 | 273 | ```java 274 | Query accountQuery = Query.of('Account'); 275 | ``` 276 | 277 | Equivalent to the following SOQL: 278 | 279 | ```sql 280 | SELECT Id FROM Account 281 | ``` 282 | 283 | ### 3.2 Select Statement 284 | 285 | | | API | Description | 286 | | ----- | ------------------------------------------------------- | -------------------------------------------------------- | 287 | | **1** | `selectBy(Object ... )` | Select up to 10 field names or functions. | 288 | | **2** | `selectBy(List)` | Select a `List` of any field names or functions. | 289 | | **3** | `selectParent(String relationshipName, Query subQuery)` | Parent chaining. | 290 | | **4** | `selectChild(String relationshipName, Query subQuery)` | Child chaining. | 291 | 292 | ```java 293 | Query accountQuery = Query.of('Account') 294 | .selectBy('Name', toLabel('Industry')) 295 | .selectBy(new List { 'Owner.Name', FORMAT('CreatedDate') }) 296 | .selectParent('Parent', Query.of('Account') 297 | .selectBy('Name', format(convertCurrency('AnnualRevenue')))) 298 | .selectChild('Contacts', Query.of('Contact').selectBy('Name', 'Email')); 299 | ``` 300 | 301 | Equivalent to the following SOQL: 302 | 303 | ```sql 304 | SELECT Name, toLabel(Industry), 305 | Owner.Name, FORMAT(CreatedDate) 306 | Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)) 307 | (SELECT Name, Email FROM Contacts) 308 | FROM Account 309 | ``` 310 | 311 | ### 3.3 Where Statement 312 | 313 | #### Set Root Filter 314 | 315 | The `whereBy(Filter filter)` API accepts either a comparison expression or a logical statement. 316 | 317 | ```java 318 | Query accountQuery = Query.of('Account') 319 | .selectBy('Name') 320 | .whereBy(gt('AnnualRevenue', 2000)); // #1. comparison filter 321 | 322 | Query accountQuery = Query.of('Account') 323 | .selectBy('Name') 324 | .whereBy(andx() // #2. logical filter 325 | .add(gt('AnnualRevenue', 2000)) 326 | .add(lt('AnnualRevenue', 6000)) 327 | ); 328 | ``` 329 | 330 | #### Get Root Filter 331 | 332 | Use `whereBy()` to access the root filter, so branch filters can be appended later. 333 | 334 | ```java 335 | // TYPE #1: a default AND logical filter will be applied 336 | Query accountQuery = Query.of('Account').selectBy('Name') 337 | .whereBy(gt('AnnualRevenue', 2000)); 338 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 339 | 340 | // TYPE #2: an existing logical filter will be reused 341 | Query accountQuery = Query.of('Account').selectBy('Name') 342 | .whereBy(andx().add(gt('AnnualRevenue', 2000))); 343 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 344 | 345 | // TYPE #3: a default AND logical filter will be applied 346 | Query accountQuery = Query.of('Account').selectBy('Name'); 347 | accountQuery.whereBy().add(gt('AnnualRevenue', 2000)); 348 | accountQuery.whereBy().add(lt('AnnualRevenue', 6000)); 349 | ``` 350 | 351 | All equivalent to the following SOQL: 352 | 353 | ```sql 354 | SELECT Name FROM Account Where AnnualRevenue > 2000 AND AnnualRevenue < 6000 355 | ``` 356 | 357 | ### 3.4 Order By Statement 358 | 359 | | | API | Description | 360 | | ----- | ----------------------- | ---------------------------------- | 361 | | **1** | `orderBy(Object...)` | Order by up to 10 fields. | 362 | | **2** | `orderBy(List)` | Order by `List` of fields. | 363 | 364 | Parameters can be either string representations or functions. 365 | 366 | ```java 367 | Query accountQuery = Query.of('Account') 368 | .selectBy('Name', toLabel('Industry')) 369 | .orderBy( 370 | 'BillingCountry DESC NULLS LAST', 371 | distance('ShippingAddress', Location.newInstance(37.775000, -122.41800), 'km') 372 | ) 373 | .orderBy(new List{ 'Owner.Profile.Name' }); 374 | ``` 375 | 376 | Parameters can also be created by `orderField()`. Equivalent to the above SOQL: 377 | 378 | ```java 379 | Query accountQuery = Query.of('Account') 380 | .selectBy('Name', toLabel('Industry')) 381 | .orderBy( 382 | orderField('BillingCountry').descending().nullsLast(), 383 | orderField(distance('ShippingAddress', Location.newInstance(37.775000, -122.41800), 'km')) 384 | ) 385 | .orderBy(new List{ orderField('Owner.Profile.Name') }); 386 | ``` 387 | 388 | Equivalent to the following SOQL: 389 | 390 | ```sql 391 | SELECT Name, toLabel(Industry) 392 | FROM Account 393 | ORDER BY BillingCountry DESC NULLS LAST, 394 | DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), 'km'), 395 | Owner.Profile.Name 396 | ``` 397 | 398 | ### 3.5 Group By Statement 399 | 400 | | | API | Description | 401 | | ----- | ----------------------- | --------------------------------- | 402 | | **1** | `groupBy(String ...)` | Group by up to 10 field names. | 403 | | **2** | `groupBy(List)` | Group by a `List` of field names. | 404 | 405 | ```java 406 | Query accountQuery = Query.of('Account') 407 | .selectBy(avg('AnnualRevenue')) 408 | .selectBy(sum('AnnualRevenue', 'RevenueSUM')) // optional alias 409 | .groupBy('BillingCountry', calendarYear('CreatedDate')) 410 | .groupBy(new List{ calendarMonth('CreatedDate') }); 411 | ``` 412 | 413 | Equivalent to the following SOQL: 414 | 415 | ```sql 416 | SELECT AVG(AnnualRevenue), SUM(AnnualRevenue) RevenueSUM 417 | FROM Account 418 | GROUP BY BillingCountry, CALENDAR_YEAR(CreatedDate), CALENDAR_MONTH(CreatedDate) 419 | ``` 420 | 421 | #### Having Clause 422 | 423 | Aggregate results can be filtered and ordered with `havingBy()` and `orderBy()`. The `havingBy(Filter filter)` method is used in the same way as `whereBy()`. 424 | 425 | ```java 426 | Query accountQuery = Query.of('Account') 427 | .selectBy(avg('AnnualRevenue'), sum('AnnualRevenue')) 428 | .groupBy('BillingCountry', 'BillingState') 429 | .rollup() 430 | .havingBy(gt(sum('AnnualRevenue'), 2000)) 431 | .orderBy(avg('AnnualRevenue'), sum('AnnualRevenue')); 432 | ``` 433 | 434 | Equivalent to the following SOQL: 435 | 436 | ```sql 437 | SELECT AVG(AnnualRevenue), SUM(AnnualRevenue) 438 | FROM Account 439 | GROUP BY ROLLUP(BillingCountry, BillingState) 440 | HAVING SUM(AnnualRevenue) > 2000 441 | ORDER BY AVG(AnnualRevenue), SUM(AnnualRevenue) 442 | ``` 443 | 444 | #### Rollup Summary 445 | 446 | Optional `rollup()` or `cube()` methods can be called on the query to generate subtotals or grand totals. 447 | 448 | ```java 449 | Query accountQuery = Query.of('Account') 450 | .selectBy(AVG('AnnualRevenue'), SUM('AnnualRevenue')) 451 | .groupBy('BillingCountry', 'BillingState') 452 | .rollup(); 453 | ``` 454 | 455 | ### 3.6 Other Keywords 456 | 457 | | API | Generated Format | 458 | | ------------------- | ----------------- | 459 | | `limitx(Integer n)` | `LIMIT n` | 460 | | `offset(Integer n)` | `OFFSET n` | 461 | | `forView()` | `FOR VIEW` | 462 | | `forReference()` | `FOR REFERENCE` | 463 | | `forUpdate()` | `FOR UPDATE` | 464 | | `updateTracking()` | `UPDATE TRACKING` | 465 | | `updateViewstat()` | `UPDATE VIEWSTAT` | 466 | 467 | ## 4. Filters 468 | 469 | ### 4.1 Comparison Filter 470 | 471 | | SOQL Operators | Apex Query Operators | Generated Format | 472 | | -------------- | -------------------------------------- | ----------------------------------------- | 473 | | **=** | `eq(param, value)` | `param = value` | 474 | | **!=** | `ne(param, value)` | `param != value` | 475 | | **\<** | `lt(param, value)` | `param < value` | 476 | | **\<=** | `lte(param, value)` | `param <= value` | 477 | | **\>** | `gt(param, value)` | `param > value` | 478 | | **\>=** | `gte(param, value)` | `param >= value` | 479 | | **BETWEEN** | `between(param, minValue, maxValue)` | `param >= minValue AND param <= maxValue` | 480 | | **LIKE** | `likex(param, String value)` | `param LIKE value` | 481 | | **NOT LIKE** | `nlike(param, String value)` | `(NOT param LIKE value)` | 482 | | **IN** | `inx(param, List values)` | `param IN :values` | 483 | | **NOT IN** | `nin(param, List values)` | `param NOT IN :values` | 484 | | **INCLUDES** | `includes(param, List values)` | `param INCLUDES (:value1, :value2)` | 485 | | **EXCLUDES** | `excludes(param, List values)` | `param EXCLUDES (:value1, :value2)` | 486 | 487 | As a rule of thumb, the first parameter can be: 488 | 489 | 1. Field names such as `AnnualRevenue`, `'Owner.Profile.Name'`. 490 | 2. Functions such as: 491 | - `toLabel()` function 492 | - date function `calendarMonth('CreatedDate')` 493 | - distance function `distance('ShippingAddress', Location.newInstance(37.775001, -122.41801), 'km')` 494 | - aggregate function `sum('AnnualRevenue')` in a having statement 495 | 496 | #### Compare with sObject List 497 | 498 | The `inx()` and `nin()` operators can also be used to compare an Id field against a `List`. 499 | 500 | ```java 501 | List accounts = ... ; // some accounts queried elsewhere 502 | List contacts = List Query.of('Contact') 503 | .selectBy('Name', toLabel('Account.Industry')) 504 | .whereBy(inx('AccountId', accounts)) 505 | .run(); 506 | ``` 507 | 508 | Equivalent to the following SOQL: 509 | 510 | ```sql 511 | SELECT Name, toLabel(Account.Industry) 512 | FROM Contact 513 | WHERE AccountId IN :accounts 514 | ``` 515 | 516 | ### 4.2 Logical Filter 517 | 518 | | AND | Generated Format | 519 | | ---------------------------------------------------- | --------------------------- | 520 | | `andx().add(Filter filter1).add(Filter filter2) ...` | `(filter1 AND filter2 ...)` | 521 | | `andx().addAll(List filters)` | `(filter1 AND filter2 ...)` | 522 | | **OR** | | 523 | | `orx().add(Filter filter1).add(Filter filter2) ...` | `(filter1 OR filter2 ...)` | 524 | | `orx().addAll(List filters)` | `(filter1 OR filter2 ...)` | 525 | | **NOT** | | 526 | | `notx(Filter filter)` | `NOT(filter)` | 527 | 528 | The following examples show various ways to compose a filter: 529 | 530 | ```java 531 | Query.Filter revenueGreaterThan = gt('AnnualRevenue', 1000); 532 | 533 | Query.LogicalFilter shanghaiRevenueLessThan = andx().addAll(new List { 534 | lt('AnnualRevenue', 1000), 535 | eq('BillingState', 'Shanghai') 536 | }); 537 | 538 | Query.LogicalFilter orFilter = orx() 539 | .add(andx() 540 | .add(revenueGreaterThan) 541 | .add(eq('BillingState', 'Beijing')) 542 | ) 543 | .add(shanghaiRevenueLessThan); 544 | ``` 545 | 546 | Equivalent to the following SOQL: 547 | 548 | ```sql 549 | (AnnualRevenue > 1000 AND BillingState = 'Beijing') 550 | OR (AnnualRevenue < 1000 AND BillingState = 'Shanghai') 551 | ``` 552 | 553 | ## 5. Functions 554 | 555 | ### 5.1 Aggregate Functions 556 | 557 | | Static Methods | Generated Format | 558 | | ----------------------------- | ----------------------------- | 559 | | `count(field)` | `COUNT(field)` | 560 | | `count(field, alias)` | `COUNT(field) alias` | 561 | | `countDistinct(field)` | `COUNT_DISTINCT(field)` | 562 | | `countDistinct(field, alias)` | `COUNT_DISTINCT(field) alias` | 563 | | `grouping(field)` | `GROUPING(field)` | 564 | | `grouping(field, alias)` | `GROUPING(field) alias` | 565 | | `sum(field)` | `SUM(field)` | 566 | | `sum(field, alias)` | `SUM(field) alias` | 567 | | `avg(field)` | `AVG(field)` | 568 | | `avg(field, alias)` | `AVG(field) alias` | 569 | | `max(field)` | `MAX(field)` | 570 | | `max(field, alias)` | `MAX(field) alias` | 571 | | `min(field)` | `MIN(field)` | 572 | | `min(field, alias)` | `MIN(field) alias` | 573 | 574 | ### 5.2 Date/Time Functions 575 | 576 | The following functions operate on Date, Time, and Datetime fields. 577 | 578 | ```java 579 | Query.of('Opportunity') 580 | .selectBy(calendarYear('CreatedDate'), sum('Amount')) 581 | .whereBy(gt(calendarYear('CreatedDate'), 2000)) 582 | .groupBy(calendarYear('CreatedDate')); 583 | ``` 584 | 585 | Equivalent to the following SOQL: 586 | 587 | ```sql 588 | SELECT CALENDAR_YEAR(CreatedDate), SUM(Amount) 589 | FROM Opportunity 590 | WHERE CALENDAR_YEAR(CreatedDate) > 2000 591 | GROUP BY CALENDAR_YEAR(CreatedDate) 592 | ``` 593 | 594 | | Static Methods | Description | 595 | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | 596 | | `convertTimezone(field)` | Converts datetime fields to the user’s time zone. **Note**: You can only use `convertTimezone()` inside the following date functions. | 597 | | `calendarMonth(field)` | Returns a number representing the calendar month of a date field. | 598 | | `calendarQuarter(field)` | Returns a number representing the calendar quarter of a date field. | 599 | | `calendarYear(field)` | Returns a number representing the calendar year of a date field. | 600 | | `dayInMonth(field)` | Returns a number representing the day in the month of a date field. | 601 | | `dayInWeek(field)` | Returns a number representing the day of the week for a date field. | 602 | | `dayInYear(field)` | Returns a number representing the day in the year for a date field. | 603 | | `dayOnly(field)` | Returns a date representing the day portion of a datetime field. | 604 | | `fiscalMonth(field)` | Returns a number representing the fiscal month of a date field. | 605 | | `fiscalQuarter(field)` | Returns a number representing the fiscal quarter of a date field. | 606 | | `fiscalYear(field)` | Returns a number representing the fiscal year of a date field. | 607 | | `hourInDay(field)` | Returns a number representing the hour in the day for a datetime field. | 608 | | `weekInMonth(field)` | Returns a number representing the week in the month for a date field. | 609 | | `weekInYear(field)` | Returns a number representing the week in the year for a date field. | 610 | 611 | ### 5.3 Other Functions 612 | 613 | Here is an example of how to generate a location-based comparison expression: 614 | 615 | ```java 616 | Query.Filter filter = lt(distance('ShippingAddreess', 617 | Location.newInstance(37.775000, -122.41800)), 20, 'km'); 618 | ``` 619 | 620 | | Static Methods | Generated Format | 621 | | -------------------------------------------- | -------------------------------------------------------------------------- | 622 | | `toLabel(field) ` | `toLabel(field)` | 623 | | `format(field)` | `FORMAT(field)` | 624 | | `convertCurrency(field)` | `convertCurrency(field)`. **Note**: It can also be used inside `format()`. | 625 | | `distance(field, Location geo, string unit)` | `DISTANCE(ShippingAddress, GEOLOCATION(37.775,-122.418), 'km')` | 626 | 627 | ## 6. Literals 628 | 629 | ### 6.1 Date Literals 630 | 631 | Below are all available date literals, referenced from Salesforce ([link](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm)). They can be created with the corresponding methods and passed into comparison operators. 632 | 633 | ```java 634 | Query.Filter filter = andx() 635 | .add(eq('LastModifiedDate', YESTERDAY())) 636 | .add(gt('CreatedDate', LAST_N_DAYS(5))) 637 | ); 638 | ``` 639 | 640 | > `YESTERDAY()`, `TODAY()`, `TOMORROW()`, `LAST_WEEK()`, `THIS_WEEK()`, `NEXT_WEEK()`, `LAST_MONTH()`, `THIS_MONTH()`, `NEXT_MONTH()`, `LAST_90_DAYS()`, `NEXT_90_DAYS()`, `THIS_QUARTER()`, `LAST_QUARTER()`, `NEXT_QUARTER()`, `THIS_YEAR()`, `LAST_YEAR()`, `NEXT_YEAR()`, `THIS_FISCAL_QUARTER()`, `LAST_FISCAL_QUARTER()`, `NEXT_FISCAL_QUARTER()`, `THIS_FISCAL_YEAR()`, `LAST_FISCAL_YEAR()`, `NEXT_FISCAL_YEAR()` 641 | > 642 | > `LAST_N_DAYS(Integer n)`, `NEXT_N_DAYS(Integer n)`, `N_DAYS_AGO(Integer n)`, `NEXT_N_WEEKS(Integer n)`, `LAST_N_WEEKS(Integer n)`, `N_WEEKS_AGO(Integer n)`, `NEXT_N_MONTHS(Integer n)`, `LAST_N_MONTHS(Integer n)`, `N_MONTHS_AGO(Integer n)`, `NEXT_N_QUARTERS(Integer n)`, `LAST_N_QUARTERS(Integer n)`, `N_QUARTERS_AGO(Integer n)`, `NEXT_N_YEARS(Integer n)`, `LAST_N_YEARS(Integer n)`, `N_YEARS_AGO(Integer n)`, `NEXT_N_FISCAL_QUARTERS(Integer n)`, `N_FISCAL_QUARTERS_AGO(Integer n)`, `NEXT_N_FISCAL_YEARS(Integer n)`, `LAST_N_FISCAL_YEARS(Integer n)`, `N_FISCAL_YEARS_AGO(Integer n)` 643 | 644 | ### 6.2 Currency Literals 645 | 646 | You can find the list of supported currency ISO codes in the Salesforce documentation ([see here](https://help.salesforce.com/s/articleView?language=en_US&id=sf.admin_supported_currencies.htm)). 647 | 648 | ```java 649 | Query.Filter filter = orx() 650 | .add(eq('AnnualRevenual', CURRENCY('USD', 2000))) 651 | .add(eq('AnnualRevenual', CURRENCY('CNY', 2000))) 652 | .add(eq('AnnualRevenual', CURRENCY('TRY', 2000))) 653 | ); 654 | ``` 655 | 656 | ## 7. **License** 657 | 658 | Apache 2.0 659 | -------------------------------------------------------------------------------- /apex-query/main/default/classes/Query.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Jeff Jin 3 | * https://github.com/apexfarm/ApexQuery 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | public virtual class Query implements Querable { 19 | public interface Querable extends Groupable { 20 | Integer getCount(); 21 | Integer getCount(AccessLevel accessLevel); 22 | Integer getCount(Map bindingVars); 23 | Integer getCount(Map bindingVars, AccessLevel accessLevel); 24 | List run(); 25 | List run(AccessLevel accessLevel); 26 | List run(Map bindingVars); 27 | List run(Map bindingVars, AccessLevel accessLevel); 28 | Database.QueryLocator getLocator(); 29 | Database.QueryLocator getLocator(AccessLevel accessLevel); 30 | Database.QueryLocator getLocator(Map bindingVars); 31 | Database.QueryLocator getLocator(Map bindingVars, AccessLevel accessLevel); 32 | 33 | Query whereBy(Filter whereClause); 34 | Query havingBy(Filter havingClause); 35 | LogicalFilter whereBy(); 36 | LogicalFilter havingBy(); 37 | 38 | Query offset(Integer n); 39 | Query limitx(Integer n); 40 | 41 | Query forView(); 42 | Query forReference(); 43 | Query updateTracking(); 44 | Query updateViewstat(); 45 | Query forUpdate(); 46 | } 47 | 48 | public interface Filter { 49 | void buildSOQL_Filter(List fragments, TmpVars tmpVars); 50 | } 51 | 52 | public interface LogicalFilter extends Filter { 53 | LogicalFilter add(Filter filter); 54 | LogicalFilter addAll(List filters); 55 | Boolean isEmpty(); 56 | } 57 | 58 | public virtual class Literal { 59 | public String literal { get; set; } 60 | 61 | private Literal(String literal) { 62 | this.literal = literal; 63 | } 64 | } 65 | 66 | private class GroupBy { 67 | private List fieldOrFuncs = new List(); 68 | private Boolean rollup { get; set; } 69 | private Boolean cube { get; set; } 70 | 71 | private Boolean isEmpty() { 72 | return this.fieldOrFuncs.isEmpty(); 73 | } 74 | } 75 | 76 | private class TmpVars { 77 | Map bindingVars = new Map(); 78 | Integer tmpVarCount = 0; 79 | 80 | private String put(Object var) { 81 | tmpVarCount++; 82 | String tmpVar = 'tmpVar' + tmpVarCount; 83 | this.bindingVars.put(tmpVar, var); 84 | return ':' + tmpVar; 85 | } 86 | } 87 | 88 | private String soql { get; set; } 89 | private TmpVars tmpVars { get; set; } 90 | 91 | public String objectName { get; private set; } 92 | private List selectFields = new List(); 93 | public Map selectParent { get; private set; } 94 | public Map selectChild { get; private set; } 95 | 96 | private Filter whereClause; 97 | private Filter havingClause; 98 | private GroupBy groupByClause = new GroupBy(); 99 | private List orderClause = new List(); 100 | 101 | private Integer offset { get; set; } 102 | private Integer limitx { get; set; } 103 | 104 | private Boolean forView { get; set; } 105 | private Boolean forReference { get; set; } 106 | private Boolean updateTracking { get; set; } 107 | private Boolean updateViewstat { get; set; } 108 | private Boolean forUpdate { get; set; } 109 | 110 | protected Query() { 111 | this.selectParent = new Map(); 112 | this.selectChild = new Map(); 113 | } 114 | 115 | private Query(String objectName) { 116 | this(); 117 | this.objectName = objectName; 118 | } 119 | 120 | public static Query of(String objectName) { 121 | return new Query(objectName); 122 | } 123 | 124 | public Query whereBy(Filter whereClause) { 125 | if (!(whereClause instanceof LogicalFilter)) { 126 | whereClause = andx().add(whereClause); 127 | } 128 | this.whereClause = whereClause; 129 | return this; 130 | } 131 | 132 | public Query havingBy(Filter havingClause) { 133 | if (!(havingClause instanceof LogicalFilter)) { 134 | havingClause = andx().add(havingClause); 135 | } 136 | this.havingClause = havingClause; 137 | return this; 138 | } 139 | 140 | public LogicalFilter whereBy() { 141 | if (this.whereClause == null) { 142 | this.whereClause = andx(); 143 | } 144 | return (LogicalFilter) this.whereClause; 145 | } 146 | 147 | public LogicalFilter havingBy() { 148 | if (this.havingClause == null) { 149 | this.havingClause = andx(); 150 | } 151 | return (LogicalFilter) this.havingClause; 152 | } 153 | 154 | public Query offset(Integer n) { 155 | this.offset = n; 156 | return this; 157 | } 158 | 159 | public Query limitx(Integer n) { 160 | this.limitx = n; 161 | return this; 162 | } 163 | 164 | public Query forView() { 165 | this.forView = true; 166 | return this; 167 | } 168 | 169 | public Query forReference() { 170 | this.forReference = true; 171 | return this; 172 | } 173 | 174 | public Query updateTracking() { 175 | this.updateTracking = true; 176 | return this; 177 | } 178 | 179 | public Query updateViewstat() { 180 | this.updateViewstat = true; 181 | return this; 182 | } 183 | 184 | public Query forUpdate() { 185 | this.forUpdate = true; 186 | return this; 187 | } 188 | 189 | public Integer getCount() { 190 | return getCount(AccessLevel.SYSTEM_MODE); 191 | } 192 | 193 | public Integer getCount(AccessLevel accessLevel) { 194 | return this.getCount(null, accessLevel); 195 | } 196 | 197 | public Integer getCount(Map bindingVars) { 198 | return this.getCount(bindingVars, AccessLevel.SYSTEM_MODE); 199 | } 200 | 201 | public Integer getCount(Map bindingVars, AccessLevel accessLevel) { 202 | buildSOQL(); 203 | if (bindingVars == null) { 204 | bindingVars = new Map(); 205 | } 206 | bindingVars.putAll(this.tmpVars.bindingVars); 207 | 208 | if (Test.isRunningTest()) { 209 | return 0; 210 | } 211 | return Database.countQueryWithBinds(this.soql, bindingVars, accessLevel); 212 | } 213 | 214 | public List run() { 215 | return this.run(AccessLevel.SYSTEM_MODE); 216 | } 217 | 218 | public List run(AccessLevel accessLevel) { 219 | return this.run(null, accessLevel); 220 | } 221 | 222 | public List run(Map bindingVars) { 223 | return this.run(bindingVars, AccessLevel.SYSTEM_MODE); 224 | } 225 | 226 | public List run(Map bindingVars, AccessLevel accessLevel) { 227 | buildSOQL(); 228 | if (bindingVars == null) { 229 | bindingVars = new Map(); 230 | } 231 | bindingVars.putAll(this.tmpVars.bindingVars); 232 | 233 | if (Test.isRunningTest()) { 234 | return new List(); 235 | } 236 | return Database.queryWithBinds(this.soql, bindingVars, accessLevel); 237 | } 238 | 239 | public Database.QueryLocator getLocator() { 240 | return this.getLocator(AccessLevel.SYSTEM_MODE); 241 | } 242 | 243 | public Database.QueryLocator getLocator(AccessLevel accessLevel) { 244 | return this.getLocator(null, accessLevel); 245 | } 246 | 247 | public Database.QueryLocator getLocator(Map bindingVars) { 248 | return this.getLocator(bindingVars, AccessLevel.SYSTEM_MODE); 249 | } 250 | 251 | public Database.QueryLocator getLocator(Map bindingVars, AccessLevel accessLevel) { 252 | buildSOQL(); 253 | if (bindingVars == null) { 254 | bindingVars = new Map(); 255 | } 256 | bindingVars.putAll(this.tmpVars.bindingVars); 257 | 258 | if (Test.isRunningTest()) { 259 | return null; 260 | } 261 | return Database.getQueryLocatorWithBinds(this.soql, tmpVars.bindingVars, accessLevel); 262 | } 263 | 264 | // ==================== 265 | // #region SOQL Builder 266 | 267 | public String buildSOQL() { 268 | if (this.soql == null) { 269 | this.tmpVars = new TmpVars(); 270 | List fragments = new List(); 271 | this.buildSOQL(fragments, this.tmpVars, this.objectName, 0); 272 | this.soql = String.join(fragments, ''); 273 | } 274 | return this.soql; 275 | } 276 | 277 | private void buildSOQL(List fragments, TmpVars tmpVars, String fromName, Integer level) { 278 | fragments.add('SELECT '); 279 | this.buildSOQL_SelectFields(fragments, null); 280 | 281 | if (!this.selectParent.isEmpty()) { 282 | for (String relationshipName : this.selectParent.keySet()) { 283 | Query parentQuery = this.selectParent.get(relationshipName); 284 | parentQuery.buildSOQL_SelectParent(fragments, relationshipName, level); 285 | } 286 | } 287 | 288 | if (level < 6 && !this.selectChild.isEmpty()) { 289 | for (String relationshipName : this.selectChild.keySet()) { 290 | Query childQuery = this.selectChild.get(relationshipName); 291 | fragments.add(', '); 292 | fragments.add('('); 293 | childQuery.buildSOQL(fragments, tmpVars, relationshipName, level + 1); 294 | fragments.add(')'); 295 | } 296 | } 297 | 298 | fragments.add(' FROM '); 299 | fragments.add(fromName); 300 | 301 | if ( 302 | this.whereClause != null && 303 | (!(this.whereClause instanceof LogicalFilter) || !((LogicalFilter) this.whereClause).isEmpty()) 304 | ) { 305 | fragments.add(' WHERE '); 306 | this.whereClause.buildSOQL_Filter(fragments, tmpVars); 307 | } 308 | 309 | if (!this.groupByClause.isEmpty()) { 310 | fragments.add(' GROUP BY '); 311 | this.buildSOQL_GroupBy(fragments); 312 | } 313 | 314 | if ( 315 | this.havingClause != null && 316 | (!(this.havingClause instanceof LogicalFilter) || !((LogicalFilter) this.havingClause).isEmpty()) 317 | ) { 318 | fragments.add(' HAVING '); 319 | this.havingClause.buildSOQL_Filter(fragments, tmpVars); 320 | } 321 | 322 | if (!this.orderClause.isEmpty()) { 323 | fragments.add(' ORDER BY '); 324 | Boolean isNotFirst = false; 325 | for (Object field : this.orderClause) { 326 | if (isNotFirst) { 327 | fragments.add(', '); 328 | } else { 329 | isNotFirst = true; 330 | } 331 | 332 | if (field instanceof OrderField) { 333 | OrderField fieldObj = (OrderField) field; 334 | fragments.add(fieldObj.fieldOrFunc); 335 | if (fieldObj.ascending == false) { 336 | fragments.add(' DESC'); 337 | } 338 | if (fieldObj.nullsFist == false) { 339 | fragments.add(' NULLS LAST'); 340 | } 341 | } else { 342 | fragments.add(String.valueOf(field)); 343 | } 344 | } 345 | } 346 | 347 | if (this.limitx != null) { 348 | fragments.add(' LIMIT '); 349 | fragments.add(String.valueOf(this.limitx)); 350 | } 351 | 352 | if (this.offset != null) { 353 | fragments.add(' OFFSET '); 354 | fragments.add(String.valueOf(this.offset)); 355 | } 356 | 357 | // * SOQL can only has one of the following 358 | if (this.forView == true) { 359 | fragments.add(' FOR VIEW'); 360 | } else if (this.forReference == true) { 361 | fragments.add(' FOR REFERENCE'); 362 | } else if (this.updateTracking == true) { 363 | fragments.add(' UPDATE TRACKING'); 364 | } else if (this.updateViewstat == true) { 365 | fragments.add(' UPDATE VIEWSTAT'); 366 | } else if (this.forUpdate == true) { 367 | fragments.add(' FOR UPDATE'); 368 | } 369 | } 370 | 371 | private void buildSOQL_GroupBy(List fragments) { 372 | Boolean isNotFirst = false; 373 | for (String field : this.groupByClause.fieldOrFuncs) { 374 | if (isNotFirst) { 375 | fragments.add(', '); 376 | } else { 377 | if (this.groupByClause.rollup == true) { 378 | fragments.add('ROLLUP('); 379 | } else if (this.groupByClause.cube == true) { 380 | fragments.add('CUBE('); 381 | } 382 | isNotFirst = true; 383 | } 384 | fragments.add(field); 385 | } 386 | if (this.groupByClause.rollup == true || this.groupByClause.cube == true) { 387 | fragments.add(')'); 388 | } 389 | } 390 | 391 | private void buildSOQL_SelectFields(List fragments, String parentRelationshipName) { 392 | if (this.selectFields.isEmpty()) { 393 | if (parentRelationshipName != null) { 394 | fragments.add(', '); 395 | fragments.add(parentRelationshipName); 396 | fragments.add('.'); 397 | } 398 | fragments.add('Id'); 399 | } else { 400 | Boolean isNotFirst = parentRelationshipName != null; 401 | for (Object field : this.selectFields) { 402 | if (isNotFirst) { 403 | fragments.add(', '); 404 | } else { 405 | isNotFirst = true; 406 | } 407 | if (field instanceof FunctionImpl) { 408 | FunctionImpl function = (FunctionImpl) field; 409 | function.buildSOQL_Function(fragments, parentRelationshipName); 410 | } else { 411 | if (parentRelationshipName != null) { 412 | fragments.add(parentRelationshipName); 413 | fragments.add('.'); 414 | } 415 | fragments.add((String) field); 416 | } 417 | } 418 | } 419 | } 420 | 421 | private void buildSOQL_SelectParent(List fragments, String parentRelationshipName, Integer level) { 422 | this.buildSOQL_SelectFields(fragments, parentRelationshipName); 423 | 424 | if (level < 6 && !this.selectParent.isEmpty()) { 425 | for (String relationshipName : this.selectParent.keySet()) { 426 | Query parentQuery = this.selectParent.get(relationshipName); 427 | parentQuery.buildSOQL_SelectParent( 428 | fragments, 429 | parentRelationshipName + '.' + relationshipName, 430 | level + 1 431 | ); 432 | } 433 | } 434 | } 435 | 436 | // #endregion 437 | // ==================== 438 | 439 | // ===================== 440 | // #region Logic Filter 441 | private virtual class LogicalFilterImpl implements LogicalFilter { 442 | private List filters { get; set; } 443 | private String operator { get; set; } 444 | 445 | private LogicalFilterImpl(String operator, List filters) { 446 | this.operator = operator; 447 | this.filters = filters; 448 | } 449 | 450 | public LogicalFilter add(Filter filter) { 451 | this.filters.add(filter); 452 | return this; 453 | } 454 | 455 | public LogicalFilter addAll(List filters) { 456 | this.filters.addAll(filters); 457 | return this; 458 | } 459 | 460 | public Boolean isEmpty() { 461 | return this.filters.isEmpty(); 462 | } 463 | 464 | public virtual void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 465 | if (this.filters.isEmpty()) { 466 | return; 467 | } 468 | 469 | fragments.add('('); 470 | Boolean isNotFirst = false; 471 | for (Filter filter : this.filters) { 472 | if (isNotFirst) { 473 | fragments.add(this.operator); 474 | } else { 475 | isNotFirst = true; 476 | } 477 | filter.buildSOQL_Filter(fragments, tmpVars); 478 | } 479 | fragments.add(')'); 480 | } 481 | } 482 | 483 | private class NotLogicalFilter extends LogicalFilterImpl { 484 | private NotLogicalFilter(Filter filter) { 485 | super('NOT', new List{ filter }); 486 | } 487 | 488 | public override void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 489 | Filter filter = this.filters[0]; 490 | fragments.add(this.operator); 491 | if (filter instanceof LogicalFilter) { 492 | filter.buildSOQL_Filter(fragments, tmpVars); 493 | } else { 494 | fragments.add('('); 495 | filter.buildSOQL_Filter(fragments, tmpVars); 496 | fragments.add(')'); 497 | } 498 | } 499 | } 500 | 501 | public static Filter notx(Filter filter) { 502 | return new NotLogicalFilter(filter); 503 | } 504 | 505 | public static LogicalFilter orx() { 506 | return new LogicalFilterImpl(' OR ', new List()); 507 | } 508 | 509 | public static LogicalFilter andx() { 510 | return new LogicalFilterImpl(' AND ', new List()); 511 | } 512 | 513 | // #endregion 514 | // ===================== 515 | 516 | // ========================= 517 | // #region Comparison Filter 518 | 519 | private virtual class ComparisonFilter implements Filter { 520 | private String fieldOrFunc { get; set; } 521 | private String operator { get; set; } 522 | private Object value { get; set; } 523 | 524 | private ComparisonFilter(String fieldOrFunc, String operator, Object value) { 525 | this.fieldOrFunc = fieldOrFunc; 526 | this.operator = operator; 527 | this.value = value; 528 | } 529 | 530 | public virtual void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 531 | fragments.add(this.fieldOrFunc); 532 | fragments.add(this.operator); 533 | if (value == null) { 534 | fragments.add('NULL'); 535 | } else if (value instanceof Literal) { 536 | fragments.add(((Literal) this.value).literal); 537 | } else { 538 | fragments.add(tmpVars.put(this.value)); 539 | } 540 | } 541 | } 542 | 543 | private class MultiComparisonFilter extends ComparisonFilter { 544 | private MultiComparisonFilter(String field, String operator, List values) { 545 | super(field, operator, values); 546 | } 547 | 548 | public override void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 549 | fragments.add(this.fieldOrFunc); 550 | fragments.add(this.operator); 551 | fragments.add('('); 552 | Boolean isNotFirst = false; 553 | for (string value : (List) this.value) { 554 | if (isNotFirst) { 555 | fragments.add(', '); 556 | } else { 557 | isNotFirst = true; 558 | } 559 | 560 | fragments.add(tmpVars.put(this.value)); 561 | } 562 | fragments.add(')'); 563 | } 564 | } 565 | 566 | private class NotLikeFilter extends ComparisonFilter { 567 | private NotLikeFilter(String fieldOrFunc, Object value) { 568 | super(fieldOrFunc, ' LIKE ', value); 569 | } 570 | 571 | public override void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 572 | fragments.add('(NOT '); 573 | super.buildSOQL_Filter(fragments, tmpVars); 574 | fragments.add(')'); 575 | } 576 | } 577 | 578 | private class InQueryFilter extends ComparisonFilter { 579 | private InQueryFilter(String field, String operator, Query query) { 580 | super(field, operator, query); 581 | } 582 | 583 | public override void buildSOQL_Filter(List fragments, TmpVars tmpVars) { 584 | fragments.add(String.valueOf(this.fieldOrFunc)); 585 | fragments.add(this.operator); 586 | fragments.add('('); 587 | Query subquery = ((Query) this.value); 588 | subquery.buildSOQL(fragments, tmpVars, subquery.objectName, 0); 589 | fragments.add(')'); 590 | } 591 | } 592 | 593 | public static Filter inx(String IdField, Query query) { 594 | return new InQueryFilter(IdField, ' IN ', query); 595 | } 596 | 597 | public static Filter nin(String IdField, Query query) { 598 | return new InQueryFilter(IdField, ' NOT IN ', query); 599 | } 600 | 601 | public static Filter eq(String fieldOrFunc, Object value) { 602 | return new ComparisonFilter(fieldOrFunc, ' = ', value); 603 | } 604 | 605 | public static Filter ne(String fieldOrFunc, Object value) { 606 | return new ComparisonFilter(fieldOrFunc, ' != ', value); 607 | } 608 | 609 | public static Filter gt(String fieldOrFunc, Object value) { 610 | return new ComparisonFilter(fieldOrFunc, ' > ', value); 611 | } 612 | 613 | public static Filter gte(String fieldOrFunc, Object value) { 614 | return new ComparisonFilter(fieldOrFunc, ' >= ', value); 615 | } 616 | 617 | public static Filter lt(String fieldOrFunc, Object value) { 618 | return new ComparisonFilter(fieldOrFunc, ' < ', value); 619 | } 620 | 621 | public static Filter lte(String fieldOrFunc, Object value) { 622 | return new ComparisonFilter(fieldOrFunc, ' <= ', value); 623 | } 624 | 625 | public static Filter between(String fieldOrFunc, Object minValue, Object maxValue) { 626 | return andx().add(gte(fieldOrFunc, minValue)).add(lte(fieldOrFunc, maxValue)); 627 | } 628 | 629 | public static Filter likex(String field, String value) { 630 | return new ComparisonFilter(field, ' LIKE ', value); 631 | } 632 | 633 | public static Filter nlike(String field, String value) { 634 | return new NotLikeFilter(field, value); 635 | } 636 | 637 | public static Filter inx(String fieldOrFunc, List values) { 638 | return new ComparisonFilter(fieldOrFunc, ' IN ', values); 639 | } 640 | 641 | public static Filter nin(String fieldOrFunc, List values) { 642 | return new ComparisonFilter(fieldOrFunc, ' NOT IN ', values); 643 | } 644 | 645 | public static Filter includes(String field, List values) { 646 | return new MultiComparisonFilter(field, ' INCLUDES ', values); 647 | } 648 | 649 | public static Filter excludes(String field, List values) { 650 | return new MultiComparisonFilter(field, ' EXCLUDES ', values); 651 | } 652 | 653 | // #endregion 654 | // ========================= 655 | 656 | // ============================= 657 | // #region Comparison Filter Ext 658 | 659 | // Special Cases for ToLabelFunction 660 | public static Filter eq(ToLabelFunction func, String value) { 661 | String funcStr = func.buildSOQL_Function(); 662 | return new ComparisonFilter(funcStr, ' = ', value); 663 | } 664 | 665 | public static Filter ne(ToLabelFunction func, String value) { 666 | String funcStr = func.buildSOQL_Function(); 667 | return new ComparisonFilter(funcStr, ' != ', value); 668 | } 669 | 670 | public static Filter gt(ToLabelFunction func, String value) { 671 | String funcStr = func.buildSOQL_Function(); 672 | return new ComparisonFilter(funcStr, ' > ', value); 673 | } 674 | 675 | public static Filter gte(ToLabelFunction func, String value) { 676 | String funcStr = func.buildSOQL_Function(); 677 | return new ComparisonFilter(funcStr, ' >= ', value); 678 | } 679 | 680 | public static Filter lt(ToLabelFunction func, String value) { 681 | String funcStr = func.buildSOQL_Function(); 682 | return new ComparisonFilter(funcStr, ' < ', value); 683 | } 684 | 685 | public static Filter lte(ToLabelFunction func, String value) { 686 | String funcStr = func.buildSOQL_Function(); 687 | return new ComparisonFilter(funcStr, ' <= ', value); 688 | } 689 | 690 | public static Filter between(ToLabelFunction func, String minValue, String maxValue) { 691 | String funcStr = func.buildSOQL_Function(); 692 | return andx().add(gte(funcStr, minValue)).add(lte(funcStr, maxValue)); 693 | } 694 | 695 | public static Filter likex(ToLabelFunction func, String value) { 696 | String funcStr = func.buildSOQL_Function(); 697 | return new ComparisonFilter(funcStr, ' LIKE ', value); 698 | } 699 | 700 | public static Filter nlike(ToLabelFunction func, String value) { 701 | String funcStr = func.buildSOQL_Function(); 702 | return new NotLikeFilter(funcStr, value); 703 | } 704 | 705 | public static Filter inx(ToLabelFunction func, List values) { 706 | String funcStr = func.buildSOQL_Function(); 707 | return new ComparisonFilter(funcStr, ' IN ', values); 708 | } 709 | 710 | public static Filter nin(ToLabelFunction func, List values) { 711 | String funcStr = func.buildSOQL_Function(); 712 | return new ComparisonFilter(funcStr, ' NOT IN ', values); 713 | } 714 | 715 | public static Filter includes(ToLabelFunction func, List values) { 716 | String funcStr = func.buildSOQL_Function(); 717 | return new MultiComparisonFilter(funcStr, ' INCLUDES ', values); 718 | } 719 | 720 | public static Filter excludes(ToLabelFunction func, List values) { 721 | String funcStr = func.buildSOQL_Function(); 722 | return new MultiComparisonFilter(funcStr, ' EXCLUDES ', values); 723 | } 724 | 725 | // Special Cases for VarLiteral 726 | public static Filter likex(String field, VarLiteral value) { 727 | return new ComparisonFilter(field, ' LIKE ', value); 728 | } 729 | 730 | public static Filter nlike(String field, VarLiteral value) { 731 | return new NotLikeFilter(field, value); 732 | } 733 | 734 | public static Filter inx(String fieldOrFunc, VarLiteral values) { 735 | return new ComparisonFilter(fieldOrFunc, ' IN ', values); 736 | } 737 | 738 | public static Filter nin(String fieldOrFunc, VarLiteral values) { 739 | return new ComparisonFilter(fieldOrFunc, ' NOT IN ', values); 740 | } 741 | 742 | public static Filter likex(ToLabelFunction func, VarLiteral value) { 743 | String funcStr = func.buildSOQL_Function(); 744 | return new ComparisonFilter(funcStr, ' LIKE ', value); 745 | } 746 | 747 | public static Filter nlike(ToLabelFunction func, VarLiteral value) { 748 | String funcStr = func.buildSOQL_Function(); 749 | return new NotLikeFilter(funcStr, value); 750 | } 751 | 752 | public static Filter inx(ToLabelFunction func, VarLiteral values) { 753 | String funcStr = func.buildSOQL_Function(); 754 | return new ComparisonFilter(funcStr, ' IN ', values); 755 | } 756 | 757 | public static Filter nin(ToLabelFunction func, VarLiteral values) { 758 | String funcStr = func.buildSOQL_Function(); 759 | return new ComparisonFilter(funcStr, ' NOT IN ', values); 760 | } 761 | 762 | // #endregion 763 | // ============================= 764 | 765 | // ===================== 766 | // #region Select Clause 767 | public interface Selectable { 768 | Query selectBy(Object field); 769 | Query selectBy(Object field1, Object field2); 770 | Query selectBy(Object field1, Object field2, Object field3); 771 | Query selectBy(Object field1, Object field2, Object field3, Object field4); 772 | Query selectBy(Object field1, Object field2, Object field3, Object field4, Object field5); 773 | Query selectBy(Object field1, Object field2, Object field3, Object field4, Object field5, Object field6); 774 | Query selectBy( 775 | Object field1, 776 | Object field2, 777 | Object field3, 778 | Object field4, 779 | Object field5, 780 | Object field6, 781 | Object field7 782 | ); 783 | Query selectBy( 784 | Object field1, 785 | Object field2, 786 | Object field3, 787 | Object field4, 788 | Object field5, 789 | Object field6, 790 | Object field7, 791 | Object field8 792 | ); 793 | Query selectBy( 794 | Object field1, 795 | Object field2, 796 | Object field3, 797 | Object field4, 798 | Object field5, 799 | Object field6, 800 | Object field7, 801 | Object field8, 802 | Object field9 803 | ); 804 | Query selectBy( 805 | Object field1, 806 | Object field2, 807 | Object field3, 808 | Object field4, 809 | Object field5, 810 | Object field6, 811 | Object field7, 812 | Object field8, 813 | Object field9, 814 | Object field10 815 | ); 816 | Query selectBy(List fields); 817 | 818 | Query selectParent(String relationshipName, Query query); 819 | Query selectChild(String relationshipName, Query query); 820 | } 821 | 822 | public Query selectBy(Object field) { 823 | this.selectFields.add(field); 824 | return this; 825 | } 826 | 827 | public Query selectBy(Object field1, Object field2) { 828 | this.selectFields.add(field1); 829 | this.selectFields.add(field2); 830 | return this; 831 | } 832 | 833 | public Query selectBy(Object field1, Object field2, Object field3) { 834 | this.selectFields.add(field1); 835 | this.selectFields.add(field2); 836 | this.selectFields.add(field3); 837 | return this; 838 | } 839 | 840 | public Query selectBy(Object field1, Object field2, Object field3, Object field4) { 841 | this.selectFields.addAll(new List{ field1, field2, field3, field4 }); 842 | return this; 843 | } 844 | 845 | public Query selectBy(Object field1, Object field2, Object field3, Object field4, Object field5) { 846 | this.selectFields.addAll(new List{ field1, field2, field3, field4, field5 }); 847 | return this; 848 | } 849 | 850 | public Query selectBy(Object field1, Object field2, Object field3, Object field4, Object field5, Object field6) { 851 | this.selectFields.addAll(new List{ field1, field2, field3, field4, field5, field6 }); 852 | return this; 853 | } 854 | 855 | public Query selectBy( 856 | Object field1, 857 | Object field2, 858 | Object field3, 859 | Object field4, 860 | Object field5, 861 | Object field6, 862 | Object field7 863 | ) { 864 | this.selectFields.addAll(new List{ field1, field2, field3, field4, field5, field6, field7 }); 865 | return this; 866 | } 867 | 868 | public Query selectBy( 869 | Object field1, 870 | Object field2, 871 | Object field3, 872 | Object field4, 873 | Object field5, 874 | Object field6, 875 | Object field7, 876 | Object field8 877 | ) { 878 | this.selectFields.addAll(new List{ field1, field2, field3, field4, field5, field6, field7, field8 }); 879 | return this; 880 | } 881 | 882 | public Query selectBy( 883 | Object field1, 884 | Object field2, 885 | Object field3, 886 | Object field4, 887 | Object field5, 888 | Object field6, 889 | Object field7, 890 | Object field8, 891 | Object field9 892 | ) { 893 | this.selectFields.addAll( 894 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9 } 895 | ); 896 | return this; 897 | } 898 | 899 | public Query selectBy( 900 | Object field1, 901 | Object field2, 902 | Object field3, 903 | Object field4, 904 | Object field5, 905 | Object field6, 906 | Object field7, 907 | Object field8, 908 | Object field9, 909 | Object field10 910 | ) { 911 | this.selectFields.addAll( 912 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 } 913 | ); 914 | return this; 915 | } 916 | 917 | public Query selectBy(List fields) { 918 | this.selectFields.addAll(fields); 919 | return this; 920 | } 921 | 922 | public Query selectParent(String relationshipName, Query query) { 923 | this.selectParent.put(relationshipName.trim(), query); 924 | return this; 925 | } 926 | 927 | public Query selectChild(String relationshipName, Query query) { 928 | this.selectChild.put(relationshipName.trim(), query); 929 | return this; 930 | } 931 | // #endregion 932 | // ===================== 933 | 934 | // ======================= 935 | // #region Order By Clause 936 | 937 | public interface Orderable extends Selectable { 938 | Query orderBy(Object field); 939 | Query orderBy(Object field1, Object field2); 940 | Query orderBy(Object field1, Object field2, Object field3); 941 | Query orderBy(Object field1, Object field2, Object field3, Object field4); 942 | Query orderBy(Object field1, Object field2, Object field3, Object field4, Object field5); 943 | Query orderBy(Object field1, Object field2, Object field3, Object field4, Object field5, Object field6); 944 | Query orderBy( 945 | Object field1, 946 | Object field2, 947 | Object field3, 948 | Object field4, 949 | Object field5, 950 | Object field6, 951 | Object field7 952 | ); 953 | Query orderBy( 954 | Object field1, 955 | Object field2, 956 | Object field3, 957 | Object field4, 958 | Object field5, 959 | Object field6, 960 | Object field7, 961 | Object field8 962 | ); 963 | Query orderBy( 964 | Object field1, 965 | Object field2, 966 | Object field3, 967 | Object field4, 968 | Object field5, 969 | Object field6, 970 | Object field7, 971 | Object field8, 972 | Object field9 973 | ); 974 | Query orderBy( 975 | Object field1, 976 | Object field2, 977 | Object field3, 978 | Object field4, 979 | Object field5, 980 | Object field6, 981 | Object field7, 982 | Object field8, 983 | Object field9, 984 | Object field10 985 | ); 986 | Query orderBy(List fields); 987 | } 988 | 989 | public class OrderField { 990 | private String fieldOrFunc { get; set; } 991 | private Boolean ascending { get; set; } 992 | private Boolean nullsFist { get; set; } 993 | 994 | public OrderField(String fieldOrFunc) { 995 | this.fieldOrFunc = fieldOrFunc; 996 | this.ascending = true; 997 | this.nullsFist = true; 998 | } 999 | 1000 | public OrderField ascending() { 1001 | this.ascending = true; 1002 | return this; 1003 | } 1004 | 1005 | public OrderField descending() { 1006 | this.ascending = false; 1007 | return this; 1008 | } 1009 | 1010 | public OrderField nullsFirst() { 1011 | this.nullsFist = true; 1012 | return this; 1013 | } 1014 | 1015 | public OrderField nullsLast() { 1016 | this.nullsFist = false; 1017 | return this; 1018 | } 1019 | } 1020 | 1021 | public static OrderField orderField(String fieldOrFunc) { 1022 | return new OrderField(fieldOrFunc); 1023 | } 1024 | 1025 | public Query orderBy(Object field) { 1026 | this.orderClause.add(field); 1027 | return this; 1028 | } 1029 | 1030 | public Query orderBy(Object field1, Object field2) { 1031 | this.orderClause.add(field1); 1032 | this.orderClause.add(field2); 1033 | return this; 1034 | } 1035 | 1036 | public Query orderBy(Object field1, Object field2, Object field3) { 1037 | this.orderClause.add(field1); 1038 | this.orderClause.add(field2); 1039 | this.orderClause.add(field3); 1040 | return this; 1041 | } 1042 | 1043 | public Query orderBy(Object field1, Object field2, Object field3, Object field4) { 1044 | this.orderClause.addAll(new List{ field1, field2, field3, field4 }); 1045 | return this; 1046 | } 1047 | 1048 | public Query orderBy(Object field1, Object field2, Object field3, Object field4, Object field5) { 1049 | this.orderClause.addAll(new List{ field1, field2, field3, field4, field5 }); 1050 | return this; 1051 | } 1052 | 1053 | public Query orderBy(Object field1, Object field2, Object field3, Object field4, Object field5, Object field6) { 1054 | this.orderClause.addAll(new List{ field1, field2, field3, field4, field5, field6 }); 1055 | return this; 1056 | } 1057 | 1058 | public Query orderBy( 1059 | Object field1, 1060 | Object field2, 1061 | Object field3, 1062 | Object field4, 1063 | Object field5, 1064 | Object field6, 1065 | Object field7 1066 | ) { 1067 | this.orderClause.addAll(new List{ field1, field2, field3, field4, field5, field6, field7 }); 1068 | return this; 1069 | } 1070 | 1071 | public Query orderBy( 1072 | Object field1, 1073 | Object field2, 1074 | Object field3, 1075 | Object field4, 1076 | Object field5, 1077 | Object field6, 1078 | Object field7, 1079 | Object field8 1080 | ) { 1081 | this.orderClause.addAll(new List{ field1, field2, field3, field4, field5, field6, field7, field8 }); 1082 | return this; 1083 | } 1084 | 1085 | public Query orderBy( 1086 | Object field1, 1087 | Object field2, 1088 | Object field3, 1089 | Object field4, 1090 | Object field5, 1091 | Object field6, 1092 | Object field7, 1093 | Object field8, 1094 | Object field9 1095 | ) { 1096 | this.orderClause.addAll( 1097 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9 } 1098 | ); 1099 | return this; 1100 | } 1101 | 1102 | public Query orderBy( 1103 | Object field1, 1104 | Object field2, 1105 | Object field3, 1106 | Object field4, 1107 | Object field5, 1108 | Object field6, 1109 | Object field7, 1110 | Object field8, 1111 | Object field9, 1112 | Object field10 1113 | ) { 1114 | this.orderClause.addAll( 1115 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 } 1116 | ); 1117 | return this; 1118 | } 1119 | 1120 | public Query orderBy(List fields) { 1121 | this.orderClause.addAll(fields); 1122 | return this; 1123 | } 1124 | 1125 | // #endregion 1126 | // ======================= 1127 | 1128 | // ======================= 1129 | // #region Group By Clause 1130 | public interface Groupable extends Orderable { 1131 | Query rollup(); 1132 | Query cube(); 1133 | 1134 | Query groupBy(String field); 1135 | Query groupBy(String field1, String field2); 1136 | Query groupBy(String field1, String field2, String field3); 1137 | Query groupBy(String field1, String field2, String field3, String field4); 1138 | Query groupBy(String field1, String field2, String field3, String field4, String field5); 1139 | Query groupBy(String field1, String field2, String field3, String field4, String field5, String field6); 1140 | Query groupBy( 1141 | String field1, 1142 | String field2, 1143 | String field3, 1144 | String field4, 1145 | String field5, 1146 | String field6, 1147 | String field7 1148 | ); 1149 | Query groupBy( 1150 | String field1, 1151 | String field2, 1152 | String field3, 1153 | String field4, 1154 | String field5, 1155 | String field6, 1156 | String field7, 1157 | String field8 1158 | ); 1159 | Query groupBy( 1160 | String field1, 1161 | String field2, 1162 | String field3, 1163 | String field4, 1164 | String field5, 1165 | String field6, 1166 | String field7, 1167 | String field8, 1168 | String field9 1169 | ); 1170 | Query groupBy( 1171 | String field1, 1172 | String field2, 1173 | String field3, 1174 | String field4, 1175 | String field5, 1176 | String field6, 1177 | String field7, 1178 | String field8, 1179 | String field9, 1180 | String field10 1181 | ); 1182 | Query groupBy(List fields); 1183 | } 1184 | 1185 | public Query rollup() { 1186 | this.groupByClause.rollup = true; 1187 | return this; 1188 | } 1189 | 1190 | public Query cube() { 1191 | this.groupByClause.cube = true; 1192 | return this; 1193 | } 1194 | 1195 | public Query groupBy(String field) { 1196 | this.groupByClause.fieldOrFuncs.add(field); 1197 | return this; 1198 | } 1199 | 1200 | public Query groupBy(String field1, String field2) { 1201 | this.groupByClause.fieldOrFuncs.add(field1); 1202 | this.groupByClause.fieldOrFuncs.add(field2); 1203 | return this; 1204 | } 1205 | 1206 | public Query groupBy(String field1, String field2, String field3) { 1207 | this.groupByClause.fieldOrFuncs.add(field1); 1208 | this.groupByClause.fieldOrFuncs.add(field2); 1209 | this.groupByClause.fieldOrFuncs.add(field3); 1210 | return this; 1211 | } 1212 | 1213 | public Query groupBy(String field1, String field2, String field3, String field4) { 1214 | this.groupByClause.fieldOrFuncs.addAll(new List{ field1, field2, field3, field4 }); 1215 | return this; 1216 | } 1217 | 1218 | public Query groupBy(String field1, String field2, String field3, String field4, String field5) { 1219 | this.groupByClause.fieldOrFuncs.addAll(new List{ field1, field2, field3, field4, field5 }); 1220 | return this; 1221 | } 1222 | 1223 | public Query groupBy(String field1, String field2, String field3, String field4, String field5, String field6) { 1224 | this.groupByClause.fieldOrFuncs.addAll(new List{ field1, field2, field3, field4, field5, field6 }); 1225 | return this; 1226 | } 1227 | 1228 | public Query groupBy( 1229 | String field1, 1230 | String field2, 1231 | String field3, 1232 | String field4, 1233 | String field5, 1234 | String field6, 1235 | String field7 1236 | ) { 1237 | this.groupByClause.fieldOrFuncs.addAll( 1238 | new List{ field1, field2, field3, field4, field5, field6, field7 } 1239 | ); 1240 | return this; 1241 | } 1242 | 1243 | public Query groupBy( 1244 | String field1, 1245 | String field2, 1246 | String field3, 1247 | String field4, 1248 | String field5, 1249 | String field6, 1250 | String field7, 1251 | String field8 1252 | ) { 1253 | this.groupByClause.fieldOrFuncs.addAll( 1254 | new List{ field1, field2, field3, field4, field5, field6, field7, field8 } 1255 | ); 1256 | return this; 1257 | } 1258 | 1259 | public Query groupBy( 1260 | String field1, 1261 | String field2, 1262 | String field3, 1263 | String field4, 1264 | String field5, 1265 | String field6, 1266 | String field7, 1267 | String field8, 1268 | String field9 1269 | ) { 1270 | this.groupByClause.fieldOrFuncs.addAll( 1271 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9 } 1272 | ); 1273 | return this; 1274 | } 1275 | 1276 | public Query groupBy( 1277 | String field1, 1278 | String field2, 1279 | String field3, 1280 | String field4, 1281 | String field5, 1282 | String field6, 1283 | String field7, 1284 | String field8, 1285 | String field9, 1286 | String field10 1287 | ) { 1288 | this.groupByClause.fieldOrFuncs.addAll( 1289 | new List{ field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 } 1290 | ); 1291 | return this; 1292 | } 1293 | 1294 | public Query groupBy(List fields) { 1295 | this.groupByClause.fieldOrFuncs.addAll(fields); 1296 | return this; 1297 | } 1298 | 1299 | // #endregion 1300 | // ======================= 1301 | 1302 | // ======================= 1303 | // #region SELECT Function 1304 | public interface Function { 1305 | void buildSOQL_Function(List fragments, String parentRelationshipName); 1306 | String buildSOQL_Function(); 1307 | } 1308 | 1309 | public interface SelectFunction extends Function { 1310 | } 1311 | 1312 | public interface FormatFunction extends SelectFunction { 1313 | } 1314 | 1315 | public interface ToLabelFunction extends SelectFunction { 1316 | } 1317 | 1318 | public interface CurrencyFunction extends SelectFunction { 1319 | } 1320 | 1321 | public interface TimezoneFunction { 1322 | } 1323 | 1324 | public virtual class FunctionImpl implements Function { 1325 | private String function { get; set; } 1326 | private Object fieldOrFunc { get; set; } 1327 | private String alias { get; set; } 1328 | 1329 | private FunctionImpl(String function, Object fieldOrFunc, String alias) { 1330 | this.function = function; 1331 | this.fieldOrFunc = fieldOrFunc; 1332 | this.alias = alias; 1333 | } 1334 | 1335 | public void buildSOQL_Function(List fragments, String parentRelationshipName) { 1336 | fragments.add(this.function); 1337 | fragments.add('('); 1338 | if (this.fieldOrFunc instanceof Function) { 1339 | ((Function) this.fieldOrFunc).buildSOQL_Function(fragments, parentRelationshipName); 1340 | } else { 1341 | if (parentRelationshipName != null) { 1342 | fragments.add(parentRelationshipName); 1343 | fragments.add('.'); 1344 | } 1345 | fragments.add((String) this.fieldOrFunc); 1346 | } 1347 | fragments.add(')'); 1348 | if (this.alias != null) { 1349 | fragments.add(' AS '); 1350 | fragments.add(this.alias); 1351 | } 1352 | } 1353 | 1354 | public String buildSOQL_Function() { 1355 | return this.function + '(' + this.fieldOrFunc + ')'; 1356 | } 1357 | } 1358 | 1359 | public class FormatFunctionImpl extends FunctionImpl implements FormatFunction { 1360 | private FormatFunctionImpl(String function, Object fieldOrFunc, String alias) { 1361 | super(function, fieldOrFunc, alias); 1362 | } 1363 | } 1364 | 1365 | public class ToLabelFunctionImpl extends FunctionImpl implements ToLabelFunction { 1366 | private ToLabelFunctionImpl(String function, String fieldOrFunc, String alias) { 1367 | super(function, fieldOrFunc, alias); 1368 | } 1369 | } 1370 | 1371 | public class CurrencyFunctionImpl extends FunctionImpl implements CurrencyFunction { 1372 | private CurrencyFunctionImpl(String function, String fieldOrFunc, String alias) { 1373 | super(function, fieldOrFunc, alias); 1374 | } 1375 | } 1376 | 1377 | public static ToLabelFunction toLabel(String field) { 1378 | return new ToLabelFunctionImpl('toLabel', field, null); 1379 | } 1380 | 1381 | public static ToLabelFunction toLabel(String field, String alias) { 1382 | return new ToLabelFunctionImpl('toLabel', field, alias); 1383 | } 1384 | 1385 | public static FormatFunction format(String field) { 1386 | return new FormatFunctionImpl('FORMAT', field, null); 1387 | } 1388 | 1389 | public static FormatFunction format(String field, String alias) { 1390 | return new FormatFunctionImpl('FORMAT', field, alias); 1391 | } 1392 | 1393 | public static FormatFunction format(CurrencyFunction field) { 1394 | return new FormatFunctionImpl('FORMAT', field, null); 1395 | } 1396 | 1397 | public static FormatFunction format(CurrencyFunction field, String alias) { 1398 | return new FormatFunctionImpl('FORMAT', field, alias); 1399 | } 1400 | 1401 | public static CurrencyFunction convertCurrency(String field) { 1402 | return new CurrencyFunctionImpl('convertCurrency', field, null); 1403 | } 1404 | 1405 | public static CurrencyFunction convertCurrency(String field, String alias) { 1406 | return new CurrencyFunctionImpl('convertCurrency', field, alias); 1407 | } 1408 | 1409 | public static String distance(String field, Location geo, string unit) { 1410 | return 'DISTANCE(' + 1411 | String.valueOf(field) + 1412 | ', GEOLOCATION(' + 1413 | geo.latitude + 1414 | ', ' + 1415 | geo.longitude + 1416 | '), \'' + 1417 | unit + 1418 | '\')'; 1419 | } 1420 | // #endregion 1421 | // ======================= 1422 | 1423 | // ============================ 1424 | // #region Group By Aggregation 1425 | 1426 | public static String count() { 1427 | return 'COUNT()'; 1428 | } 1429 | 1430 | public static String count(String field) { 1431 | return 'COUNT(' + field + ')'; 1432 | } 1433 | 1434 | public static String count(String field, String alias) { 1435 | return 'COUNT(' + field + ') ' + alias; 1436 | } 1437 | 1438 | public static String countDistinct(String field) { 1439 | return 'COUNT_DISTINCT(' + field + ')'; 1440 | } 1441 | 1442 | public static String countDistinct(String field, String alias) { 1443 | return 'COUNT_DISTINCT(' + field + ') ' + alias; 1444 | } 1445 | 1446 | public static String sum(String field) { 1447 | return 'SUM(' + field + ')'; 1448 | } 1449 | 1450 | public static String sum(String field, String alias) { 1451 | return 'SUM(' + field + ') ' + alias; 1452 | } 1453 | 1454 | public static String avg(String field) { 1455 | return 'AVG(' + field + ')'; 1456 | } 1457 | 1458 | public static String avg(String field, String alias) { 1459 | return 'AVG(' + field + ') ' + alias; 1460 | } 1461 | 1462 | public static String max(String field) { 1463 | return 'MAX(' + field + ')'; 1464 | } 1465 | 1466 | public static String max(String field, String alias) { 1467 | return 'MAX(' + field + ') ' + alias; 1468 | } 1469 | 1470 | public static String min(String field) { 1471 | return 'MIN(' + field + ')'; 1472 | } 1473 | 1474 | public static String min(String field, String alias) { 1475 | return 'MIN(' + field + ') ' + alias; 1476 | } 1477 | 1478 | // #endregion 1479 | // ============================ 1480 | 1481 | // =============================== 1482 | // #region Group By Dimension 1483 | 1484 | public static String grouping(String field) { 1485 | return 'GROUPING(' + field + ')'; 1486 | } 1487 | 1488 | public static String grouping(String field, String alias) { 1489 | return 'GROUPING(' + field + ') ' + alias; 1490 | } 1491 | 1492 | public static String convertTimezone(String field) { 1493 | return 'convertTimezone(' + field + ')'; 1494 | } 1495 | 1496 | public static String calendarMonth(String field) { 1497 | return 'CALENDAR_MONTH(' + field + ')'; 1498 | } 1499 | 1500 | public static String calendarQuarter(String field) { 1501 | return 'CALENDAR_QUARTER(' + field + ')'; 1502 | } 1503 | 1504 | public static String calendarYear(String field) { 1505 | return 'CALENDAR_YEAR(' + field + ')'; 1506 | } 1507 | 1508 | public static String dayInMonth(String field) { 1509 | return 'DAY_IN_MONTH(' + field + ')'; 1510 | } 1511 | 1512 | public static String dayInWeek(String field) { 1513 | return 'DAY_IN_WEEK(' + field + ')'; 1514 | } 1515 | 1516 | public static String dayInYear(String field) { 1517 | return 'DAY_IN_YEAR(' + field + ')'; 1518 | } 1519 | 1520 | public static String dayOnly(String field) { 1521 | return 'DAY_ONLY(' + field + ')'; 1522 | } 1523 | 1524 | public static String fiscalMonth(String field) { 1525 | return 'FISCAL_MONTH(' + field + ')'; 1526 | } 1527 | 1528 | public static String fiscalQuarter(String field) { 1529 | return 'FISCAL_QUARTER(' + field + ')'; 1530 | } 1531 | 1532 | public static String fiscalYear(String field) { 1533 | return 'FISCAL_YEAR(' + field + ')'; 1534 | } 1535 | 1536 | public static String hourInDay(String field) { 1537 | return 'HOUR_IN_DAY(' + field + ')'; 1538 | } 1539 | 1540 | public static String weekInMonth(String field) { 1541 | return 'WEEK_IN_MONTH(' + field + ')'; 1542 | } 1543 | 1544 | public static String weekInYear(String field) { 1545 | return 'WEEK_IN_YEAR(' + field + ')'; 1546 | } 1547 | 1548 | // #endregion 1549 | // =============================== 1550 | 1551 | // ==================== 1552 | // #region VAR Literal 1553 | 1554 | public class VarLiteral extends Literal { 1555 | public VarLiteral(String literal) { 1556 | super(literal); 1557 | } 1558 | } 1559 | 1560 | public static VarLiteral var(String name) { 1561 | return new VarLiteral(':' + name); 1562 | } 1563 | 1564 | // #endregion 1565 | // ==================== 1566 | 1567 | // ==================== 1568 | // #region Date Literal 1569 | public static Literal YESTERDAY() { 1570 | return new Literal('YESTERDAY'); 1571 | } 1572 | 1573 | public static Literal TODAY() { 1574 | return new Literal('TODAY'); 1575 | } 1576 | 1577 | public static Literal TOMORROW() { 1578 | return new Literal('TOMORROW'); 1579 | } 1580 | 1581 | public static Literal LAST_WEEK() { 1582 | return new Literal('LAST_WEEK'); 1583 | } 1584 | 1585 | public static Literal THIS_WEEK() { 1586 | return new Literal('THIS_WEEK'); 1587 | } 1588 | 1589 | public static Literal NEXT_WEEK() { 1590 | return new Literal('NEXT_WEEK'); 1591 | } 1592 | 1593 | public static Literal LAST_MONTH() { 1594 | return new Literal('LAST_MONTH'); 1595 | } 1596 | 1597 | public static Literal THIS_MONTH() { 1598 | return new Literal('THIS_MONTH'); 1599 | } 1600 | 1601 | public static Literal NEXT_MONTH() { 1602 | return new Literal('NEXT_MONTH'); 1603 | } 1604 | 1605 | public static Literal LAST_90_DAYS() { 1606 | return new Literal('LAST_90_DAYS'); 1607 | } 1608 | 1609 | public static Literal NEXT_90_DAYS() { 1610 | return new Literal('NEXT_90_DAYS'); 1611 | } 1612 | 1613 | public static Literal THIS_QUARTER() { 1614 | return new Literal('THIS_QUARTER'); 1615 | } 1616 | 1617 | public static Literal LAST_QUARTER() { 1618 | return new Literal('LAST_QUARTER'); 1619 | } 1620 | 1621 | public static Literal NEXT_QUARTER() { 1622 | return new Literal('NEXT_QUARTER'); 1623 | } 1624 | 1625 | public static Literal THIS_YEAR() { 1626 | return new Literal('THIS_YEAR'); 1627 | } 1628 | 1629 | public static Literal LAST_YEAR() { 1630 | return new Literal('LAST_YEAR'); 1631 | } 1632 | 1633 | public static Literal NEXT_YEAR() { 1634 | return new Literal('NEXT_YEAR'); 1635 | } 1636 | 1637 | public static Literal THIS_FISCAL_QUARTER() { 1638 | return new Literal('THIS_FISCAL_QUARTER'); 1639 | } 1640 | 1641 | public static Literal LAST_FISCAL_QUARTER() { 1642 | return new Literal('LAST_FISCAL_QUARTER'); 1643 | } 1644 | 1645 | public static Literal NEXT_FISCAL_QUARTER() { 1646 | return new Literal('NEXT_FISCAL_QUARTER'); 1647 | } 1648 | 1649 | public static Literal THIS_FISCAL_YEAR() { 1650 | return new Literal('THIS_FISCAL_YEAR'); 1651 | } 1652 | 1653 | public static Literal LAST_FISCAL_YEAR() { 1654 | return new Literal('LAST_FISCAL_YEAR'); 1655 | } 1656 | 1657 | public static Literal NEXT_FISCAL_YEAR() { 1658 | return new Literal('NEXT_FISCAL_YEAR'); 1659 | } 1660 | 1661 | public static Literal LAST_N_DAYS(Integer n) { 1662 | return new Literal('LAST_N_DAYS:' + n); 1663 | } 1664 | 1665 | public static Literal NEXT_N_DAYS(Integer n) { 1666 | return new Literal('NEXT_N_DAYS:' + n); 1667 | } 1668 | 1669 | public static Literal N_DAYS_AGO(Integer n) { 1670 | return new Literal('N_DAYS_AGO:' + n); 1671 | } 1672 | 1673 | public static Literal NEXT_N_WEEKS(Integer n) { 1674 | return new Literal('NEXT_N_WEEKS:' + n); 1675 | } 1676 | 1677 | public static Literal LAST_N_WEEKS(Integer n) { 1678 | return new Literal('LAST_N_WEEKS:' + n); 1679 | } 1680 | 1681 | public static Literal N_WEEKS_AGO(Integer n) { 1682 | return new Literal('N_WEEKS_AGO:' + n); 1683 | } 1684 | 1685 | public static Literal NEXT_N_MONTHS(Integer n) { 1686 | return new Literal('NEXT_N_MONTHS:' + n); 1687 | } 1688 | 1689 | public static Literal LAST_N_MONTHS(Integer n) { 1690 | return new Literal('LAST_N_MONTHS:' + n); 1691 | } 1692 | 1693 | public static Literal N_MONTHS_AGO(Integer n) { 1694 | return new Literal('N_MONTHS_AGO:' + n); 1695 | } 1696 | 1697 | public static Literal NEXT_N_QUARTERS(Integer n) { 1698 | return new Literal('NEXT_N_QUARTERS:' + n); 1699 | } 1700 | 1701 | public static Literal LAST_N_QUARTERS(Integer n) { 1702 | return new Literal('LAST_N_QUARTERS:' + n); 1703 | } 1704 | 1705 | public static Literal N_QUARTERS_AGO(Integer n) { 1706 | return new Literal('N_QUARTERS_AGO:' + n); 1707 | } 1708 | 1709 | public static Literal NEXT_N_YEARS(Integer n) { 1710 | return new Literal('NEXT_N_YEARS:' + n); 1711 | } 1712 | 1713 | public static Literal LAST_N_YEARS(Integer n) { 1714 | return new Literal('LAST_N_YEARS:' + n); 1715 | } 1716 | 1717 | public static Literal N_YEARS_AGO(Integer n) { 1718 | return new Literal('N_YEARS_AGO:' + n); 1719 | } 1720 | 1721 | public static Literal NEXT_N_FISCAL_QUARTERS(Integer n) { 1722 | return new Literal('NEXT_N_FISCAL_QUARTERS:' + n); 1723 | } 1724 | 1725 | public static Literal N_FISCAL_QUARTERS_AGO(Integer n) { 1726 | return new Literal('N_FISCAL_QUARTERS_AGO:' + n); 1727 | } 1728 | 1729 | public static Literal NEXT_N_FISCAL_YEARS(Integer n) { 1730 | return new Literal('NEXT_N_FISCAL_YEARS:' + n); 1731 | } 1732 | 1733 | public static Literal LAST_N_FISCAL_YEARS(Integer n) { 1734 | return new Literal('LAST_N_FISCAL_YEARS:' + n); 1735 | } 1736 | 1737 | public static Literal N_FISCAL_YEARS_AGO(Integer n) { 1738 | return new Literal('N_FISCAL_YEARS_AGO:' + n); 1739 | } 1740 | 1741 | // #endregion 1742 | // ==================== 1743 | 1744 | // ======================== 1745 | // #region Currency Literal 1746 | public static Literal CURRENCY(String code, Decimal value) { 1747 | return new Literal(code + value); 1748 | } 1749 | 1750 | // #endregion 1751 | // ======================== 1752 | } 1753 | -------------------------------------------------------------------------------- /apex-query/main/default/classes/QueryTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class QueryTest extends Query { 3 | @isTest 4 | static void testFrom() { 5 | Query accountQuery = Query.of('Account'); 6 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 7 | Query contactQuery = Query.of('Contact'); 8 | Assert.areEqual('SELECT Id FROM Contact', contactQuery.buildSOQL()); 9 | } 10 | 11 | @isTest 12 | static void testMisc() { 13 | Query accountQuery; 14 | 15 | accountQuery = new Query(); 16 | 17 | accountQuery = Query.of('Account').updateTracking(); 18 | Assert.areEqual('SELECT Id FROM Account UPDATE TRACKING', accountQuery.buildSOQL()); 19 | 20 | accountQuery = Query.of('Account').updateViewstat(); 21 | Assert.areEqual('SELECT Id FROM Account UPDATE VIEWSTAT', accountQuery.buildSOQL()); 22 | 23 | accountQuery = Query.of('Account').forView(); 24 | Assert.areEqual('SELECT Id FROM Account FOR VIEW', accountQuery.buildSOQL()); 25 | 26 | accountQuery = Query.of('Account').forReference(); 27 | Assert.areEqual('SELECT Id FROM Account FOR REFERENCE', accountQuery.buildSOQL()); 28 | 29 | accountQuery = Query.of('Account').forUpdate(); 30 | Assert.areEqual('SELECT Id FROM Account FOR UPDATE', accountQuery.buildSOQL()); 31 | 32 | accountQuery = Query.of('Account').limitx(3).offset(3); 33 | Assert.areEqual('SELECT Id FROM Account LIMIT 3 OFFSET 3', accountQuery.buildSOQL()); 34 | } 35 | 36 | @isTest 37 | static void testRun() { 38 | Query accountQuery = Query.of('Account').limitx(3).offset(3); 39 | accountQuery.run(); 40 | accountQuery.getLocator(); 41 | } 42 | 43 | @isTest 44 | static void testRun_getCount() { 45 | Query accountQuery = Query.of('Account').selectBy(count()).whereBy(eq('BillingCountry', 'China')); 46 | Assert.areEqual('SELECT COUNT() FROM Account WHERE (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 47 | Integer count = accountQuery.getCount(); 48 | Assert.areEqual(0, count); 49 | } 50 | 51 | @isTest 52 | static void testRun_VAR() { 53 | Query.Filter filter = eq('BillingCountry', var('country')); 54 | Query accountQuery = Query.of('Account').whereBy(filter); 55 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :country)', accountQuery.buildSOQL()); 56 | 57 | accountQuery.run(new Map{ 'country' => 'China' }); 58 | 59 | accountQuery = Query.of('Account').whereBy(filter); 60 | accountQuery.getLocator(new Map{ 'country' => 'China' }); 61 | 62 | accountQuery = Query.of('Account').whereBy(filter); 63 | accountQuery.getCount(new Map{ 'country' => 'China' }); 64 | } 65 | 66 | @isTest 67 | static void testRun_VAR_SpecialOperators() { 68 | Query.Filter filter; 69 | // Field Name 70 | filter = likex('BillingCountry', var('country')); 71 | Assert.areEqual( 72 | 'SELECT Id FROM Account WHERE (BillingCountry LIKE :country)', 73 | Query.of('Account').whereBy(filter).buildSOQL() 74 | ); 75 | 76 | filter = nlike('BillingCountry', var('country')); 77 | Assert.areEqual( 78 | 'SELECT Id FROM Account WHERE ((NOT BillingCountry LIKE :country))', 79 | Query.of('Account').whereBy(filter).buildSOQL() 80 | ); 81 | 82 | filter = inx('BillingCountry', var('countries')); 83 | Assert.areEqual( 84 | 'SELECT Id FROM Account WHERE (BillingCountry IN :countries)', 85 | Query.of('Account').whereBy(filter).buildSOQL() 86 | ); 87 | 88 | filter = nin('BillingCountry', var('countries')); 89 | Assert.areEqual( 90 | 'SELECT Id FROM Account WHERE (BillingCountry NOT IN :countries)', 91 | Query.of('Account').whereBy(filter).buildSOQL() 92 | ); 93 | 94 | // ToLabelFunction 95 | filter = likex(toLabel('BillingCountry'), var('country')); 96 | Assert.areEqual( 97 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) LIKE :country)', 98 | Query.of('Account').whereBy(filter).buildSOQL() 99 | ); 100 | 101 | filter = nlike(toLabel('BillingCountry'), var('country')); 102 | Assert.areEqual( 103 | 'SELECT Id FROM Account WHERE ((NOT toLabel(BillingCountry) LIKE :country))', 104 | Query.of('Account').whereBy(filter).buildSOQL() 105 | ); 106 | 107 | filter = inx(toLabel('BillingCountry'), var('countries')); 108 | Assert.areEqual( 109 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) IN :countries)', 110 | Query.of('Account').whereBy(filter).buildSOQL() 111 | ); 112 | 113 | filter = nin(toLabel('BillingCountry'), var('countries')); 114 | Assert.areEqual( 115 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) NOT IN :countries)', 116 | Query.of('Account').whereBy(filter).buildSOQL() 117 | ); 118 | } 119 | 120 | @isTest 121 | static void testRun_VAR_BindingVars() { 122 | Query.Filter filter = andx().add(eq('BillingCountry', var('country'))).add(gt('AnnualRevenue', 1000)); 123 | Query accountQuery = Query.of('Account').whereBy(filter); 124 | Assert.areEqual( 125 | 'SELECT Id FROM Account WHERE (BillingCountry = :country AND AnnualRevenue > :tmpVar1)', 126 | accountQuery.buildSOQL() 127 | ); 128 | 129 | Map bindingVars; 130 | bindingVars = new Map{ 'country' => 'China' }; 131 | accountQuery.run(bindingVars); 132 | Assert.areEqual('China', bindingVars.get('country')); 133 | Assert.areEqual(1000, bindingVars.get('tmpVar1')); 134 | 135 | bindingVars = new Map{ 'country' => 'Japan' }; 136 | accountQuery.run(bindingVars); 137 | Assert.areEqual('Japan', bindingVars.get('country')); 138 | Assert.areEqual(1000, bindingVars.get('tmpVar1')); 139 | } 140 | 141 | // ====================== 142 | // #region Logical Filter 143 | 144 | @isTest 145 | static void testLogical_And() { 146 | Query.Filter filter = andx() 147 | .add(eq('BillingCountry', 'China')) 148 | .add(eq('BillingCountry', 'China')) 149 | .add(eq('BillingCountry', 'China')); 150 | Query accountQuery = Query.of('Account').whereBy(filter); 151 | Assert.areEqual( 152 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 ' + 153 | 'AND BillingCountry = :tmpVar2 ' + 154 | 'AND BillingCountry = :tmpVar3)', 155 | accountQuery.buildSOQL() 156 | ); 157 | 158 | filter = andx() 159 | .addAll( 160 | new List{ 161 | eq('BillingCountry', 'China'), 162 | eq('BillingCountry', 'China'), 163 | eq('BillingCountry', 'China') 164 | } 165 | ); 166 | accountQuery = Query.of('Account').whereBy(filter); 167 | Assert.areEqual( 168 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 ' + 169 | 'AND BillingCountry = :tmpVar2 ' + 170 | 'AND BillingCountry = :tmpVar3)', 171 | accountQuery.buildSOQL() 172 | ); 173 | } 174 | 175 | @isTest 176 | static void testLogical_Or() { 177 | Query.Filter filter = orx() 178 | .add(eq('BillingCountry', 'China')) 179 | .add(eq('BillingCountry', 'China')) 180 | .add(eq('BillingCountry', 'China')); 181 | Query accountQuery = Query.of('Account').whereBy(filter); 182 | Assert.areEqual( 183 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 ' + 184 | 'OR BillingCountry = :tmpVar2 ' + 185 | 'OR BillingCountry = :tmpVar3)', 186 | accountQuery.buildSOQL() 187 | ); 188 | 189 | filter = orx() 190 | .addAll( 191 | new List{ 192 | eq('BillingCountry', 'China'), 193 | eq('BillingCountry', 'China'), 194 | eq('BillingCountry', 'China') 195 | } 196 | ); 197 | accountQuery = Query.of('Account').whereBy(filter); 198 | Assert.areEqual( 199 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 ' + 200 | 'OR BillingCountry = :tmpVar2 ' + 201 | 'OR BillingCountry = :tmpVar3)', 202 | accountQuery.buildSOQL() 203 | ); 204 | } 205 | 206 | @isTest 207 | static void testLogical_Not() { 208 | Query.Filter filter = notx(eq('BillingCountry', 'China')); 209 | Query accountQuery = Query.of('Account').whereBy(filter); 210 | Assert.areEqual('SELECT Id FROM Account WHERE NOT(BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 211 | 212 | filter = notx( 213 | andx().add(eq('BillingCountry', 'China')).add(eq('BillingState', 'Shanghai')).add(eq('AnnualRevenue', 1500)) 214 | ); 215 | accountQuery = Query.of('Account').whereBy(filter); 216 | Assert.areEqual( 217 | 'SELECT Id FROM Account WHERE NOT(BillingCountry = :tmpVar1 ' + 218 | 'AND BillingState = :tmpVar2 ' + 219 | 'AND AnnualRevenue = :tmpVar3)', 220 | accountQuery.buildSOQL() 221 | ); 222 | } 223 | 224 | @isTest // prettier-ignore 225 | static void testLogical_Where() { 226 | Filter filter = eq('BillingCountry', 'China'); 227 | 228 | // empty where clause 229 | Query accountQuery = Query.of('Account').whereBy(orx()); 230 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 231 | 232 | accountQuery = Query.of('Account').whereBy(andx()); 233 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 234 | 235 | accountQuery = Query.of('Account'); 236 | accountQuery.whereBy(); 237 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 238 | 239 | 240 | // single filter in logical operator 241 | accountQuery = Query.of('Account').whereBy(orx()); 242 | accountQuery.whereBy().add(filter); 243 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 244 | 245 | accountQuery = Query.of('Account').whereBy(andx()); 246 | accountQuery.whereBy().add(filter); 247 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 248 | 249 | accountQuery = Query.of('Account'); 250 | accountQuery.whereBy().add(filter); 251 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 252 | 253 | // double filters in logical operator 254 | accountQuery = Query.of('Account').whereBy(orx()); 255 | accountQuery.whereBy().add(filter); 256 | accountQuery.whereBy().add(filter); 257 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 OR BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 258 | 259 | accountQuery = Query.of('Account').whereBy(andx()); 260 | accountQuery.whereBy().add(filter); 261 | accountQuery.whereBy().add(filter); 262 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 263 | 264 | accountQuery = Query.of('Account'); 265 | accountQuery.whereBy().add(filter); 266 | accountQuery.whereBy().add(filter); 267 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 268 | 269 | // double filters in logical operator 270 | accountQuery = Query.of('Account').whereBy(orx().add(filter)); 271 | accountQuery.whereBy().add(filter); 272 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 OR BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 273 | 274 | accountQuery = Query.of('Account').whereBy(andx().add(filter)); 275 | accountQuery.whereBy().add(filter); 276 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 277 | 278 | accountQuery = Query.of('Account').whereBy(filter); 279 | accountQuery.whereBy().add(filter); 280 | Assert.areEqual('SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 281 | } 282 | 283 | @isTest // prettier-ignore 284 | static void testLogical_Having() { 285 | Filter filter = eq('BillingCountry', 'China'); 286 | 287 | // empty where clause 288 | Query accountQuery = Query.of('Account').havingBy(orx()); 289 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 290 | 291 | accountQuery = Query.of('Account').havingBy(andx()); 292 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 293 | 294 | accountQuery = Query.of('Account'); 295 | accountQuery.havingBy(); 296 | Assert.areEqual('SELECT Id FROM Account', accountQuery.buildSOQL()); 297 | 298 | 299 | // single filter in logical operator 300 | accountQuery = Query.of('Account').havingBy(orx()); 301 | accountQuery.havingBy().add(filter); 302 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 303 | 304 | accountQuery = Query.of('Account').havingBy(andx()); 305 | accountQuery.havingBy().add(filter); 306 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 307 | 308 | accountQuery = Query.of('Account'); 309 | accountQuery.havingBy().add(filter); 310 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1)', accountQuery.buildSOQL()); 311 | 312 | // double filters in logical operator 313 | accountQuery = Query.of('Account').havingBy(orx()); 314 | accountQuery.havingBy().add(filter); 315 | accountQuery.havingBy().add(filter); 316 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1 OR BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 317 | 318 | accountQuery = Query.of('Account').havingBy(andx()); 319 | accountQuery.havingBy().add(filter); 320 | accountQuery.havingBy().add(filter); 321 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 322 | 323 | accountQuery = Query.of('Account'); 324 | accountQuery.havingBy().add(filter); 325 | accountQuery.havingBy().add(filter); 326 | Assert.areEqual('SELECT Id FROM Account HAVING (BillingCountry = :tmpVar1 AND BillingCountry = :tmpVar2)', accountQuery.buildSOQL()); 327 | } 328 | 329 | // #endregion 330 | // ====================== 331 | 332 | // ========================= 333 | // #region Comparison Filter 334 | 335 | @isTest // prettier-ignore 336 | static void testComparison_EQ() { 337 | // param1 are Strings 338 | Query.Filter filter = eq('BillingCountry', 'China'); 339 | Assert.areEqual( 340 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 341 | Query.of('Account').whereBy(filter).buildSOQL() 342 | ); 343 | 344 | filter = eq('BillingCountry', true); 345 | Assert.areEqual( 346 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 347 | Query.of('Account').whereBy(filter).buildSOQL() 348 | ); 349 | 350 | filter = eq('BillingCountry', 123.456); 351 | Assert.areEqual( 352 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 353 | Query.of('Account').whereBy(filter).buildSOQL() 354 | ); 355 | 356 | filter = eq('BillingCountry', Datetime.newInstance(2023, 1, 1, 12, 12, 12)); 357 | Assert.areEqual( 358 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 359 | Query.of('Account').whereBy(filter).buildSOQL() 360 | ); 361 | 362 | filter = eq('BillingCountry', Date.newInstance(2023, 1, 1)); 363 | Assert.areEqual( 364 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 365 | Query.of('Account').whereBy(filter).buildSOQL() 366 | ); 367 | 368 | filter = eq('BillingCountry', Time.newInstance(12, 12, 12, 12)); 369 | Assert.areEqual( 370 | 'SELECT Id FROM Account WHERE (BillingCountry = :tmpVar1)', 371 | Query.of('Account').whereBy(filter).buildSOQL() 372 | ); 373 | 374 | filter = eq('BillingCountry', LAST_N_DAYS(2)); 375 | Assert.areEqual( 376 | 'SELECT Id FROM Account WHERE (BillingCountry = LAST_N_DAYS:2)', 377 | Query.of('Account').whereBy(filter).buildSOQL() 378 | ); 379 | 380 | filter = eq('BillingCountry', CURRENCY('CNY', 2000)); 381 | Assert.areEqual( 382 | 'SELECT Id FROM Account WHERE (BillingCountry = CNY2000)', 383 | Query.of('Account').whereBy(filter).buildSOQL() 384 | ); 385 | 386 | filter = eq('BillingCountry', null); 387 | Assert.areEqual( 388 | 'SELECT Id FROM Account WHERE (BillingCountry = NULL)', 389 | Query.of('Account').whereBy(filter).buildSOQL() 390 | ); 391 | 392 | // param1 are others 393 | filter = eq(distance('ShippingAddress', Location.newInstance(37.775001, -122.41801), 'km'), 12.3); 394 | Assert.areEqual( 395 | 'SELECT Id FROM Account WHERE (DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), \'km\') = :tmpVar1)', 396 | Query.of('Account').whereBy(filter).buildSOQL() 397 | ); 398 | 399 | filter = eq(calendarMonth('CreatedDate'), 2); 400 | Assert.areEqual( 401 | 'SELECT Id FROM Account WHERE (CALENDAR_MONTH(CreatedDate) = :tmpVar1)', 402 | Query.of('Account').whereBy(filter).buildSOQL() 403 | ); 404 | 405 | filter = eq(toLabel('AccountSource'), 'Advertisement'); 406 | Assert.areEqual( 407 | 'SELECT Id FROM Account WHERE (toLabel(AccountSource) = :tmpVar1)', 408 | Query.of('Account').whereBy(filter).buildSOQL() 409 | ); 410 | 411 | filter = eq(max('BillingCountry'), 'China'); 412 | Assert.areEqual( 413 | 'SELECT Id FROM Account HAVING (MAX(BillingCountry) = :tmpVar1)', 414 | Query.of('Account').havingBy(filter).buildSOQL() 415 | ); 416 | } 417 | 418 | @isTest // prettier-ignore 419 | static void testOperator_Comparison_NE() { 420 | // param1 are Strings 421 | Query.Filter filter = ne('BillingCountry', 'China'); 422 | Assert.areEqual( 423 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 424 | Query.of('Account').whereBy(filter).buildSOQL() 425 | ); 426 | 427 | filter = ne('BillingCountry', true); 428 | Assert.areEqual( 429 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 430 | Query.of('Account').whereBy(filter).buildSOQL() 431 | ); 432 | 433 | filter = ne('BillingCountry', 123.456); 434 | Assert.areEqual( 435 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 436 | Query.of('Account').whereBy(filter).buildSOQL() 437 | ); 438 | 439 | filter = ne('BillingCountry', Datetime.newInstance(2023, 1, 1, 12, 12, 12)); 440 | Assert.areEqual( 441 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 442 | Query.of('Account').whereBy(filter).buildSOQL() 443 | ); 444 | 445 | filter = ne('BillingCountry', Date.newInstance(2023, 1, 1)); 446 | Assert.areEqual( 447 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 448 | Query.of('Account').whereBy(filter).buildSOQL() 449 | ); 450 | 451 | filter = ne('BillingCountry', Time.newInstance(12, 12, 12, 12)); 452 | Assert.areEqual( 453 | 'SELECT Id FROM Account WHERE (BillingCountry != :tmpVar1)', 454 | Query.of('Account').whereBy(filter).buildSOQL() 455 | ); 456 | 457 | filter = ne('BillingCountry', LAST_N_DAYS(2)); 458 | Assert.areEqual( 459 | 'SELECT Id FROM Account WHERE (BillingCountry != LAST_N_DAYS:2)', 460 | Query.of('Account').whereBy(filter).buildSOQL() 461 | ); 462 | 463 | filter = ne('BillingCountry', CURRENCY('CNY', 2000)); 464 | Assert.areEqual( 465 | 'SELECT Id FROM Account WHERE (BillingCountry != CNY2000)', 466 | Query.of('Account').whereBy(filter).buildSOQL() 467 | ); 468 | 469 | filter = ne('BillingCountry', null); 470 | Assert.areEqual( 471 | 'SELECT Id FROM Account WHERE (BillingCountry != NULL)', 472 | Query.of('Account').whereBy(filter).buildSOQL() 473 | ); 474 | 475 | // param1 are others 476 | filter = ne(distance('ShippingAddress', Location.newInstance(37.775001, -122.41801), 'km'), 12.3); 477 | Assert.areEqual( 478 | 'SELECT Id FROM Account WHERE (DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), \'km\') != :tmpVar1)', 479 | Query.of('Account').whereBy(filter).buildSOQL() 480 | ); 481 | 482 | filter = ne(calendarMonth('CreatedDate'), 2); 483 | Assert.areEqual( 484 | 'SELECT Id FROM Account WHERE (CALENDAR_MONTH(CreatedDate) != :tmpVar1)', 485 | Query.of('Account').whereBy(filter).buildSOQL() 486 | ); 487 | 488 | filter = ne(toLabel('AccountSource'), 'Advertisement'); 489 | Assert.areEqual( 490 | 'SELECT Id FROM Account WHERE (toLabel(AccountSource) != :tmpVar1)', 491 | Query.of('Account').whereBy(filter).buildSOQL() 492 | ); 493 | 494 | filter = ne(max('BillingCountry'), 'China'); 495 | Assert.areEqual( 496 | 'SELECT Id FROM Account HAVING (MAX(BillingCountry) != :tmpVar1)', 497 | Query.of('Account').havingBy(filter).buildSOQL() 498 | ); 499 | } 500 | 501 | @isTest // prettier-ignore 502 | static void testComparison_BETWEEN() { 503 | // param1 are Strings 504 | Query.Filter filter = between('BillingCountry', 'China', 'China'); 505 | Assert.areEqual( 506 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1 AND BillingCountry <= :tmpVar2)', 507 | Query.of('Account').whereBy(filter).buildSOQL() 508 | ); 509 | 510 | filter = between('BillingCountry', 123.456, 123.456); 511 | Assert.areEqual( 512 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1 AND BillingCountry <= :tmpVar2)', 513 | Query.of('Account').whereBy(filter).buildSOQL() 514 | ); 515 | 516 | filter = between( 517 | 'BillingCountry', 518 | Datetime.newInstance(2023, 1, 1, 12, 12, 12), 519 | Datetime.newInstance(2023, 1, 1, 12, 12, 12) 520 | ); 521 | Assert.areEqual( 522 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1 AND BillingCountry <= :tmpVar2)', 523 | Query.of('Account').whereBy(filter).buildSOQL() 524 | ); 525 | 526 | filter = between('BillingCountry', Date.newInstance(2023, 1, 1), Date.newInstance(2023, 1, 1)); 527 | Assert.areEqual( 528 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1 AND BillingCountry <= :tmpVar2)', 529 | Query.of('Account').whereBy(filter).buildSOQL() 530 | ); 531 | 532 | filter = between('BillingCountry', Time.newInstance(12, 12, 12, 12), Time.newInstance(12, 12, 12, 12)); 533 | Assert.areEqual( 534 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1 AND BillingCountry <= :tmpVar2)', 535 | Query.of('Account').whereBy(filter).buildSOQL() 536 | ); 537 | 538 | filter = between('BillingCountry', LAST_N_DAYS(2), LAST_N_DAYS(2)); 539 | Assert.areEqual( 540 | 'SELECT Id FROM Account WHERE (BillingCountry >= LAST_N_DAYS:2 AND BillingCountry <= LAST_N_DAYS:2)', 541 | Query.of('Account').whereBy(filter).buildSOQL() 542 | ); 543 | 544 | filter = between('BillingCountry', CURRENCY('CNY', 2000), CURRENCY('CNY', 2000)); 545 | Assert.areEqual( 546 | 'SELECT Id FROM Account WHERE (BillingCountry >= CNY2000 AND BillingCountry <= CNY2000)', 547 | Query.of('Account').whereBy(filter).buildSOQL() 548 | ); 549 | 550 | // param1 are others 551 | filter = between( 552 | DISTANCE('ShippingAddress', Location.newInstance(37.775001, -122.41801), 'km'), 553 | 12.3, 554 | 12.3 555 | ); 556 | Assert.areEqual( 557 | 'SELECT Id FROM Account WHERE (DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), \'km\') >= :tmpVar1 AND DISTANCE(ShippingAddress, GEOLOCATION(37.775001, -122.41801), \'km\') <= :tmpVar2)', 558 | Query.of('Account').whereBy(filter).buildSOQL() 559 | ); 560 | 561 | filter = between(calendarMonth('CreatedDate'), 2, 2); 562 | Assert.areEqual( 563 | 'SELECT Id FROM Account WHERE (CALENDAR_MONTH(CreatedDate) >= :tmpVar1 AND CALENDAR_MONTH(CreatedDate) <= :tmpVar2)', 564 | Query.of('Account').whereBy(filter).buildSOQL() 565 | ); 566 | 567 | filter = between(toLabel('AccountSource'), 'Advertisement', 'Advertisement'); 568 | Assert.areEqual( 569 | 'SELECT Id FROM Account WHERE (toLabel(AccountSource) >= :tmpVar1 AND toLabel(AccountSource) <= :tmpVar2)', 570 | Query.of('Account').whereBy(filter).buildSOQL() 571 | ); 572 | 573 | filter = between(max('BillingCountry'), 'China', 'China'); 574 | Assert.areEqual( 575 | 'SELECT Id FROM Account HAVING (MAX(BillingCountry) >= :tmpVar1 AND MAX(BillingCountry) <= :tmpVar2)', 576 | Query.of('Account').havingBy(filter).buildSOQL() 577 | ); 578 | 579 | } 580 | 581 | @isTest 582 | static void testComparison_LT_GT() { 583 | // param1 are Strings 584 | Query.Filter filter = lt('BillingCountry', 'China'); 585 | Assert.areEqual( 586 | 'SELECT Id FROM Account WHERE (BillingCountry < :tmpVar1)', 587 | Query.of('Account').whereBy(filter).buildSOQL() 588 | ); 589 | 590 | filter = lte('BillingCountry', 'China'); 591 | Assert.areEqual( 592 | 'SELECT Id FROM Account WHERE (BillingCountry <= :tmpVar1)', 593 | Query.of('Account').whereBy(filter).buildSOQL() 594 | ); 595 | 596 | filter = gt('BillingCountry', 'China'); 597 | Assert.areEqual( 598 | 'SELECT Id FROM Account WHERE (BillingCountry > :tmpVar1)', 599 | Query.of('Account').whereBy(filter).buildSOQL() 600 | ); 601 | 602 | filter = gte('BillingCountry', 'China'); 603 | Assert.areEqual( 604 | 'SELECT Id FROM Account WHERE (BillingCountry >= :tmpVar1)', 605 | Query.of('Account').whereBy(filter).buildSOQL() 606 | ); 607 | 608 | filter = lt(toLabel('BillingCountry'), 'China'); 609 | Assert.areEqual( 610 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) < :tmpVar1)', 611 | Query.of('Account').whereBy(filter).buildSOQL() 612 | ); 613 | 614 | filter = lte(toLabel('BillingCountry'), 'China'); 615 | Assert.areEqual( 616 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) <= :tmpVar1)', 617 | Query.of('Account').whereBy(filter).buildSOQL() 618 | ); 619 | 620 | filter = gt(toLabel('BillingCountry'), 'China'); 621 | Assert.areEqual( 622 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) > :tmpVar1)', 623 | Query.of('Account').whereBy(filter).buildSOQL() 624 | ); 625 | 626 | filter = gte(toLabel('BillingCountry'), 'China'); 627 | Assert.areEqual( 628 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) >= :tmpVar1)', 629 | Query.of('Account').whereBy(filter).buildSOQL() 630 | ); 631 | } 632 | 633 | @isTest // prettier-ignore 634 | static void testComparison_LIKE() { 635 | Query.Filter filter; 636 | filter = likex('BillingCountry', '%China%'); 637 | Assert.areEqual( 638 | 'SELECT Id FROM Account WHERE (BillingCountry LIKE :tmpVar1)', 639 | Query.of('Account').whereBy(filter).buildSOQL() 640 | ); 641 | 642 | filter = likex(toLabel('BillingCountry'), '%China%'); 643 | Assert.areEqual( 644 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) LIKE :tmpVar1)', 645 | Query.of('Account').whereBy(filter).buildSOQL() 646 | ); 647 | 648 | filter = likex(max('BillingCountry'), '%China%'); 649 | Assert.areEqual( 650 | 'SELECT Id FROM Account HAVING (MAX(BillingCountry) LIKE :tmpVar1)', 651 | Query.of('Account').havingBy(filter).buildSOQL() 652 | ); 653 | } 654 | 655 | @isTest // prettier-ignore 656 | static void testComparison_NLIKE() { 657 | Query.Filter filter; 658 | filter = nlike('BillingCountry', '%China%'); 659 | Assert.areEqual( 660 | 'SELECT Id FROM Account WHERE ((NOT BillingCountry LIKE :tmpVar1))', 661 | Query.of('Account').whereBy(filter).buildSOQL() 662 | ); 663 | 664 | filter = nlike(toLabel('BillingCountry'), '%China%'); 665 | Assert.areEqual( 666 | 'SELECT Id FROM Account WHERE ((NOT toLabel(BillingCountry) LIKE :tmpVar1))', 667 | Query.of('Account').whereBy(filter).buildSOQL() 668 | ); 669 | 670 | filter = nlike(max('BillingCountry'), '%China%'); 671 | Assert.areEqual( 672 | 'SELECT Id FROM Account HAVING ((NOT MAX(BillingCountry) LIKE :tmpVar1))', 673 | Query.of('Account').havingBy(filter).buildSOQL() 674 | ); 675 | } 676 | 677 | @isTest // prettier-ignore 678 | static void testComparison_INCLUDES() { 679 | Query.Filter filter; 680 | filter = includes('BillingCountry', new List{ 'AAA', 'BBB', 'CCC', 'DDD', 'EEE' }); 681 | Assert.areEqual( 682 | 'SELECT Id FROM Account WHERE (BillingCountry INCLUDES (:tmpVar1, :tmpVar2, :tmpVar3, :tmpVar4, :tmpVar5))', 683 | Query.of('Account').whereBy(filter).buildSOQL() 684 | ); 685 | 686 | filter = excludes('BillingCountry', new List{ 'AAA', 'BBB', 'CCC', 'DDD', 'EEE' }); 687 | Assert.areEqual( 688 | 'SELECT Id FROM Account WHERE (BillingCountry EXCLUDES (:tmpVar1, :tmpVar2, :tmpVar3, :tmpVar4, :tmpVar5))', 689 | Query.of('Account').whereBy(filter).buildSOQL() 690 | ); 691 | 692 | filter = includes(toLabel('BillingCountry'), new List{ 'AAA', 'BBB', 'CCC', 'DDD', 'EEE' }); 693 | Assert.areEqual( 694 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) INCLUDES (:tmpVar1, :tmpVar2, :tmpVar3, :tmpVar4, :tmpVar5))', 695 | Query.of('Account').whereBy(filter).buildSOQL() 696 | ); 697 | 698 | filter = excludes(toLabel('BillingCountry'), new List{ 'AAA', 'BBB', 'CCC', 'DDD', 'EEE' }); 699 | Assert.areEqual( 700 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) EXCLUDES (:tmpVar1, :tmpVar2, :tmpVar3, :tmpVar4, :tmpVar5))', 701 | Query.of('Account').whereBy(filter).buildSOQL() 702 | ); 703 | } 704 | 705 | @isTest // prettier-ignore 706 | static void testComparison_INX() { 707 | // String 708 | Query.Filter filter = inx('BillingCountry', new List{ 'AAA', 'BBB', null }); 709 | Assert.areEqual( 710 | 'SELECT Id FROM Account WHERE (BillingCountry IN :tmpVar1)', 711 | Query.of('Account').whereBy(filter).buildSOQL() 712 | ); 713 | 714 | filter = inx(toLabel('BillingCountry'), new List{ 'AAA', 'BBB', null }); 715 | Assert.areEqual( 716 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) IN :tmpVar1)', 717 | Query.of('Account').whereBy(filter).buildSOQL() 718 | ); 719 | 720 | Query contactQuery = Query.of('Contact').selectBy('AccountId'); 721 | filter = inx('Id', contactQuery); 722 | Assert.areEqual( 723 | 'SELECT Id FROM Account WHERE (Id IN (SELECT AccountId FROM Contact))', 724 | Query.of('Account').whereBy(filter).buildSOQL() 725 | ); 726 | } 727 | 728 | @isTest 729 | static void testOperator_Comparison_NIN() { 730 | Query.Filter filter = nin('BillingCountry', new List{ 'AAA', 'BBB', null }); 731 | Assert.areEqual( 732 | 'SELECT Id FROM Account WHERE (BillingCountry NOT IN :tmpVar1)', 733 | Query.of('Account').whereBy(filter).buildSOQL() 734 | ); 735 | 736 | filter = nin(toLabel('BillingCountry'), new List{ 'AAA', 'BBB', null }); 737 | Assert.areEqual( 738 | 'SELECT Id FROM Account WHERE (toLabel(BillingCountry) NOT IN :tmpVar1)', 739 | Query.of('Account').whereBy(filter).buildSOQL() 740 | ); 741 | 742 | Query contactQuery = Query.of('Contact').selectBy('AccountId'); 743 | filter = nin('Id', contactQuery); 744 | Assert.areEqual( 745 | 'SELECT Id FROM Account WHERE (Id NOT IN (SELECT AccountId FROM Contact))', 746 | Query.of('Account').whereBy(filter).buildSOQL() 747 | ); 748 | } 749 | 750 | // #endregion 751 | // ========================= 752 | 753 | // ===================== 754 | // #region Select Clause 755 | 756 | @isTest // prettier-ignore 757 | static void testSelect_Fields() { 758 | Query accountQuery = Query.of('Account').selectBy('Name1'); 759 | Assert.areEqual('SELECT Name1 FROM Account', accountQuery.buildSOQL()); 760 | 761 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2'); 762 | Assert.areEqual('SELECT Name1, Name2 FROM Account', accountQuery.buildSOQL()); 763 | 764 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3'); 765 | Assert.areEqual('SELECT Name1, Name2, Name3 FROM Account', accountQuery.buildSOQL()); 766 | 767 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4'); 768 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4 FROM Account', accountQuery.buildSOQL()); 769 | 770 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5'); 771 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5 FROM Account', accountQuery.buildSOQL()); 772 | 773 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5', 'Name6'); 774 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6 FROM Account', accountQuery.buildSOQL()); 775 | 776 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5', 'Name6', 'Name7'); 777 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6, Name7 FROM Account', accountQuery.buildSOQL()); 778 | 779 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5', 'Name6', 'Name7', 'Name8'); 780 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6, Name7, Name8 FROM Account', accountQuery.buildSOQL()); 781 | 782 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5', 'Name6', 'Name7', 'Name8', 'Name9'); 783 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6, Name7, Name8, Name9 FROM Account', accountQuery.buildSOQL()); 784 | 785 | accountQuery = Query.of('Account').selectBy('Name1', 'Name2', 'Name3', 'Name4', 'Name5', 'Name6', 'Name7', 'Name8', 'Name9', 'Name10'); 786 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6, Name7, Name8, Name9, Name10 FROM Account', accountQuery.buildSOQL()); 787 | 788 | accountQuery = Query.of('Account').selectBy(new List { 'Name1', 'Name2', 'Name3' }); 789 | Assert.areEqual('SELECT Name1, Name2, Name3 FROM Account', accountQuery.buildSOQL()); 790 | 791 | accountQuery = Query.of('Account') 792 | .selectBy(new List { 'Name1', 'Name2', 'Name3' }) 793 | .selectBy(new List { 'Name4', 'Name5', 'Name6' }); 794 | Assert.areEqual('SELECT Name1, Name2, Name3, Name4, Name5, Name6 FROM Account', accountQuery.buildSOQL()); 795 | } 796 | 797 | @isTest // prettier-ignore 798 | static void testSelect_Functions() { 799 | Query accountQuery; 800 | 801 | accountQuery = Query.of('Account').selectBy(toLabel('AccountSource')); 802 | Assert.areEqual('SELECT toLabel(AccountSource) FROM Account', accountQuery.buildSOQL()); 803 | 804 | accountQuery = Query.of('Account').selectBy(convertCurrency('AnnualRevenue')); 805 | Assert.areEqual('SELECT convertCurrency(AnnualRevenue) FROM Account', accountQuery.buildSOQL()); 806 | 807 | accountQuery = Query.of('Account').selectBy(format('AnnualRevenue')); 808 | Assert.areEqual('SELECT FORMAT(AnnualRevenue) FROM Account', accountQuery.buildSOQL()); 809 | 810 | accountQuery = Query.of('Account').selectBy(format(convertCurrency('AnnualRevenue'))); 811 | Assert.areEqual('SELECT FORMAT(convertCurrency(AnnualRevenue)) FROM Account', accountQuery.buildSOQL()); 812 | 813 | accountQuery = Query.of('Account') 814 | .whereBy(lte(DISTANCE('ShippingAddress', Location.newInstance(37.775000, -122.41800), 'mi'), 20)); 815 | Assert.areEqual( 816 | 'SELECT Id FROM Account WHERE (DISTANCE(ShippingAddress, GEOLOCATION(37.775, -122.418), \'mi\') <= :tmpVar1)', 817 | accountQuery.buildSOQL() 818 | ); 819 | 820 | // with alias 821 | accountQuery = Query.of('Account').selectBy(toLabel('AccountSource', 'alias')); 822 | Assert.areEqual('SELECT toLabel(AccountSource) AS alias FROM Account', accountQuery.buildSOQL()); 823 | 824 | accountQuery = Query.of('Account').selectBy(convertCurrency('AnnualRevenue', 'alias')); 825 | Assert.areEqual('SELECT convertCurrency(AnnualRevenue) AS alias FROM Account', accountQuery.buildSOQL()); 826 | 827 | accountQuery = Query.of('Account').selectBy(format('AnnualRevenue', 'alias')); 828 | Assert.areEqual('SELECT FORMAT(AnnualRevenue) AS alias FROM Account', accountQuery.buildSOQL()); 829 | 830 | accountQuery = Query.of('Account').selectBy(format(convertCurrency('AnnualRevenue'), 'alias')); 831 | Assert.areEqual('SELECT FORMAT(convertCurrency(AnnualRevenue)) AS alias FROM Account', accountQuery.buildSOQL()); 832 | } 833 | 834 | @isTest 835 | static void testSelect_Parent() { 836 | Query grandGrandParentAccountQuery = Query.of('Account'); 837 | 838 | Query grandParentAccountQuery = Query.of('Account') 839 | .selectBy('Name', format(convertCurrency('AnnualRevenue'), 'alias'), toLabel('Industry')) 840 | .selectParent('Parent', grandGrandParentAccountQuery); 841 | 842 | Query parentAccountQuery = Query.of('Account') 843 | .selectBy('Name', format(convertCurrency('AnnualRevenue')), toLabel('Industry')) 844 | .selectParent('Parent', grandParentAccountQuery); 845 | 846 | Query accountQuery = Query.of('Account') 847 | .selectBy('Name', format(convertCurrency('AnnualRevenue')), toLabel('Industry')) 848 | .selectParent('Parent', parentAccountQuery); 849 | Assert.areEqual( 850 | 'SELECT Name, FORMAT(convertCurrency(AnnualRevenue)), toLabel(Industry), ' + 851 | 'Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)), toLabel(Parent.Industry), ' + 852 | 'Parent.Parent.Name, FORMAT(convertCurrency(Parent.Parent.AnnualRevenue)) AS alias, toLabel(Parent.Parent.Industry), ' + 853 | 'Parent.Parent.Parent.Id FROM Account', 854 | accountQuery.buildSOQL() 855 | ); 856 | } 857 | 858 | @isTest 859 | static void testSelect_Child() { 860 | Query parentAccountQuery = Query.of('Account') 861 | .selectBy('Name', format(convertCurrency('AnnualRevenue')), toLabel('Industry')); 862 | Query opprotunityQuery = Query.of('Opportunity').selectParent('Account', parentAccountQuery); 863 | Query contactQuery = Query.of('Contact').selectChild('Opportunities', opprotunityQuery); 864 | 865 | Query accountQuery = Query.of('Account') 866 | .selectParent('Parent', parentAccountQuery) 867 | .selectChild('Contacts', contactQuery); 868 | Assert.areEqual( 869 | 'SELECT Id, ' + 870 | 'Parent.Name, FORMAT(convertCurrency(Parent.AnnualRevenue)), toLabel(Parent.Industry), ' + 871 | '(SELECT Id, (SELECT Id, ' + 872 | 'Account.Name, FORMAT(convertCurrency(Account.AnnualRevenue)), toLabel(Account.Industry) ' + 873 | 'FROM Opportunities) FROM Contacts) FROM Account', 874 | accountQuery.buildSOQL() 875 | ); 876 | } 877 | 878 | // #endregion 879 | // ===================== 880 | 881 | // ======================= 882 | // #region Group By Clause 883 | 884 | @isTest // prettier-ignore 885 | static void testGroupBy_Measure() { 886 | Query accountQuery; 887 | accountQuery = Query.of('Account').selectBy(count('Name'), count('Name', 'alias')); 888 | Assert.areEqual('SELECT COUNT(Name), COUNT(Name) alias FROM Account', accountQuery.buildSOQL()); 889 | 890 | accountQuery = Query.of('Account').selectBy(countDistinct('Name'), countDistinct('Name', 'alias')); 891 | Assert.areEqual('SELECT COUNT_DISTINCT(Name), COUNT_DISTINCT(Name) alias FROM Account', accountQuery.buildSOQL()); 892 | 893 | accountQuery = Query.of('Account').selectBy(sum('Name'), sum('Name', 'alias')); 894 | Assert.areEqual('SELECT SUM(Name), SUM(Name) alias FROM Account', accountQuery.buildSOQL()); 895 | 896 | accountQuery = Query.of('Account').selectBy(avg('Name'), avg('Name', 'alias')); 897 | Assert.areEqual('SELECT AVG(Name), AVG(Name) alias FROM Account', accountQuery.buildSOQL()); 898 | 899 | accountQuery = Query.of('Account').selectBy(max('Name'), max('Name', 'alias')); 900 | Assert.areEqual('SELECT MAX(Name), MAX(Name) alias FROM Account', accountQuery.buildSOQL()); 901 | 902 | accountQuery = Query.of('Account').selectBy(min('Name'), min('Name', 'alias')); 903 | Assert.areEqual('SELECT MIN(Name), MIN(Name) alias FROM Account', accountQuery.buildSOQL()); 904 | } 905 | 906 | @isTest // prettier-ignore 907 | static void testGroupBy_Dimension() { 908 | Query accountQuery; 909 | 910 | accountQuery = Query.of('Account').selectBy(grouping('Name'), grouping('Name', 'alias')); 911 | Assert.areEqual('SELECT GROUPING(Name), GROUPING(Name) alias FROM Account', accountQuery.buildSOQL()); 912 | 913 | accountQuery = Query.of('Account').selectBy(calendarMonth('CreatedDate')); 914 | Assert.areEqual('SELECT CALENDAR_MONTH(CreatedDate) FROM Account', accountQuery.buildSOQL()); 915 | 916 | accountQuery = Query.of('Account').selectBy(calendarQuarter('CreatedDate')); 917 | Assert.areEqual('SELECT CALENDAR_QUARTER(CreatedDate) FROM Account', accountQuery.buildSOQL()); 918 | 919 | accountQuery = Query.of('Account').selectBy(calendarYear('CreatedDate')); 920 | Assert.areEqual('SELECT CALENDAR_YEAR(CreatedDate) FROM Account', accountQuery.buildSOQL()); 921 | 922 | accountQuery = Query.of('Account').selectBy(dayInMonth('CreatedDate')); 923 | Assert.areEqual('SELECT DAY_IN_MONTH(CreatedDate) FROM Account', accountQuery.buildSOQL()); 924 | 925 | accountQuery = Query.of('Account').selectBy(dayInWeek('CreatedDate')); 926 | Assert.areEqual('SELECT DAY_IN_WEEK(CreatedDate) FROM Account', accountQuery.buildSOQL()); 927 | 928 | accountQuery = Query.of('Account').selectBy(dayInYear('CreatedDate')); 929 | Assert.areEqual('SELECT DAY_IN_YEAR(CreatedDate) FROM Account', accountQuery.buildSOQL()); 930 | 931 | accountQuery = Query.of('Account').selectBy(dayOnly('CreatedDate')); 932 | Assert.areEqual('SELECT DAY_ONLY(CreatedDate) FROM Account', accountQuery.buildSOQL()); 933 | 934 | accountQuery = Query.of('Account').selectBy(fiscalMonth('CreatedDate')); 935 | Assert.areEqual('SELECT FISCAL_MONTH(CreatedDate) FROM Account', accountQuery.buildSOQL()); 936 | 937 | accountQuery = Query.of('Account').selectBy(fiscalQuarter('CreatedDate')); 938 | Assert.areEqual('SELECT FISCAL_QUARTER(CreatedDate) FROM Account', accountQuery.buildSOQL()); 939 | 940 | accountQuery = Query.of('Account').selectBy(fiscalYear('CreatedDate')); 941 | Assert.areEqual('SELECT FISCAL_YEAR(CreatedDate) FROM Account', accountQuery.buildSOQL()); 942 | 943 | accountQuery = Query.of('Account').selectBy(hourInDay('CreatedDate')); 944 | Assert.areEqual('SELECT HOUR_IN_DAY(CreatedDate) FROM Account', accountQuery.buildSOQL()); 945 | 946 | accountQuery = Query.of('Account').selectBy(weekInMonth('CreatedDate')); 947 | Assert.areEqual('SELECT WEEK_IN_MONTH(CreatedDate) FROM Account', accountQuery.buildSOQL()); 948 | 949 | accountQuery = Query.of('Account').selectBy(weekInYear('CreatedDate')); 950 | Assert.areEqual('SELECT WEEK_IN_YEAR(CreatedDate) FROM Account', accountQuery.buildSOQL()); 951 | 952 | 953 | // convertTimezone 954 | accountQuery = Query.of('Account').selectBy(calendarMonth(convertTimezone('CreatedDate'))); 955 | Assert.areEqual('SELECT CALENDAR_MONTH(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 956 | 957 | accountQuery = Query.of('Account').selectBy(calendarQuarter(convertTimezone('CreatedDate'))); 958 | Assert.areEqual('SELECT CALENDAR_QUARTER(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 959 | 960 | accountQuery = Query.of('Account').selectBy(calendarYear(convertTimezone('CreatedDate'))); 961 | Assert.areEqual('SELECT CALENDAR_YEAR(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 962 | 963 | accountQuery = Query.of('Account').selectBy(dayInMonth(convertTimezone('CreatedDate'))); 964 | Assert.areEqual('SELECT DAY_IN_MONTH(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 965 | 966 | accountQuery = Query.of('Account').selectBy(dayInWeek(convertTimezone('CreatedDate'))); 967 | Assert.areEqual('SELECT DAY_IN_WEEK(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 968 | 969 | accountQuery = Query.of('Account').selectBy(dayInYear(convertTimezone('CreatedDate'))); 970 | Assert.areEqual('SELECT DAY_IN_YEAR(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 971 | 972 | accountQuery = Query.of('Account').selectBy(dayOnly(convertTimezone('CreatedDate'))); 973 | Assert.areEqual('SELECT DAY_ONLY(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 974 | 975 | accountQuery = Query.of('Account').selectBy(fiscalMonth(convertTimezone('CreatedDate'))); 976 | Assert.areEqual('SELECT FISCAL_MONTH(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 977 | 978 | accountQuery = Query.of('Account').selectBy(fiscalQuarter(convertTimezone('CreatedDate'))); 979 | Assert.areEqual('SELECT FISCAL_QUARTER(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 980 | 981 | accountQuery = Query.of('Account').selectBy(fiscalYear(convertTimezone('CreatedDate'))); 982 | Assert.areEqual('SELECT FISCAL_YEAR(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 983 | 984 | accountQuery = Query.of('Account').selectBy(hourInDay(convertTimezone('CreatedDate'))); 985 | Assert.areEqual('SELECT HOUR_IN_DAY(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 986 | 987 | accountQuery = Query.of('Account').selectBy(weekInMonth(convertTimezone('CreatedDate'))); 988 | Assert.areEqual('SELECT WEEK_IN_MONTH(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 989 | 990 | accountQuery = Query.of('Account').selectBy(weekInYear(convertTimezone('CreatedDate'))); 991 | Assert.areEqual('SELECT WEEK_IN_YEAR(convertTimezone(CreatedDate)) FROM Account', accountQuery.buildSOQL()); 992 | } 993 | 994 | @isTest // prettier-ignore 995 | static void testGroupBy() { 996 | // String 997 | Query accountQuery = Query.of('Account').groupBy('Name'); 998 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name', accountQuery.buildSOQL()); 999 | 1000 | accountQuery = Query.of('Account').groupBy('Name', 'Name'); 1001 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name', accountQuery.buildSOQL()); 1002 | 1003 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name'); 1004 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name', accountQuery.buildSOQL()); 1005 | 1006 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name'); 1007 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name', accountQuery.buildSOQL()); 1008 | 1009 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name'); 1010 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1011 | 1012 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name', 'Name'); 1013 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1014 | 1015 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name'); 1016 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1017 | 1018 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name'); 1019 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1020 | 1021 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name'); 1022 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1023 | 1024 | accountQuery = Query.of('Account').groupBy('Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name', 'Name'); 1025 | Assert.areEqual('SELECT Id FROM Account GROUP BY Name, Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1026 | 1027 | accountQuery = Query.of('Account') 1028 | .groupBy(new List{ 'Name', 'Name', calendarMonth('CreatedDate') }); 1029 | Assert.areEqual( 1030 | 'SELECT Id FROM Account GROUP BY Name, Name, CALENDAR_MONTH(CreatedDate)', 1031 | accountQuery.buildSOQL() 1032 | ); 1033 | 1034 | // Function 1035 | accountQuery = Query.of('Account').groupBy(calendarMonth('CreatedDate')); 1036 | Assert.areEqual('SELECT Id FROM Account GROUP BY CALENDAR_MONTH(CreatedDate)', accountQuery.buildSOQL()); 1037 | 1038 | accountQuery = Query.of('Account').groupBy(calendarMonth(convertTimezone('CreatedDate'))); 1039 | Assert.areEqual('SELECT Id FROM Account GROUP BY CALENDAR_MONTH(convertTimezone(CreatedDate))', accountQuery.buildSOQL()); 1040 | } 1041 | 1042 | @isTest // prettier-ignore 1043 | static void testGroupBy_Options() { 1044 | // String 1045 | Query accountQuery = Query.of('Account').groupBy('Name', calendarMonth('CreatedDate')).rollup(); 1046 | Assert.areEqual('SELECT Id FROM Account GROUP BY ROLLUP(Name, CALENDAR_MONTH(CreatedDate))', accountQuery.buildSOQL()); 1047 | 1048 | accountQuery = Query.of('Account').groupBy('Name', calendarMonth('CreatedDate')).cube(); 1049 | Assert.areEqual('SELECT Id FROM Account GROUP BY CUBE(Name, CALENDAR_MONTH(CreatedDate))', accountQuery.buildSOQL()); 1050 | } 1051 | 1052 | // #endregion 1053 | // ======================= 1054 | 1055 | // ======================= 1056 | // #region Order By Clause 1057 | 1058 | @isTest // prettier-ignore 1059 | static void testOrderBy() { 1060 | // String 1061 | Query.OrderField orderField = orderField('Name'); 1062 | Query accountQuery = Query.of('Account').orderBy(orderField); 1063 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name', accountQuery.buildSOQL()); 1064 | 1065 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1066 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name', accountQuery.buildSOQL()); 1067 | 1068 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField); 1069 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name', accountQuery.buildSOQL()); 1070 | 1071 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField); 1072 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name', accountQuery.buildSOQL()); 1073 | 1074 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField); 1075 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1076 | 1077 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1078 | orderField); 1079 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1080 | 1081 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1082 | orderField, orderField); 1083 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1084 | 1085 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1086 | orderField, orderField, orderField); 1087 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1088 | 1089 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1090 | orderField, orderField, orderField, orderField); 1091 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1092 | 1093 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1094 | orderField, orderField, orderField, orderField, orderField); 1095 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1096 | 1097 | accountQuery = Query.of('Account').orderBy(new List{ orderField, orderField, orderField }); 1098 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name', accountQuery.buildSOQL()); 1099 | 1100 | // Function 1101 | accountQuery = Query.of('Account').orderBy(orderField(max('Name'))); 1102 | Assert.areEqual('SELECT Id FROM Account ORDER BY MAX(Name)', accountQuery.buildSOQL()); 1103 | 1104 | // Mixing 1105 | accountQuery = Query.of('Account').orderBy(new List{ orderField, orderField(max('Name')) }); 1106 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, MAX(Name)', accountQuery.buildSOQL()); 1107 | } 1108 | 1109 | @isTest // prettier-ignore 1110 | static void testOrderBy_StringInputs() { 1111 | // String 1112 | String orderField = 'Name'; 1113 | Query accountQuery = Query.of('Account').orderBy(orderField); 1114 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name', accountQuery.buildSOQL()); 1115 | 1116 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1117 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name', accountQuery.buildSOQL()); 1118 | 1119 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField); 1120 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name', accountQuery.buildSOQL()); 1121 | 1122 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField); 1123 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name', accountQuery.buildSOQL()); 1124 | 1125 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField); 1126 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1127 | 1128 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1129 | orderField); 1130 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1131 | 1132 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1133 | orderField, orderField); 1134 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1135 | 1136 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1137 | orderField, orderField, orderField); 1138 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1139 | 1140 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1141 | orderField, orderField, orderField, orderField); 1142 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1143 | 1144 | accountQuery = Query.of('Account').orderBy(orderField, orderField, orderField, orderField, orderField, 1145 | orderField, orderField, orderField, orderField, orderField); 1146 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name, Name, Name, Name, Name, Name, Name, Name', accountQuery.buildSOQL()); 1147 | 1148 | accountQuery = Query.of('Account').orderBy(new List{ orderField, orderField, orderField }); 1149 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name, Name', accountQuery.buildSOQL()); 1150 | 1151 | // Function 1152 | accountQuery = Query.of('Account').orderBy(max('Name')); 1153 | Assert.areEqual('SELECT Id FROM Account ORDER BY MAX(Name)', accountQuery.buildSOQL()); 1154 | 1155 | // Mixing 1156 | accountQuery = Query.of('Account').orderBy(new List{ orderField, max('Name') }); 1157 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, MAX(Name)', accountQuery.buildSOQL()); 1158 | } 1159 | 1160 | @isTest 1161 | static void testOrderBy_Options() { 1162 | Query.OrderField orderField = orderField('Name').ascending().nullsFirst(); 1163 | Query accountQuery = Query.of('Account').orderBy(orderField, orderField); 1164 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name', accountQuery.buildSOQL()); 1165 | 1166 | orderField = orderField('Name').descending().nullsLast().ascending().nullsFirst(); 1167 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1168 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name', accountQuery.buildSOQL()); 1169 | 1170 | orderField = orderField('Name').descending().nullsLast(); 1171 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1172 | Assert.areEqual( 1173 | 'SELECT Id FROM Account ORDER BY Name DESC NULLS LAST, Name DESC NULLS LAST', 1174 | accountQuery.buildSOQL() 1175 | ); 1176 | 1177 | orderField = orderField('Name').ascending().nullsFirst().descending().nullsLast(); 1178 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1179 | Assert.areEqual( 1180 | 'SELECT Id FROM Account ORDER BY Name DESC NULLS LAST, Name DESC NULLS LAST', 1181 | accountQuery.buildSOQL() 1182 | ); 1183 | } 1184 | 1185 | @isTest 1186 | static void testOrderBy_Options_StringInputs() { 1187 | String orderField = 'Name'; 1188 | Query accountQuery = Query.of('Account').orderBy(orderField, orderField); 1189 | Assert.areEqual('SELECT Id FROM Account ORDER BY Name, Name', accountQuery.buildSOQL()); 1190 | 1191 | orderField = 'Name DESC NULLS LAST'; 1192 | accountQuery = Query.of('Account').orderBy(orderField, orderField); 1193 | Assert.areEqual( 1194 | 'SELECT Id FROM Account ORDER BY Name DESC NULLS LAST, Name DESC NULLS LAST', 1195 | accountQuery.buildSOQL() 1196 | ); 1197 | } 1198 | 1199 | // #endregion 1200 | // ======================= 1201 | 1202 | // =============== 1203 | // #region Literal 1204 | 1205 | @isTest 1206 | static void testLiteral_Date() { 1207 | Query accountQuery = Query.of('Account') 1208 | .whereBy( 1209 | orx() 1210 | .add(eq('CreatedDate', YESTERDAY())) 1211 | .add(eq('CreatedDate', TODAY())) 1212 | .add(eq('CreatedDate', TOMORROW())) 1213 | .add(eq('CreatedDate', LAST_WEEK())) 1214 | .add(eq('CreatedDate', THIS_WEEK())) 1215 | .add(eq('CreatedDate', NEXT_WEEK())) 1216 | .add(eq('CreatedDate', LAST_MONTH())) 1217 | .add(eq('CreatedDate', THIS_MONTH())) 1218 | .add(eq('CreatedDate', NEXT_MONTH())) 1219 | .add(eq('CreatedDate', LAST_90_DAYS())) 1220 | .add(eq('CreatedDate', NEXT_90_DAYS())) 1221 | .add(eq('CreatedDate', THIS_QUARTER())) 1222 | .add(eq('CreatedDate', LAST_QUARTER())) 1223 | .add(eq('CreatedDate', NEXT_QUARTER())) 1224 | .add(eq('CreatedDate', THIS_YEAR())) 1225 | .add(eq('CreatedDate', LAST_YEAR())) 1226 | .add(eq('CreatedDate', NEXT_YEAR())) 1227 | .add(eq('CreatedDate', THIS_FISCAL_QUARTER())) 1228 | .add(eq('CreatedDate', LAST_FISCAL_QUARTER())) 1229 | .add(eq('CreatedDate', NEXT_FISCAL_QUARTER())) 1230 | .add(eq('CreatedDate', THIS_FISCAL_YEAR())) 1231 | .add(eq('CreatedDate', LAST_FISCAL_YEAR())) 1232 | .add(eq('CreatedDate', NEXT_FISCAL_YEAR())) 1233 | .add(eq('CreatedDate', LAST_N_DAYS(3))) 1234 | .add(eq('CreatedDate', NEXT_N_DAYS(3))) 1235 | .add(eq('CreatedDate', N_DAYS_AGO(3))) 1236 | .add(eq('CreatedDate', NEXT_N_WEEKS(3))) 1237 | .add(eq('CreatedDate', LAST_N_WEEKS(3))) 1238 | .add(eq('CreatedDate', N_WEEKS_AGO(3))) 1239 | .add(eq('CreatedDate', NEXT_N_MONTHS(3))) 1240 | .add(eq('CreatedDate', LAST_N_MONTHS(3))) 1241 | .add(eq('CreatedDate', N_MONTHS_AGO(3))) 1242 | .add(eq('CreatedDate', NEXT_N_QUARTERS(3))) 1243 | .add(eq('CreatedDate', LAST_N_QUARTERS(3))) 1244 | .add(eq('CreatedDate', N_QUARTERS_AGO(3))) 1245 | .add(eq('CreatedDate', NEXT_N_YEARS(3))) 1246 | .add(eq('CreatedDate', LAST_N_YEARS(3))) 1247 | .add(eq('CreatedDate', N_YEARS_AGO(3))) 1248 | .add(eq('CreatedDate', NEXT_N_FISCAL_QUARTERS(3))) 1249 | .add(eq('CreatedDate', N_FISCAL_QUARTERS_AGO(3))) 1250 | .add(eq('CreatedDate', NEXT_N_FISCAL_YEARS(3))) 1251 | .add(eq('CreatedDate', LAST_N_FISCAL_YEARS(3))) 1252 | .add(eq('CreatedDate', N_FISCAL_YEARS_AGO(3))) 1253 | ); 1254 | Assert.areEqual( 1255 | 'SELECT Id FROM Account WHERE ' + 1256 | '(CreatedDate = YESTERDAY OR CreatedDate = TODAY OR CreatedDate = TOMORROW OR ' + 1257 | 'CreatedDate = LAST_WEEK OR CreatedDate = THIS_WEEK OR CreatedDate = NEXT_WEEK OR ' + 1258 | 'CreatedDate = LAST_MONTH OR CreatedDate = THIS_MONTH OR CreatedDate = NEXT_MONTH OR ' + 1259 | 'CreatedDate = LAST_90_DAYS OR CreatedDate = NEXT_90_DAYS OR CreatedDate = THIS_QUARTER OR ' + 1260 | 'CreatedDate = LAST_QUARTER OR CreatedDate = NEXT_QUARTER OR CreatedDate = THIS_YEAR OR ' + 1261 | 'CreatedDate = LAST_YEAR OR CreatedDate = NEXT_YEAR OR CreatedDate = THIS_FISCAL_QUARTER OR ' + 1262 | 'CreatedDate = LAST_FISCAL_QUARTER OR CreatedDate = NEXT_FISCAL_QUARTER OR ' + 1263 | 'CreatedDate = THIS_FISCAL_YEAR OR CreatedDate = LAST_FISCAL_YEAR OR CreatedDate = NEXT_FISCAL_YEAR OR ' + 1264 | 'CreatedDate = LAST_N_DAYS:3 OR CreatedDate = NEXT_N_DAYS:3 OR CreatedDate = N_DAYS_AGO:3 OR ' + 1265 | 'CreatedDate = NEXT_N_WEEKS:3 OR CreatedDate = LAST_N_WEEKS:3 OR CreatedDate = N_WEEKS_AGO:3 OR ' + 1266 | 'CreatedDate = NEXT_N_MONTHS:3 OR CreatedDate = LAST_N_MONTHS:3 OR CreatedDate = N_MONTHS_AGO:3 OR ' + 1267 | 'CreatedDate = NEXT_N_QUARTERS:3 OR CreatedDate = LAST_N_QUARTERS:3 OR CreatedDate = N_QUARTERS_AGO:3 OR ' + 1268 | 'CreatedDate = NEXT_N_YEARS:3 OR CreatedDate = LAST_N_YEARS:3 OR CreatedDate = N_YEARS_AGO:3 OR ' + 1269 | 'CreatedDate = NEXT_N_FISCAL_QUARTERS:3 OR CreatedDate = N_FISCAL_QUARTERS_AGO:3 OR ' + 1270 | 'CreatedDate = NEXT_N_FISCAL_YEARS:3 OR CreatedDate = LAST_N_FISCAL_YEARS:3 OR CreatedDate = N_FISCAL_YEARS_AGO:3)', 1271 | accountQuery.buildSOQL() 1272 | ); 1273 | } 1274 | 1275 | @isTest 1276 | static void testLiteral_Currency() { 1277 | Query accountQuery = Query.of('Account').whereBy(eq('AnnualRevenue', CURRENCY('TRY', 100))); 1278 | Assert.areEqual('SELECT Id FROM Account WHERE (AnnualRevenue = TRY100)', accountQuery.buildSOQL()); 1279 | } 1280 | 1281 | // #endregion 1282 | // =============== 1283 | } 1284 | --------------------------------------------------------------------------------