├── 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 |   
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 |   
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 |
--------------------------------------------------------------------------------