├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.txt ├── README.md ├── docs └── introduction.md ├── go.mod ├── go.sum ├── marshaller.go ├── marshaller_test.go └── soql_suite_test.go /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | - Using welcoming and inclusive language 39 | - Being respectful of differing viewpoints and experiences 40 | - Gracefully accepting constructive criticism 41 | - Focusing on what is best for the community 42 | - Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | - The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | - Personal attacks, insulting/derogatory comments, or trolling 49 | - Public or private harassment 50 | - Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | - Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | - Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org "https://www.contributor-covenant.org/" 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | digest = "1:59392ed8afb901aab4287d4894df8191722e34f3957716f4350c8c133ce99046" 7 | name = "github.com/hpcloud/tail" 8 | packages = [ 9 | ".", 10 | "ratelimiter", 11 | "util", 12 | "watch", 13 | "winfile", 14 | ] 15 | pruneopts = "UT" 16 | revision = "a1dbeea552b7c8df4b542c66073e393de198a800" 17 | 18 | [[projects]] 19 | digest = "1:51766ddcba65b7f0eece2906249d7603970959ba0f1011b72037485044339ece" 20 | name = "github.com/onsi/ginkgo" 21 | packages = [ 22 | ".", 23 | "config", 24 | "internal/codelocation", 25 | "internal/containernode", 26 | "internal/failer", 27 | "internal/leafnodes", 28 | "internal/remote", 29 | "internal/spec", 30 | "internal/spec_iterator", 31 | "internal/specrunner", 32 | "internal/suite", 33 | "internal/testingtproxy", 34 | "internal/writer", 35 | "reporters", 36 | "reporters/stenographer", 37 | "reporters/stenographer/support/go-colorable", 38 | "reporters/stenographer/support/go-isatty", 39 | "types", 40 | ] 41 | pruneopts = "UT" 42 | revision = "ce5d301e555bb672c693c099ba6ca5087b06c0b4" 43 | version = "v1.10.3" 44 | 45 | [[projects]] 46 | digest = "1:7e57cd10c5424b2abf91f29354796a2468720396419585fef5a2d346c5a0f24d" 47 | name = "github.com/onsi/gomega" 48 | packages = [ 49 | ".", 50 | "format", 51 | "internal/assertion", 52 | "internal/asyncassertion", 53 | "internal/oraclematcher", 54 | "internal/testingtsupport", 55 | "matchers", 56 | "matchers/support/goraph/bipartitegraph", 57 | "matchers/support/goraph/edge", 58 | "matchers/support/goraph/node", 59 | "matchers/support/goraph/util", 60 | "types", 61 | ] 62 | pruneopts = "UT" 63 | revision = "f9a52764bd5a0fd2d201bcca584351d03b72f8da" 64 | version = "v1.7.1" 65 | 66 | [[projects]] 67 | branch = "master" 68 | digest = "1:e18aa5dcd958515838eca1c07d25e298b8450022c6efd99d04d614b32f0318e3" 69 | name = "golang.org/x/net" 70 | packages = [ 71 | "html", 72 | "html/atom", 73 | "html/charset", 74 | ] 75 | pruneopts = "UT" 76 | revision = "5ee1b9f4859acd2e99987ef94ec7a58427c53bef" 77 | 78 | [[projects]] 79 | branch = "master" 80 | digest = "1:4da420ceda5f68e8d748aa2169d0ed44ffadb1bbd6537cf778a49563104189b8" 81 | name = "golang.org/x/sys" 82 | packages = ["unix"] 83 | pruneopts = "UT" 84 | revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945" 85 | 86 | [[projects]] 87 | digest = "1:8a0baffd5559acaa560f854d7d525c02f4fec2d4f8a214398556fb661a10f6e0" 88 | name = "golang.org/x/text" 89 | packages = [ 90 | "encoding", 91 | "encoding/charmap", 92 | "encoding/htmlindex", 93 | "encoding/internal", 94 | "encoding/internal/identifier", 95 | "encoding/japanese", 96 | "encoding/korean", 97 | "encoding/simplifiedchinese", 98 | "encoding/traditionalchinese", 99 | "encoding/unicode", 100 | "internal/gen", 101 | "internal/language", 102 | "internal/language/compact", 103 | "internal/tag", 104 | "internal/utf8internal", 105 | "language", 106 | "runes", 107 | "transform", 108 | "unicode/cldr", 109 | ] 110 | pruneopts = "UT" 111 | revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" 112 | version = "v0.3.2" 113 | 114 | [[projects]] 115 | digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" 116 | name = "gopkg.in/fsnotify/fsnotify.v1" 117 | packages = ["."] 118 | pruneopts = "UT" 119 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" 120 | version = "v1.4.7" 121 | 122 | [[projects]] 123 | digest = "1:3c839a777de0e6da035c9de900b60cbec463b0a89351192c1ea083eaf9e0fce0" 124 | name = "gopkg.in/tomb.v1" 125 | packages = ["."] 126 | pruneopts = "UT" 127 | revision = "c131134a1947e9afd9cecfe11f4c6dff0732ae58" 128 | 129 | [[projects]] 130 | digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737" 131 | name = "gopkg.in/yaml.v2" 132 | packages = ["."] 133 | pruneopts = "UT" 134 | revision = "1f64d6156d11335c3f22d9330b0ad14fc1e789ce" 135 | version = "v2.2.7" 136 | 137 | [solve-meta] 138 | analyzer-name = "dep" 139 | analyzer-version = 1 140 | input-imports = [ 141 | "github.com/onsi/ginkgo", 142 | "github.com/onsi/gomega", 143 | ] 144 | solver-name = "gps-cdcl" 145 | solver-version = 1 146 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/onsi/ginkgo" 30 | version = "1.10.3" 31 | 32 | [[constraint]] 33 | name = "github.com/onsi/gomega" 34 | version = "1.7.1" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOQL 2 | 3 | This package supports marshalling a golang struct into SOQL. Like `json` tags, this package provides `soql` tags that you can use to annotate your golang structs. Once tagged `Marshal` method will return SOQL query that will let you query the required Salesforce object using Salesforce API. 4 | 5 | ## Introduction 6 | 7 | Please refer to [introduction](./docs/introduction.md) to understand the basics. This blog [post](https://developer.salesforce.com/blogs/2020/02/soql-tags-for-golang.html) also captures basics in little more detail. 8 | 9 | Once you read through it you can refer to documentation below that covers features of this repo in more depth. 10 | 11 | ## How to use 12 | 13 | Start with using `soql` tags on members of your golang structs. `soql` is the main tag. There are following subtags supported: 14 | 15 | ``` 16 | selectClause // is the tag to be used when marking the struct to be considered for select clause in soql. 17 | whereClause // is the tag to be used when marking the struct to be considered for where clause in soql. 18 | orderByClause // is the tag to be used when marking the Order slice to be considered for order by clause in soql. 19 | limitClause // is the tag to be used when marking the *int to be considered for limit clause in soql. 20 | offsetClause // is the tag to be used when marking the *int to be considered for offset clause in soql. 21 | selectColumn // is the tag to be used for selecting a column in select clause. It should be used on members of struct that have been tagged with selectClause. 22 | selectChild // is the tag to be used when selecting from child tables. It should be used on members of struct that have been tagged with selectClause. 23 | likeOperator // is the tag to be used for "like" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 24 | notLikeOperator // is the tag to be used for "not like" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 25 | inOperator // is the tag to be used for "in" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 26 | notInOperator // is the tag to be used for "not in" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 27 | equalsOperator // is the tag to be used for "=" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 28 | notEqualsOperator // is the tag to be used for "!=" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 29 | nullOperator // is the tag to be used for " = null " or "!= null" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 30 | greaterThanOperator // is the tag to be used for ">" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 31 | lessThanOperator // is the tag to be used for "<" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 32 | greaterThanOrEqualsToOperator // is the tag to be used for ">=" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 33 | lessThanOrEqualsToOperator // is the tag to be used for "<=" operator in where clause. It should be used on members of struct that have been tagged with whereClause. 34 | greaterNextNDaysOperator // is the tag to be used for "> NEXT_N_DAYS:n" operator in where clause 35 | greaterOrEqualNextNDaysOperator // is the tag to be used for ">= NEXT_N_DAYS:n" operator in where clause 36 | equalsNextNDaysOperator // is the tag to be used for "= NEXT_N_DAYS:n" operator in where clause 37 | lessNextNDaysOperator // is the tag to be used for "< NEXT_N_DAYS:n" operator in where clause 38 | lessOrEqualNextNDaysOperator // is the tag to be used for "<= NEXT_N_DAYS:n" operator in where clause 39 | greaterLastNDaysOperator // is the tag to be used for "> LAST_N_DAYS:n" operator in where clause 40 | greaterOrEqualLastNDaysOperator // is the tag to be used for ">= LAST_N_DAYS:n" operator in where clause 41 | equalsLastNDaysOperator // is the tag to be used for "= LAST_N_DAYS:n" operator in where clause 42 | lessLastNDaysOperator // is the tag to be used for "< LAST_N_DAYS:n" operator in where clause 43 | lessOrEqualLastNDaysOperator // is the tag to be used for "<= LAST_N_DAYS:n" operator in where clause 44 | ``` 45 | 46 | Following are supported parameters: 47 | 48 | ``` 49 | fieldName // is the parameter to be used to specify the name of the field in underlying Salesforce object. It can be used with all tags listed above other than selectClause and whereClause. 50 | tableName // is the parameter to be used to specify the name of the table of underlying Salesforce Object. It can be be used only with selectClause. 51 | 52 | ``` 53 | 54 | If `fieldName` and `tableName` parameters are not provided then the name of the field will be used as default. 55 | 56 | ### Basic Usage 57 | 58 | Lets take a look at one example of a simple non-nested struct and how it can be used to construct a soql query: 59 | 60 | ``` 61 | type TestSoqlStruct struct { 62 | SelectClause NonNestedStruct `soql:"selectClause,tableName=SM_SomeObject__c"` 63 | WhereClause TestQueryCriteria `soql:"whereClause"` 64 | OrderByClause []Order `soql:"orderByClause"` 65 | LimitClause *int `soql:"limitClause"` 66 | OffsetClause *int `soql:"offsetClause"` 67 | } 68 | type TestQueryCriteria struct { 69 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Name__c"` 70 | Roles []string `soql:"inOperator,fieldName=Role__c"` 71 | } 72 | type NonNestedStruct struct { 73 | Name string `soql:"selectColumn,fieldName=Name__c"` 74 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 75 | } 76 | ``` 77 | 78 | To use above structs to create SOQL query 79 | 80 | ``` 81 | limit := 5 82 | offset := 10 83 | soqlStruct := TestSoqlStruct{ 84 | WhereClause: TestQueryCriteria { 85 | IncludeNamePattern: []string{"foo", "bar"}, 86 | Roles: []string{"admin", "user"}, 87 | }, 88 | OrderByClause: []Order{Order{Field:"Name", IsDesc:true}}, 89 | LimitClause: &limit, 90 | OffsetClause: &offset, 91 | } 92 | soqlQuery, err := Marshal(soqlStruct) 93 | if err != nil { 94 | fmt.Printf("Error in marshaling: %s\n", err.Error()) 95 | } 96 | fmt.Println(soqlQuery) 97 | ``` 98 | 99 | Above struct will result in following SOQL query: 100 | 101 | ``` 102 | SELECT Name__c,SomeValue__c FROM SM_SomeObject__C WHERE (Name__c LIKE '%foo%' OR Name__c LIKE '%bar%') AND Role__c IN ('admin','user') ORDER BY Name__c DESC LIMIT 5 OFFSET 10 103 | ``` 104 | 105 | ### Advanced usage 106 | 107 | #### Relationships 108 | 109 | This package supports child to parent as well as parent to child relationships. Here's a more complex example that includes both the relationships and how the soql query is marshalled: 110 | 111 | ``` 112 | type ComplexSoqlStruct struct { 113 | SelectClause ParentStruct `soql:"selectClause,tableName=SM_Parent__c"` 114 | WhereClause QueryCriteria `soql:"whereClause"` 115 | } 116 | 117 | type QueryCriteria struct { 118 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Name__c"` 119 | Roles []string `soql:"inOperator,fieldName=Role__r.Name"` 120 | ExcludeNamePattern []string `soql:"notLikeOperator,fieldName=Name__c"` 121 | SomeType string `soql:"equalsOperator,fieldName=Some_Parent__r.Some_Type__c"` 122 | Status string `soql:"notEqualsOperator,fieldName=Status__c"` 123 | AllowNullValue *bool `soql:"nullOperator,fieldName=Value__c"` 124 | } 125 | 126 | type ParentStruct struct { 127 | ID string `soql:"selectColumn,fieldName=Id"` 128 | Name string `soql:"selectColumn,fieldName=Name__c"` 129 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` // child to parent relationship 130 | ChildStruct TestChildStruct `soql:"selectChild,fieldName=Child__r"` // parent to child relationship 131 | SomeNonSoqlMember string `json:"some_nonsoql_member"` 132 | } 133 | 134 | type NonNestedStruct struct { 135 | Name string `soql:"selectColumn,fieldName=Name"` 136 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 137 | NonSoqlStruct NonSoqlStruct 138 | } 139 | 140 | type NonSoqlStruct struct { 141 | Key string 142 | Value string 143 | } 144 | 145 | type TestChildStruct struct { 146 | SelectClause ChildStruct `soql:"selectClause,tableName=SM_Child__c"` 147 | WhereClause ChildQueryCriteria `soql:"whereClause"` 148 | } 149 | 150 | type ChildStruct struct { 151 | Version string `soql:"selectColumn,fieldName=Version__c"` 152 | } 153 | 154 | type ChildQueryCriteria struct { 155 | Name string `soql:"equalsOperator,fieldName=Name__c"` 156 | } 157 | 158 | allowNull := false 159 | soqlStruct := ComplexSoqlStruct{ 160 | SelectClause: ParentStruct{ 161 | ChildStruct: TestChildStruct{ 162 | WhereClause: ChildQueryCriteria{ 163 | Name: "some-name", 164 | }, 165 | }, 166 | }, 167 | WhereClause: QueryCriteria{ 168 | SomeType: "typeA", 169 | IncludeNamePattern: []string{"-foo", "-bar"}, 170 | Roles: []string{"admin", "user"}, 171 | ExcludeNamePattern: []string{"-far", "-baz"}, 172 | Status: "InActive", 173 | AllowNullValue: &allowNull, 174 | }, 175 | } 176 | soqlQuery, err := soql.Marshal(soqlStruct) 177 | if err != nil { 178 | fmt.Printf("Error in marshalling: %s\n", err.Error()) 179 | } 180 | fmt.Println(soqlQuery) 181 | ``` 182 | 183 | Above struct will result in following SOQL query: 184 | 185 | ``` 186 | SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c,(SELECT SM_Child__c.Version__c FROM Child__r WHERE SM_Child__c.Name__c = 'some-name') FROM SM_Parent__c WHERE (Name__c LIKE '%-foo%' OR Name__c LIKE '%-bar%') AND Role__r.Name IN ('admin','user') AND ((NOT Name__c LIKE '%-far%') AND (NOT Name__c LIKE '%-baz%')) AND Some_Parent__r.Some_Type__c = 'typeA' AND Status__c != 'InActive' AND Value__c != null 187 | ``` 188 | 189 | You can find detailed usage in `marshaller_test.go`. 190 | 191 | #### Subqueries 192 | 193 | This package supports nested conditions within `WHERE` clauses as well. For example: 194 | 195 | ``` go 196 | 197 | type contact struct { 198 | Name string `soql:"selectColumn,fieldName=Name" json:"Name"` 199 | Email string `soql:"selectColumn,fieldName=Email" json:"Email"` 200 | Phone string `soql:"selectColumn,fieldName=Phone" json:"Phone"` 201 | } 202 | 203 | type soqlQuery struct { 204 | SelectClause contact `soql:"selectClause,tableName=Contact"` 205 | WhereClause queryCriteria `soql:"whereClause"` 206 | } 207 | 208 | type queryCriteria struct { 209 | Position positionCriteria `soql:"subquery,joiner=OR"` 210 | Contactable contactableCriteria `soql:"subquery,joiner=OR"` 211 | AlreadyContacted alreadyContactedSoqlQuery `soql:"subquery,joiner=NOT IN,fieldName=Name"` 212 | } 213 | 214 | type positionCriteria struct { 215 | Title string `soql:"equalsOperator,fieldName=Title"` 216 | DepartmentManager deptManagerCriteria `soql:"subquery"` 217 | } 218 | 219 | type deptManagerCriteria struct { 220 | Department string `soql:"equalsOperator,fieldName=Department"` 221 | Title []string `soql:"likeOperator,fieldName=Title"` 222 | } 223 | 224 | type contactableCriteria struct { 225 | EmailOK emailCheck `soql:"subquery,joiner=and"` 226 | PhoneOK phoneCheck `soql:"subquery,joiner=and"` 227 | } 228 | 229 | type emailCheck struct { 230 | Email bool `soql:"nullOperator,fieldName=Email"` 231 | EmailOptedOut bool `soql:"equalsOperator,fieldName=HasOptedOutOfEmail"` 232 | } 233 | 234 | type phoneCheck struct { 235 | Phone bool `soql:"nullOperator,fieldName=Phone"` 236 | DoNotCall bool `soql:"equalsOperator,fieldName=DoNotCall"` 237 | } 238 | 239 | type alreadyContactedSoqlQuery struct { 240 | SelectClause alreadyContacted `soql:"selectClause,tableName=Calls"` 241 | WhereClause alreadyContactedCriteria `soql:"whereClause"` 242 | } 243 | 244 | type alreadyContacted struct { 245 | Name string `soql:"selectColumn,fieldName=Name"` 246 | } 247 | 248 | type alreadyContactedCriteria struct { 249 | IsContacted bool `soql:"equalsOperator,fieldName=IsContacted"` 250 | } 251 | 252 | soqlStruct := soqlQuery{ 253 | WhereClause: queryCriteria{ 254 | Position: positionCriteria{ 255 | Title: "Purchasing Manager", 256 | DepartmentManager: deptManagerCriteria{ 257 | Department: "Accounting", 258 | Title: []string{"Manager"}, 259 | }, 260 | }, 261 | Contactable: contactableCriteria{ 262 | EmailOK: emailCheck{ 263 | Email: false, 264 | EmailOptedOut: false, 265 | }, 266 | PhoneOK: phoneCheck{ 267 | Phone: false, 268 | DoNotCall: false, 269 | }, 270 | }, 271 | AlreadyContacted: alreadyContactedSoqlQuery{ 272 | WhereClause: alreadyContactedCriteria{ 273 | IsContacted: true, 274 | } 275 | } 276 | }, 277 | } 278 | query, err := soql.Marshal(soqlStruct) 279 | if err != nil { 280 | fmt.Printf("Error in marshalling: %s\n", err.Error()) 281 | } 282 | fmt.Println(soqlQuery) 283 | 284 | ``` 285 | 286 | The above code will generate this SOQL query: 287 | 288 | ``` sql 289 | SELECT Name,Email,Phone 290 | FROM Contact 291 | WHERE (Title = 'Purchasing Manager' OR (Department = 'Accounting' AND Title LIKE '%Manager%')) AND ((Email != null AND HasOptedOutOfEmail = false) OR (Phone != null AND DoNotCall = false)) AND Name NOT IN (SELECT Name FROM Calls WHERE IsContacted = true) 292 | ``` 293 | 294 | #### Advantages 295 | 296 | Intended users of this package are developers writing clients to interact with Salesforce. They can now define golang structs, annotate them and generate SOQL queries to be passed to Salesforce API. Great thing about this is that the json structure of returned response matches with selectClause, so you can just unmarshal response into the golang struct that was annotated with `selectClause` and now you have your query response directly available in golang struct. 297 | 298 | ## Tags explained 299 | 300 | This section explains each of the supported tags in detail 301 | 302 | ### Top level tags 303 | 304 | This section explains top level tags used in constructing SOQL query. Following snippet will be used as example for explaining these tags: 305 | 306 | ``` 307 | type TestSoqlStruct struct { 308 | SelectClause NonNestedStruct `soql:"selectClause,tableName=SM_SomeObject__c"` 309 | WhereClause TestQueryCriteria `soql:"whereClause"` 310 | } 311 | type TestQueryCriteria struct { 312 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Name__c"` 313 | Roles []string `soql:"inOperator,fieldName=Role__c"` 314 | } 315 | type NonNestedStruct struct { 316 | Name string `soql:"selectColumn,fieldName=Name__c"` 317 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 318 | } 319 | ``` 320 | 321 | 1. `selectClause`: This tag is used on the struct which should be considered for generating part of SOQL query that contains columns/fields that should be selected. It should be used only on `struct` type. If used on types other than `struct` then `ErrInvalidTag` error will be returned. This tag is associated with `tableName` parameter. It specifies the name of the table (Salesforce object) from which the columns should be selected. If not specified name of the field is used as table name (Salesforce object). In the snippet above `SelectClause` member of `TestSoqlStruct` is tagged with `selectClause` to indicate that members in `NonNestedStruct` should be considered as fields to be selected from Salesforce object `SM_SomeObject__c`. 322 | 323 | 1. `whereClause`: This tag is used on the struct which encapsulates the query criteria for SOQL query. There is an optional parameter `joiner` for this tag. In the snippet above `WhereClause` member of `TestSoqlStruct` is tagged with `whereClause` to indicate that members in `TestQueryCriteria` should be considered for generating `WHERE` clause in SOQL query. If there are more than one field in `TestQueryCriteria` struct then they will be combined using `AND` logical operator. If the `joiner` parameter is set to `or` (case insensitive), then the fields will be combined using `OR` logical operator. If the `joiner` parameter is set to `and` (case insensitive) or is not set, then the fields will be combined using `AND` logical operator. If any other value is provided, then `ErrInvalidTag` error will be returned. The `joiner` parameter is only supported when using `Marshal`; when calling `MarshalWhereClause`, the fields will always be combined with the `AND` logical operator. 324 | 325 | 1. `orderByClause`: This tag is used on the slice of `Order` to capture the ordering of columns and sort order. There are no parameters for this tag. Clients using this library can expose `Order` struct from this library to their users if they wish to allow users of the client to control ordering of the result. 326 | 327 | 1. `limitClause`: This tag is used on the \*int that describes the limit value for SOQL query. There are no parameters for this tag. Passing `nil` here will omit the `LIMIT` clause from the generated query. Passing a pointer to an integer value less than zero will cause an error. 328 | 329 | 1. `offsetClause`: This tag is used on the \*int that describes the offset value for SOQL query. There are no parameters for this tag. Passing `nil` here will omit the `OFFSET` clause from the generated query. Passing a pointer to an integer value less than zero will cause an error. 330 | 331 | ### Second level tags 332 | 333 | This section explains the tags that should be used on members of struct tagged with `selectClause` and `whereClause`. These tags indicate how the members of the struct should be used in generating `SELECT` and `WHERE` clause. 334 | 335 | #### Tags to be used on selectClause structs 336 | 337 | This section explains the list of tags that can be used on members tagged with `selectClause`. Following snippet will be used explaining these tags: 338 | 339 | ``` 340 | type ParentStruct struct { 341 | ID string `soql:"selectColumn,fieldName=Id"` 342 | Name string `soql:"selectColumn,fieldName=Name__c"` 343 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` // child to parent relationship 344 | ChildStruct TestChildStruct `soql:"selectChild,fieldName=Child__r"` // parent to child relationship 345 | SomeNonSoqlMember string `json:"some_nonsoql_member"` 346 | } 347 | 348 | type NonNestedStruct struct { 349 | Name string `soql:"selectColumn,fieldName=Name"` 350 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 351 | NonSoqlStruct NonSoqlStruct 352 | } 353 | 354 | type TestChildStruct struct { 355 | SelectClause ChildStruct `soql:"selectClause,tableName=SM_Child__c"` 356 | WhereClause ChildQueryCriteria `soql:"whereClause"` 357 | } 358 | ``` 359 | 360 | 1. `selectColumn`: Members that are tagged with this tag will be considered in generating select clause of SOQL query. This tag is associated with `fieldName` parameter. It specifies the name of the field of underlying Salesforce object. If not specified the name of the field is used as underlying Salesforce object field name. This tag can be used on primitive data types as well as user defined structs. If used on user defined structs like `NonNestedStruct` member in `ParentStruct` it will be treated as child to parent relationship and the value specified in `fieldName` parameter (or default value of name of the member itself) will be prefixed to the members of that struct (`NonNestedStruct` in case of our example above). 361 | 1. `selectChild`: This tag is used on members which should be modelled as parent to child relation. It should be used on `struct` type only. If used on any other type then `ErrInvalidTag` error will be returned. The member on which this tag is used should in turn consist of members tagged with `selectClause` and `whereClause`. Please refer to `ChildStruct` member of `ParentStruct`. 362 | 363 | #### Tags to be used on whereClause structs 364 | 365 | This section explains the list of tags that can be used on members tagged with `whereClause`. Following snippet will be used as example for explaining these tags: 366 | 367 | ``` 368 | type QueryCriteria struct { 369 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Name__c"` 370 | ExcludeNamePattern []string `soql:"notLikeOperator,fieldName=Name__c"` 371 | Roles []string `soql:"inOperator,fieldName=Role__r.Name"` 372 | SomeType string `soql:"equalsOperator,fieldName=Some_Type__c"` 373 | SomeBoolType *bool `soql:"equalsOperator,fieldName=Some_Bool_Type__c"` 374 | Status string `soql:"notEqualsOperator,fieldName=Status__c"` 375 | AllowNullValue *bool `soql:"nullOperator,fieldName=Value__c"` 376 | NumOfCPUCores int `soql:"greaterThanOperator,fieldName=Num_of_CPU_Cores__c"` 377 | PhysicalCPUCount uint8 `soql:"greaterThanOrEqualsToOperator,fieldName=Physical_CPU_Count__c"` 378 | AllocationLatency float64 `soql:"lessThanOperator,fieldName=Allocation_Latency__c"` 379 | PvtTestFailCount int64 `soql:"lessThanOrEqualsToOperator,fieldName=Pvt_Test_Fail_Count__c"` 380 | UpdateDate time.Time `soql:"equalsOperator,fieldName=UpdateDate,format=2006-01-02"` 381 | Subquery sub `soql:"subquery"` 382 | } 383 | 384 | type sub struct { 385 | NumOfCPUCores int `soql:"lessThanOperator,fieldName=Num_of_CPU_Cores__c"` 386 | PhysicalCPUCount uint8 `soql:"lessThanOrEqualsToOperator,fieldName=Physical_CPU_Count__c"` 387 | } 388 | ``` 389 | 390 | 1. `likeOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `LIKE` comparison operator. This tag should be used on member of type `[]string`. Used on any other type, `ErrInvalidTag` error will be returned. If there are more than one item in the slice then they will be combined using `OR` logical operator. Example will clarify this more: 391 | 392 | ``` 393 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 394 | IncludeNamePattern: []string{"-foo", "-bar"}, 395 | }) 396 | // whereClause will be: WHERE (Name__c LIKE '%-foo%' OR Name__c LIKE '%-bar%') 397 | ``` 398 | 399 | 1. `notLikeOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `NOT LIKE` comparison operator. This tag should be used on member of type `[]string`. Used on any other type, `ErrInvalidTag` error will be returned. If there are more than one item in the slice then they will be combined using `AND` logical operator. Example will clarify this more: 400 | 401 | ``` 402 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 403 | ExcludeNamePattern: []string{"-far", "-baz"}, 404 | }) 405 | // whereClause will be: WHERE ((NOT Name__c LIKE '%-far%') AND (NOT Name__c LIKE '%-baz%')) 406 | ``` 407 | 408 | 1. `inOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `IN` comparison operator. This tag should be used on member of type `[]string`, `[]int`, `[]int8`, `[]int16`, `[]int32`, `[]int64`, `[]uint`, `[]uint8`, `[]uint16`, `[]uint32`, `[]uint64`, `[]float32`, `[]float64`, `[]bool` or `[]time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 409 | 410 | ``` 411 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 412 | Roles: []string{"admin", "user"}, 413 | }) 414 | // whereClause will be: WHERE Role__r.Name IN ('admin','user') 415 | ``` 416 | 417 | 1. `notInOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `NOT IN` comparison operator. This tag should be used on member of type `[]string`, `[]int`, `[]int8`, `[]int16`, `[]int32`, `[]int64`, `[]uint`, `[]uint8`, `[]uint16`, `[]uint32`, `[]uint64`, `[]float32`, `[]float64`, `[]bool` or `[]time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 418 | 419 | ``` 420 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 421 | ExcludeIDs: []string{"123", "456"}, 422 | }) 423 | // whereClause will be: WHERE Id NOT IN ('123','456') 424 | ``` 425 | 426 | 1. `equalsOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `=` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 427 | 428 | ``` 429 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 430 | SomeType: "SomeValue", 431 | }) 432 | // whereClause will be: WHERE Some_Type__c = 'SomeValue' 433 | ``` 434 | 435 | If pointers are used as data type then the field will be included in WHERE clause only if the variable is initialized. So in case below it will be included because the variable is initialized unlike example above. 436 | 437 | ``` 438 | b := true 439 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 440 | SomeType: "SomeValue", 441 | SomeBoolType: &b, 442 | }) 443 | // whereClause will be: WHERE Some_Type__c = 'SomeValue' AND Some_Bool_Type__c = true 444 | ``` 445 | 446 | 1. `notEqualsOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `!=` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 447 | 448 | ``` 449 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 450 | Status: "DOWN", 451 | }) 452 | // whereClause will be: WHERE Status__c != 'DOWN' 453 | ``` 454 | 455 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 456 | 457 | 1. `nullOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `= null` or `!= null` comparison operator. This tag should be used on member of type `*bool` or `bool`. Used on any other type, `ErrInvalidTag` error will be returned. Recommended to use `*bool` as `bool` will always be initialized by golang to `false` and will result in `!= null` check even if not intended. Example will clarify this more: 458 | 459 | ``` 460 | allowNull := true 461 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 462 | AllowNullValue: &allowNull, 463 | }) 464 | // whereClause will be: WHERE Value__c = null 465 | allowNull = false 466 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 467 | AllowNullValue: &allowNull, 468 | }) 469 | // whereClause will be: WHERE Value__c != null 470 | ``` 471 | 472 | 1. `greaterThanOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `>` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 473 | 474 | ``` 475 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 476 | NumOfCPUCores: 8, 477 | }) 478 | // whereClause will be: WHERE Num_of_CPU_Cores__c > 8 479 | ``` 480 | 481 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 482 | 483 | 1. `greaterThanOrEqualsToOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `>=` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 484 | 485 | ``` 486 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 487 | PhysicalCPUCount: 4, 488 | }) 489 | // whereClause will be: WHERE Physical_CPU_Count__c >= 4 490 | ``` 491 | 492 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 493 | 494 | 1. `lessThanOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `<` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 495 | 496 | ``` 497 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 498 | AllocationLatency: 28.9, 499 | }) 500 | // whereClause will be: WHERE Allocation_Latency__c < 28.9 501 | ``` 502 | 503 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 504 | 505 | 1. `lessThanOrEqualsToOperator`: This tag is used on members which should be considered to construct field expressions in where clause using `<=` comparison operator. This tag should be used on member of type `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `bool`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`, `*float32`, `*float64`, `*bool` or `time.Time`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 506 | 507 | ``` 508 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 509 | PvtTestFailCount: 32, 510 | }) 511 | // whereClause will be: WHERE Pvt_Test_Fail_Count__c <= 32 512 | ``` 513 | 514 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 515 | 516 | 1. `greaterNextNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `>` comparison operator and `NEXT_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 517 | 518 | ``` 519 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 520 | CreatedDate: 5, 521 | }) 522 | // whereClause will be: WHERE CreatedDate > NEXT_N_DAYS:5 523 | ``` 524 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 525 | 526 | 1. `greaterOrEqualNextNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `>=` comparison operator and `NEXT_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 527 | 528 | ``` 529 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 530 | CreatedDate: 5, 531 | }) 532 | // whereClause will be: WHERE CreatedDate >= NEXT_N_DAYS:5 533 | ``` 534 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 535 | 536 | 1. `equalsNextNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `=` comparison operator and `NEXT_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 537 | 538 | ``` 539 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 540 | CreatedDate: 5, 541 | }) 542 | // whereClause will be: WHERE CreatedDate = NEXT_N_DAYS:5 543 | ``` 544 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 545 | 546 | 1. `lessNextNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `<` comparison operator and `NEXT_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 547 | 548 | ``` 549 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 550 | CreatedDate: 5, 551 | }) 552 | // whereClause will be: WHERE CreatedDate < NEXT_N_DAYS:5 553 | ``` 554 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 555 | 556 | 1. `lessOrEqualNextNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `<=` comparison operator and `NEXT_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 557 | 558 | ``` 559 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 560 | CreatedDate: 5, 561 | }) 562 | // whereClause will be: WHERE CreatedDate <= NEXT_N_DAYS:5 563 | ``` 564 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 565 | 566 | 1. `greaterLastNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `>` comparison operator and `LAST_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 567 | 568 | ``` 569 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 570 | CreatedDate: 5, 571 | }) 572 | // whereClause will be: WHERE CreatedDate > LAST_N_DAYS:5 573 | ``` 574 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 575 | 576 | 1. `greaterOrEqualLastNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `>=` comparison operator and `LAST_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 577 | 578 | ``` 579 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 580 | CreatedDate: 5, 581 | }) 582 | // whereClause will be: WHERE CreatedDate >= LAST_N_DAYS:5 583 | ``` 584 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 585 | 586 | 1. `equalsLastNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `=` comparison operator and `LAST_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 587 | 588 | ``` 589 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 590 | CreatedDate: 5, 591 | }) 592 | // whereClause will be: WHERE CreatedDate = LAST_N_DAYS:5 593 | ``` 594 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 595 | 596 | 1. `lessLastNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `<` comparison operator and `LAST_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 597 | 598 | ``` 599 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 600 | CreatedDate: 5, 601 | }) 602 | // whereClause will be: WHERE CreatedDate < LAST_N_DAYS:5 603 | ``` 604 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 605 | 606 | 1. `lessOrEqualLastNDaysOperator`: This tag is used on members which should be considered to field expressions in where clause using `<=` comparison operator and `LAST_N_DAYS` date literal. This tag should be used on member of type `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `*int`, `*int8`, `*int16`, `*int32`, `*int64`, `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64`. Used on any other type, `ErrInvalidTag` error will be returned. Example will clarify this more: 607 | 608 | ``` 609 | whereClause, _ := MarshalWhereClause(QueryCriteria{ 610 | CreatedDate: 5, 611 | }) 612 | // whereClause will be: WHERE CreatedDate <= LAST_N_DAYS:5 613 | ``` 614 | Fields that are pointers will only be included if they are initialized else they will be skipped from WHERE clause. 615 | 616 | 617 | If there are more than one fields in the struct tagged with `whereClause` then they will be combined using `AND` logical operator. This has been demonstrated in the code snippets in [Advanced usage](#advanced-usage). 618 | 619 | 1. `subquery`: This tag is used on members which should be used to construct related sets of conditions wrapped in `()` in the query. This tag should only be used on members of type `struct`. Used on any other type, `ErrInvalidTag` error will be returned. Any of the above property tags (including `subquery`) may be used in the designated `struct`. 620 | 621 | The following tag can be included for modifying the marshalling behavior: 622 | 623 | 1. `format`: This tag can be included to specify the formatting of `time.Time` and `*time.Time` fields when marshalled to a 624 | query. If omitted, the default format "2006-01-02T15:04:05.000-0700" is used. A format of "2006-01-02" can be used when 625 | forming a soql query against a Date type. 626 | 627 | ``` 628 | whereClause, _ := MarshalWehereClause(QueryCriteria{ 629 | UpdateDate: time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC), 630 | }) 631 | // whereClause will be: WHERE UpdateDate = 2009-11-17 632 | ``` 633 | 634 | #### The Order struct and orderByClause 635 | 636 | This section explains the Order struct to be used for the `orderByClause`. 637 | 638 | ``` 639 | type Order struct { 640 | Field string 641 | IsDesc bool 642 | } 643 | ``` 644 | 645 | The `Order` struct is part of this library and has two fields, `Field` which is of type string and `IsDesc` of type bool. Each struct represents a column from the select column list that should be included in the `ORDER BY` clause, as well as the sort order on that column. The value of `Field` should be the name of the struct field with the `selectColumn` tag, and set `IsDesc` to `true` to specify the sort order on that column as `DESC` (set it to `false` for `ASC`). Create a slice of `Order` structs and tag it with the `orderByClause` soql tag to define the `ORDER BY` clause. Using the following `NestedStruct` as an example of the `selectClause`: 646 | 647 | ``` 648 | type NonNestedStruct struct { 649 | Name string `soql:"selectColumn,fieldName=Name"` 650 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 651 | NonSoqlStruct NonSoqlStruct 652 | } 653 | 654 | type NestedStruct struct { 655 | ID string `soql:"selectColumn,fieldName=Id"` 656 | Name string `soql:"selectColumn,fieldName=Name__c"` 657 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 658 | } 659 | ``` 660 | 661 | To order the query results using the `Name__c` field of the NestedStruct in `ASC` order and `SomeValue__c` of the `NonNestedStruct` field in `DESC` order, the following `Order` slice should be used: 662 | 663 | ``` 664 | order := []Order{Order{Field:"Name",IsDesc:false},Order{Field:"NonNestedStruct.SomeValue",IsDesc:true}} 665 | ``` 666 | 667 | To specify fields in nested structs, use the `.` dot notation. 668 | 669 | The final soql query struct would look like: 670 | 671 | ``` 672 | type TestSoqlStruct struct { 673 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_SomeObject__c"` 674 | OrderByClause []Order `soql:"orderByClause"` 675 | } 676 | ``` 677 | 678 | ## License 679 | 680 | go-soql is BSD3 licensed. Here is the link to license [file](./LICENSE.txt) 681 | 682 | ## Contributing 683 | 684 | You are welcome to contribute to this repo. Please create PR and send it for review. Please follow code of conduct as documented [here](./CODE_OF_CONDUCT.md) 685 | 686 | If you have a question, comment, bug report, feature request, etc. please open a GitHub issue. 687 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | If you are a Golang developer intending to write client for interacting with Salesforce, this doc is for you. Learn how you can annotate your Golang structs to generate SOQL queries to be used in Salesforce APIs and how this will make it easy for you to write Golang client for Salesforce. 4 | 5 | Salesforce has REST [API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest_resources.htm) that allows any third party to integrate with it using their language of choice. One of the APIs (/services/data//query) allows developers to query Salesforce object using [SOQL](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm) query. SOQL is very powerful, SQL like query language that allows developers to query Salesforce objects. It has extensive support for conditional expression to filter the objects being retrieved as well as very powerful way of performing join operations using relationship queries. 6 | 7 | Golang is gaining popularity in recent years and is now very widely adopted languages in enterprises. As more and more developers embrace Golang it is important to provide easier means and ways to interact with Salesforce APIs. One such step is to provide soql annotation library that will allow Golang developers to tag their structs very similar to how they tag their structs for JSON marshaling/unmarshaling. And that is the aim of new [go-soql](https://github.com/forcedotcom/go-soql) library. It allows Golang developers to tag their structs and then marshal it into SOQL query that can be used in the query API of Salesforce. Lets take a look at how it works. 8 | 9 | You will start by defining the Golang struct representing the Salesforce object(s) you want to query. Lets say the Salesforce object is Account and it has following fields (subset of fields shown here from the actual [list](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_account.htm)): 10 | 11 | | FieldName | Type | 12 | | ------------------ | -------- | 13 | | Name | string | 14 | | AccountNumber | picklist | 15 | | AccountSource | string | 16 | | BillingAddress | string | 17 | | HasOptedOutOfEmail | boolean | 18 | | LastActivityDate | Date | 19 | | NumberOfEmployees | int | 20 | 21 | Golang struct representation for it looks as follows: 22 | 23 | ``` 24 | type Account struct { 25 | Name string 26 | AccountNumber string 27 | AccountSource string 28 | BillingAddress string 29 | HasOptedOutOfEmail bool 30 | LastActivityDate time.Time 31 | NumberOfEmployees int 32 | } 33 | ``` 34 | 35 | Now if you want to query all Account where AccountSource is one of ‘Advertisement’ or Data.com’ then the SOQL query will look as follows: 36 | 37 | ``` 38 | 39 | SELECT Name,AccountNumber,AccountSource,BillingAddress,HasOptedOutOfEmail,LastActivityDate,NumberOfEmployees FROM Account WHERE AccountSource IN ('Advertisement', 'Data.com') 40 | 41 | ``` 42 | 43 | Now wouldn’t it be great if instead of hardcoding this query we can generate it from our Account struct directly. Not only it will help us automatically change SOQL query based on addition/removal of attributes from our model but also it will help us directly unmarshal the response from Salesforce into the struct! Lets see how we can achieve it. We start by annotating our Account struct as follows: 44 | 45 | ``` 46 | type Account struct { 47 | Name string `soql:"selectColumn,fieldName=Name" json:"Name"` 48 | AccountNumber string `soql:"selectColumn,fieldName=AccountNumber" json:"AccountNumber"` 49 | AccountSource string `soql:"selectColumn,fieldName=AccountSource" json:"AccountSource"` 50 | BillingAddress string `soql:"selectColumn,fieldName=BillingAddress" json:"BillingAddress"` 51 | HasOptedOutOfEmail bool `soql:"selectColumn,fieldName=HasOptedOutOfEmail" json:"HasOptedOutOfEmail"` 52 | LastActivityDate time.Time `soql:"selectColumn,fieldName=LastActivityDate" json:"LastActivityDate"` 53 | NumberOfEmployees int `soql:"selectColumn,fieldName=NumberOfEmployees" json:"NumberOfEmployees"` 54 | } 55 | ``` 56 | 57 | If we just want to generate the select clause without conditional expression then it can be done as follows: 58 | 59 | ``` 60 | soqlQuery, err := soql.MarshalSelectClause(Account{}, "") 61 | ``` 62 | 63 | And this will result in following 64 | 65 | ``` 66 | 67 | Name,AccountNumber,AccountSource,BillingAddress,HasOptedOutOfEmail,LastActivityDate,NumberOfEmployees 68 | 69 | ``` 70 | 71 | This is, of course, of limited use. We want full SOQL query to be automatically generated. This needs us to define few more structs to model this: 72 | 73 | ``` 74 | type AccountQueryCriteria struct { 75 | AccountSource []string `soql:"inOperator,fieldName=AccountSource"` 76 | } 77 | 78 | type AccountSoqlQuery struct { 79 | SelectClause Account `soql:"selectClause,tableName=Account"` 80 | WhereClause AccountQueryCriteria `soql:"whereClause"` 81 | } 82 | ``` 83 | 84 | Now we can generate complete SOQL query using the above two structs: 85 | 86 | ``` 87 | soqlStruct := AccountSoqlQuery{ 88 | SelectClause: Account{}, 89 | WhereClause: AccountQueryCriteria{ 90 | AccountSource: []string{"Advertisement", "Data.com"}, 91 | }, 92 | } 93 | soqlQuery, err := soql.Marshal(soqlStruct) 94 | ``` 95 | 96 | And viola! This will generate SOQL query that we expect. Now we can use that in our call to /services/data//query API. Note that you can add/remove/change the AccountQueryCriteria struct to add/remove/change the conditional expression. If you add more than one field to AccountQueryCriteria then they are combined using AND logical operator. What is cool is that the returned JSON data from Salesforce can be directly unmarshalled into Account struct. Here’s the struct that represents response from query API 97 | 98 | ``` 99 | type QueryResponse struct { 100 | Done bool `json:"done"` 101 | NextRecordsURL string `json:"nextRecordsUrl"` 102 | Accounts []Account `json:"records"` 103 | TotalSize int `json:"totalSize"` 104 | } 105 | ``` 106 | 107 | Your client code will look something like this 108 | 109 | ``` 110 | values := url.Values{} 111 | values.Set("q", soqlQuery) // soqlQuery variable defined in code snippet above 112 | path := fmt.Sprintf("/services/data/v44.0/query?%s",values.Encode()) 113 | serverURL := "https://.salesforce.com" 114 | req, err := http.NewRequest(http.MethodGet, serverURL+path, nil) 115 | if err != nil { 116 | // Handle error case 117 | } 118 | req.Header.Add("Authorization", "Bearer ") 119 | req.Header.Add("Content-Type", "application/json") 120 | httpClient := &http.Client{} 121 | resp, err := httpClient.Do(req) 122 | if err != nil { 123 | // Handle error case 124 | } 125 | payload, err := ioutil.ReadAll(resp.Body) 126 | if err != nil { 127 | // Handle error case 128 | } 129 | var queryResponse QueryResponse 130 | err = json.Unmarshal(payload, &queryResponse) 131 | if err != nil { 132 | // Handle error case 133 | } 134 | ``` 135 | 136 | As you can note above using this writing Golang client for Salesforce can now be much more easy affair with new soql tag library. All you need to do is define the struct and annotate it. 137 | 138 | Hopefully now you have a feel of what go-soql library can do for you. However, this doc just scratches the surface of what go-sosql library can do. It has very extensive support for logical operators as well as child to parent and parent to child relationships. More details on how to use that can be found out in README of the repo. 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/forcedotcom/go-soql 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/onsi/ginkgo v1.10.3 7 | github.com/onsi/gomega v1.7.1 8 | golang.org/x/text v0.3.8 // indirect 9 | gopkg.in/yaml.v2 v2.2.7 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 2 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 3 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 4 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 6 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 7 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 8 | github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= 9 | github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 10 | github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= 11 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 12 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 14 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 15 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 16 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 17 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 18 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 19 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= 20 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 21 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 24 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 37 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 38 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 39 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 40 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 41 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 42 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 46 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 47 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 48 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 49 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 51 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 52 | -------------------------------------------------------------------------------- /marshaller.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package soql 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "reflect" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | const ( 19 | openBrace = "(" 20 | closeBrace = ")" 21 | orCondition = " OR " 22 | andCondition = " AND " 23 | singleQuote = "'" 24 | safeSingleQuote = "\\'" 25 | doubleQuote = "\"" 26 | safeDoubleQuote = "\\\"" 27 | backslash = "\\" 28 | safeBackslash = "\\\\" 29 | newLine = "\n" 30 | safeNewLine = "\\n" 31 | carriageReturn = "\r" 32 | safeCarriageReturn = "\\r" 33 | tab = "\t" 34 | safeTab = "\\t" 35 | bell = "\b" 36 | safeBell = "\\b" 37 | formFeed = "\f" 38 | safeFormFeed = "\\f" 39 | underscore = "_" 40 | safeUnderscore = "\\_" 41 | percentSign = "%" 42 | safePercentSign = "\\%" 43 | comma = "," 44 | notOperator = "NOT " 45 | openLike = " LIKE '%" 46 | closeLike = "%'" 47 | inOperator = " IN " 48 | notInOperator = " NOT IN " 49 | equalsOperator = " = " 50 | period = "." 51 | null = "null" 52 | notEqualsOperator = " != " 53 | greaterThanOperator = " > " 54 | greaterThanOrEqualsToOperator = " >= " 55 | lessThanOperator = " < " 56 | lessThanOrEqualsToOperator = " <= " 57 | greaterNextNDaysOperator = " > NEXT_N_DAYS:" 58 | greaterOrEqualNextNDaysOperator = " >= NEXT_N_DAYS:" 59 | equalsNextNDaysOperator = " = NEXT_N_DAYS:" 60 | lessNextNDaysOperator = " < NEXT_N_DAYS:" 61 | lessOrEqualNextNDaysOperator = " <= NEXT_N_DAYS:" 62 | greaterLastNDaysOperator = " > LAST_N_DAYS:" 63 | greaterOrEqualLastNDaysOperator = " >= LAST_N_DAYS:" 64 | equalsLastNDaysOperator = " = LAST_N_DAYS:" 65 | lessLastNDaysOperator = " < LAST_N_DAYS:" 66 | lessOrEqualLastNDaysOperator = " <= LAST_N_DAYS:" 67 | selectKeyword = "SELECT " 68 | whereKeyword = " WHERE " 69 | fromKeyword = " FROM " 70 | orderByKeyword = " ORDER BY " 71 | limitKeyword = " LIMIT " 72 | offsetKeyword = " OFFSET " 73 | ascKeyword = " ASC" 74 | descKeyword = " DESC" 75 | 76 | // DateTimeFormat is the golang reference time in the soql dateTime fields format 77 | DateTimeFormat = "2006-01-02T15:04:05.000-0700" 78 | 79 | // SoqlTag is the main tag name to be used to mark a struct field to be considered for soql marshaling 80 | SoqlTag = "soql" 81 | // SelectClause is the tag to be used when marking the struct to be considered for select clause 82 | SelectClause = "selectClause" 83 | // TableName is the parameter to be used to specify the name of the underlying SOQL table. It should be used 84 | // with SelectClause 85 | TableName = "tableName" 86 | // SelectColumn is the tag to be used for selecting a column in select clause 87 | SelectColumn = "selectColumn" 88 | // SelectChild is the tag to be used when selecting from child tables 89 | SelectChild = "selectChild" 90 | // FieldName is the parameter to be used to specify the name of the field in underlying SOQL object 91 | FieldName = "fieldName" 92 | // WhereClause is the tag to be used when marking the struct to be considered for where clause 93 | WhereClause = "whereClause" 94 | // Joiner is the parameter to be used to specify the joiner to use between properties within a where clause 95 | Joiner = "joiner" 96 | // Format is the parameter to be used to specify string formatting, it is only valid for time.Time fields 97 | Format = "format" 98 | // OrderByClause is the tag to be used when marking the string slice to be considered for order by clause 99 | OrderByClause = "orderByClause" 100 | // LimitClause is the tag to be used when marking the int to be considered for limit clause 101 | LimitClause = "limitClause" 102 | // OffsetClause is the tag to be used when marking the int to be considered for offset clause 103 | OffsetClause = "offsetClause" 104 | // LikeOperator is the tag to be used for "like" operator in where clause 105 | LikeOperator = "likeOperator" 106 | // NotLikeOperator is the tag to be used for "not like" operator in where clause 107 | NotLikeOperator = "notLikeOperator" 108 | // InOperator is the tag to be used for "in" operator in where clause 109 | InOperator = "inOperator" 110 | // NotInOperator is the tag to be used for "not in" operator in where clause 111 | NotInOperator = "notInOperator" 112 | // EqualsOperator is the tag to be used for "=" operator in where clause 113 | EqualsOperator = "equalsOperator" 114 | // NotEqualsOperator is the tag to be used for "!=" operator in where clause 115 | NotEqualsOperator = "notEqualsOperator" 116 | // NullOperator is the tag to be used for " = null " or "!= null" operator in where clause 117 | NullOperator = "nullOperator" 118 | // GreaterThanOperator is the tag to be used for ">" operator in where clause 119 | GreaterThanOperator = "greaterThanOperator" 120 | // GreaterThanOrEqualsToOperator is the tag to be used for ">=" operator in where clause 121 | GreaterThanOrEqualsToOperator = "greaterThanOrEqualsToOperator" 122 | // LessThanOperator is the tag to be used for "<" operator in where clause 123 | LessThanOperator = "lessThanOperator" 124 | // LessThanOrEqualsToOperator is the tag to be used for "<=" operator in where clause 125 | LessThanOrEqualsToOperator = "lessThanOrEqualsToOperator" 126 | // GreaterNextNDaysOperator is the tag to be used for "> NEXT_N_DAYS:n" operator in where clause 127 | GreaterNextNDaysOperator = "greaterNextNDaysOperator" 128 | // GreaterOrEqualNextNDaysOperator is the tag to be used for ">= NEXT_N_DAYS:n" operator in where clause 129 | GreaterOrEqualNextNDaysOperator = "greaterOrEqualNextNDaysOperator" 130 | // EqualsNextNDaysOperator is the tag to be used for "= NEXT_N_DAYS:n" operator in where clause 131 | EqualsNextNDaysOperator = "equalsNextNDaysOperator" 132 | // LessNextNDaysOperator is the tag to be used for "< NEXT_N_DAYS:n" operator in where clause 133 | LessNextNDaysOperator = "lessNextNDaysOperator" 134 | // LessOrEqualNextNDaysOperator is the tag to be used for "<= NEXT_N_DAYS:n" operator in where clause 135 | LessOrEqualNextNDaysOperator = "lessOrEqualNextNDaysOperator" 136 | // GreaterLastNDaysOperator is the tag to be used for "> LAST_N_DAYS:n" operator in where clause 137 | GreaterLastNDaysOperator = "greaterLastNDaysOperator" 138 | // GreaterOrEqualLastNDaysOperator is the tag to be used for ">= LAST_N_DAYS:n" operator in where clause 139 | GreaterOrEqualLastNDaysOperator = "greaterOrEqualLastNDaysOperator" 140 | // EqualsLastNDaysOperator is the tag to be used for "= LAST_N_DAYS:n" operator in where clause 141 | EqualsLastNDaysOperator = "equalsLastNDaysOperator" 142 | // LessLastNDaysOperator is the tag to be used for "< LAST_N_DAYS:n" operator in where clause 143 | LessLastNDaysOperator = "lessLastNDaysOperator" 144 | // LessOrEqualLastNDaysOperator is the tag to be used for "<= LAST_N_DAYS:n" operator in where clause 145 | LessOrEqualLastNDaysOperator = "lessOrEqualLastNDaysOperator" 146 | 147 | // Subquery is the tag to be used for a subquery in a where clause 148 | Subquery = "subquery" 149 | ) 150 | 151 | var clauseBuilderMap = map[string]func(v interface{}, fieldName string, tags map[string]string) (string, error){ 152 | LikeOperator: buildLikeClause, 153 | NotLikeOperator: buildNotLikeClause, 154 | InOperator: buildInClause, 155 | NotInOperator: buildNotInClause, 156 | EqualsOperator: buildEqualsClause, 157 | NullOperator: buildNullClause, 158 | NotEqualsOperator: buildNotEqualsClause, 159 | GreaterThanOperator: buildGreaterThanClause, 160 | GreaterThanOrEqualsToOperator: buildGreaterThanOrEqualsToClause, 161 | LessThanOperator: buildLessThanClause, 162 | LessThanOrEqualsToOperator: buildLessThanOrEqualsToClause, 163 | GreaterNextNDaysOperator: buildGreaterNextNDaysOperator, 164 | GreaterOrEqualNextNDaysOperator: buildGreaterOrEqualNextNDaysOperator, 165 | EqualsNextNDaysOperator: buildEqualsNextNDaysOperator, 166 | LessNextNDaysOperator: buildLessNextNDaysOperator, 167 | LessOrEqualNextNDaysOperator: buildLessOrEqualNextNDaysOperator, 168 | GreaterLastNDaysOperator: buildGreaterLastNDaysOperator, 169 | GreaterOrEqualLastNDaysOperator: buildGreaterOrEqualLastNDaysOperator, 170 | EqualsLastNDaysOperator: buildEqualsLastNDaysOperator, 171 | LessLastNDaysOperator: buildLessLastNDaysOperator, 172 | LessOrEqualLastNDaysOperator: buildLessOrEqualLastNDaysOperator, 173 | } 174 | 175 | var ( 176 | // ErrInvalidTag error is returned when invalid key is used in soql tag 177 | ErrInvalidTag = errors.New("ErrInvalidTag") 178 | 179 | // ErrNilValue error is returned when nil pointer is passed as argument 180 | ErrNilValue = errors.New("ErrNilValue") 181 | 182 | // ErrMultipleSelectClause error is returned when there are multiple selectClause in struct 183 | ErrMultipleSelectClause = errors.New("ErrMultipleSelectClause") 184 | 185 | // ErrNoSelectClause error is returned when there are No selectClause in struct 186 | ErrNoSelectClause = errors.New("ErrNoSelectClause") 187 | 188 | // ErrMultipleWhereClause error is returned when there are multiple whereClause in struct 189 | ErrMultipleWhereClause = errors.New("ErrMultipleWhereClause") 190 | 191 | // ErrInvalidOrderByClause error is returned when field with orderByClause tag is invalid 192 | ErrInvalidOrderByClause = errors.New("ErrInvalidOrderByClause") 193 | 194 | // ErrMultipleOrderByClause error is returned when there are multiple orderByClause in struct 195 | ErrMultipleOrderByClause = errors.New("ErrMultipleOrderByClause") 196 | 197 | // ErrInvalidSelectColumnOrderByClause error is returned when the selectColumn 198 | // associated with the order by clause is invalid 199 | ErrInvalidSelectColumnOrderByClause = errors.New("ErrInvalidSelectColumnOrderByClause") 200 | 201 | // ErrInvalidLimitClause error is returned when field with limitClause tag is invalid 202 | ErrInvalidLimitClause = errors.New("ErrInvalidLimitClause") 203 | 204 | // ErrMultipleLimitClause error is returned when there are multiple limitClause in struct 205 | ErrMultipleLimitClause = errors.New("ErrMultipleLimitClause") 206 | 207 | // ErrInvalidOffsetClause error is returned when field with offsetClause tag is invalid 208 | ErrInvalidOffsetClause = errors.New("ErrInvalidOffsetClause") 209 | 210 | // ErrMultipleOffsetClause error is returned when there are multiple offsetClause in struct 211 | ErrMultipleOffsetClause = errors.New("ErrMultipleOffsetClause") 212 | ) 213 | 214 | // Order is the struct for defining the order by clause on a per column basis 215 | // A slice of this struct tagged with the orderByClause tag in a soql struct 216 | // specifies the columns from the selectClause struct to be included in the 217 | // order by clause and their sort order 218 | type Order struct { 219 | // Field contains the name of the field of the selectClause struct to be 220 | // included in the order by clause 221 | Field string 222 | // IsDesc indicates whether the ordering is DESC (true) or ASC (false) 223 | IsDesc bool 224 | } 225 | 226 | // https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_quotedstringescapes.htm 227 | var sanitizeCharacters = []string{ 228 | singleQuote, safeSingleQuote, 229 | doubleQuote, safeDoubleQuote, 230 | backslash, safeBackslash, 231 | newLine, safeNewLine, 232 | carriageReturn, safeCarriageReturn, 233 | tab, safeTab, 234 | bell, safeBell, 235 | formFeed, safeFormFeed, 236 | } 237 | 238 | var sanitizeReplacer = strings.NewReplacer(sanitizeCharacters...) 239 | 240 | var sanitizeLikeCharacters = append( 241 | sanitizeCharacters, 242 | underscore, safeUnderscore, 243 | percentSign, safePercentSign, 244 | ) 245 | 246 | var sanitizeLikeReplacer = strings.NewReplacer(sanitizeLikeCharacters...) 247 | 248 | func buildLikeClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 249 | return constructLikeClause(v, fieldName, false) 250 | } 251 | 252 | func buildNotLikeClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 253 | return constructLikeClause(v, fieldName, true) 254 | } 255 | 256 | func constructLikeClause(v interface{}, fieldName string, exclude bool) (string, error) { 257 | var buff strings.Builder 258 | patterns, ok := v.([]string) 259 | if !ok { 260 | return buff.String(), ErrInvalidTag 261 | } 262 | if len(patterns) > 1 { 263 | buff.WriteString(openBrace) 264 | } 265 | for indx, pattern := range patterns { 266 | if indx > 0 { 267 | if exclude { 268 | buff.WriteString(andCondition) 269 | } else { 270 | buff.WriteString(orCondition) 271 | } 272 | } 273 | if exclude { 274 | buff.WriteString(openBrace) 275 | buff.WriteString(notOperator) 276 | } 277 | buff.WriteString(fieldName) 278 | buff.WriteString(openLike) 279 | buff.WriteString(sanitizeLikeReplacer.Replace(pattern)) 280 | buff.WriteString(closeLike) 281 | if exclude { 282 | buff.WriteString(closeBrace) 283 | } 284 | } 285 | if len(patterns) > 1 { 286 | buff.WriteString(closeBrace) 287 | } 288 | return buff.String(), nil 289 | } 290 | 291 | func buildInClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 292 | return constructContainsClause(v, fieldName, inOperator, tags) 293 | } 294 | 295 | func buildNotInClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 296 | return constructContainsClause(v, fieldName, notInOperator, tags) 297 | } 298 | 299 | func constructContainsClause(v interface{}, fieldName string, operator string, tags map[string]string) (string, error) { 300 | var buff strings.Builder 301 | var items []string 302 | useSingleQuotes := false 303 | 304 | switch u := v.(type) { 305 | case []string: 306 | useSingleQuotes = true 307 | items = u 308 | case []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []float32, []float64, []bool: 309 | items = strings.Fields(strings.Trim(fmt.Sprint(u), "[]")) 310 | case []time.Time: 311 | for _, item := range u { 312 | items = append(items, item.Format(getDateFormat(tags))) 313 | } 314 | default: 315 | return buff.String(), ErrInvalidTag 316 | } 317 | 318 | if len(items) > 0 { 319 | buff.WriteString(fieldName) 320 | buff.WriteString(operator) 321 | buff.WriteString(openBrace) 322 | } 323 | for indx, item := range items { 324 | if indx > 0 { 325 | buff.WriteString(comma) 326 | } 327 | if useSingleQuotes { 328 | buff.WriteString(singleQuote) 329 | buff.WriteString(sanitizeReplacer.Replace(item)) 330 | buff.WriteString(singleQuote) 331 | } else { 332 | buff.WriteString(item) 333 | } 334 | } 335 | if len(items) > 0 { 336 | buff.WriteString(closeBrace) 337 | } 338 | return buff.String(), nil 339 | } 340 | 341 | func buildNotEqualsClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 342 | return constructComparisonClause(v, fieldName, notEqualsOperator, tags) 343 | } 344 | 345 | func buildEqualsClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 346 | return constructComparisonClause(v, fieldName, equalsOperator, tags) 347 | } 348 | 349 | func buildGreaterThanClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 350 | return constructComparisonClause(v, fieldName, greaterThanOperator, tags) 351 | } 352 | 353 | func buildGreaterThanOrEqualsToClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 354 | return constructComparisonClause(v, fieldName, greaterThanOrEqualsToOperator, tags) 355 | } 356 | 357 | func buildLessThanClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 358 | return constructComparisonClause(v, fieldName, lessThanOperator, tags) 359 | } 360 | 361 | func buildLessThanOrEqualsToClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 362 | return constructComparisonClause(v, fieldName, lessThanOrEqualsToOperator, tags) 363 | } 364 | 365 | func constructComparisonClause(v interface{}, fieldName, operator string, tags map[string]string) (string, error) { 366 | var buff strings.Builder 367 | var value string 368 | useSingleQuotes := false 369 | 370 | switch u := v.(type) { 371 | case string: 372 | useSingleQuotes = true 373 | value = u 374 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: 375 | value = fmt.Sprint(u) 376 | case time.Time: 377 | value = u.Format(getDateFormat(tags)) 378 | case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64, *float32, *float64, *bool: 379 | if !reflect.ValueOf(u).IsNil() { 380 | value = fmt.Sprint(reflect.Indirect(reflect.ValueOf(u))) 381 | } 382 | case *time.Time: 383 | if !reflect.ValueOf(u).IsNil() { 384 | value = reflect.Indirect(reflect.ValueOf(u)).Interface().(time.Time).Format(getDateFormat(tags)) 385 | } 386 | default: 387 | return buff.String(), ErrInvalidTag 388 | } 389 | 390 | if value != "" { 391 | buff.WriteString(fieldName) 392 | buff.WriteString(operator) 393 | if useSingleQuotes { 394 | buff.WriteString(singleQuote) 395 | buff.WriteString(sanitizeReplacer.Replace(value)) 396 | buff.WriteString(singleQuote) 397 | } else { 398 | buff.WriteString(value) 399 | } 400 | } 401 | return buff.String(), nil 402 | } 403 | 404 | func buildGreaterNextNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 405 | return constructDateLiteralsClause(v, fieldName, greaterNextNDaysOperator) 406 | } 407 | 408 | func buildGreaterOrEqualNextNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 409 | return constructDateLiteralsClause(v, fieldName, greaterOrEqualNextNDaysOperator) 410 | } 411 | 412 | func buildEqualsNextNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 413 | return constructDateLiteralsClause(v, fieldName, equalsNextNDaysOperator) 414 | } 415 | 416 | func buildLessNextNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 417 | return constructDateLiteralsClause(v, fieldName, lessNextNDaysOperator) 418 | } 419 | 420 | func buildLessOrEqualNextNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 421 | return constructComparisonClause(v, fieldName, lessOrEqualNextNDaysOperator, tags) 422 | } 423 | 424 | func buildGreaterLastNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 425 | return constructDateLiteralsClause(v, fieldName, greaterLastNDaysOperator) 426 | } 427 | 428 | func buildGreaterOrEqualLastNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 429 | return constructDateLiteralsClause(v, fieldName, greaterOrEqualLastNDaysOperator) 430 | } 431 | 432 | func buildEqualsLastNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 433 | return constructDateLiteralsClause(v, fieldName, equalsLastNDaysOperator) 434 | } 435 | 436 | func buildLessLastNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 437 | return constructDateLiteralsClause(v, fieldName, lessLastNDaysOperator) 438 | } 439 | 440 | func buildLessOrEqualLastNDaysOperator(v interface{}, fieldName string, tags map[string]string) (string, error) { 441 | return constructDateLiteralsClause(v, fieldName, lessOrEqualLastNDaysOperator) 442 | } 443 | 444 | func constructDateLiteralsClause(v interface{}, fieldName string, operator string) (string, error) { 445 | var buff strings.Builder 446 | var value string 447 | 448 | switch u := v.(type) { 449 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 450 | value = fmt.Sprint(u) 451 | case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: 452 | if !reflect.ValueOf(u).IsNil() { 453 | value = fmt.Sprint(reflect.Indirect(reflect.ValueOf(u))) 454 | } 455 | default: 456 | return buff.String(), ErrInvalidTag 457 | } 458 | 459 | if value != "" { 460 | buff.WriteString(fieldName) 461 | buff.WriteString(operator) 462 | buff.WriteString(value) 463 | } 464 | return buff.String(), nil 465 | } 466 | 467 | func buildNullClause(v interface{}, fieldName string, tags map[string]string) (string, error) { 468 | reflectedValue, _, err := getReflectedValueAndType(v) 469 | if err == ErrNilValue { 470 | // Not an error case because nil value for *bool is valid 471 | return "", nil 472 | } 473 | val := reflectedValue.Interface() 474 | allowNull, ok := val.(bool) 475 | if !ok { 476 | return "", ErrInvalidTag 477 | } 478 | if allowNull { 479 | return fieldName + equalsOperator + null, nil 480 | } 481 | return fieldName + notEqualsOperator + null, nil 482 | } 483 | 484 | func getReflectedValueAndType(v interface{}) (reflect.Value, reflect.Type, error) { 485 | var reflectedValue reflect.Value 486 | var reflectedType reflect.Type 487 | if reflect.ValueOf(v).Kind() == reflect.Ptr { 488 | if reflect.ValueOf(v).IsNil() { 489 | return reflect.Value{}, nil, ErrNilValue 490 | } 491 | reflectedValue = reflect.Indirect(reflect.ValueOf(v)) 492 | } else { 493 | reflectedValue = reflect.ValueOf(v) 494 | } 495 | reflectedType = reflectedValue.Type() 496 | return reflectedValue, reflectedType, nil 497 | } 498 | 499 | // mapSelectColumns maps the selectColumn field name in the soql tag to their 500 | // corresponding field name in the struct needed by marshalOrderByClause 501 | func mapSelectColumns(mappings map[string]string, parent string, gusParent string, v interface{}) error { 502 | reflectedValue, reflectedType, err := getReflectedValueAndType(v) 503 | if err != nil { 504 | return ErrInvalidSelectColumnOrderByClause 505 | } 506 | 507 | for i := 0; i < reflectedType.NumField(); i++ { 508 | field := reflectedType.Field(i) 509 | tag := field.Tag.Get(SoqlTag) 510 | if tag == "" { 511 | continue 512 | } 513 | // skip all fields that are not tagged as selectColumn 514 | if getClauseKey(tag) != SelectColumn { 515 | continue 516 | } 517 | 518 | fieldName := field.Name 519 | gusFieldName := getFieldName(tag, field.Name) 520 | 521 | // inside a nested struct, prepend parent to create full field names 522 | if parent != "" { 523 | fieldName = parent + period + fieldName 524 | gusFieldName = gusParent + period + gusFieldName 525 | } 526 | 527 | fieldValue := reflectedValue.Field(i) 528 | 529 | // the mapping for a struct field should be added regardless, to cover 530 | // the case of a struct field not being a nested field (e.g. time.Time) 531 | mappings[fieldName] = gusFieldName 532 | if fieldValue.Kind() == reflect.Struct { 533 | err := mapSelectColumns(mappings, fieldName, gusFieldName, fieldValue.Interface()) 534 | if err != nil { 535 | return err 536 | } 537 | } 538 | } 539 | 540 | return nil 541 | } 542 | 543 | // v is the Order slice for specifying the columns and sort order 544 | // s is the struct value containing fields with the selectColumn tag 545 | func marshalOrderByClause(v interface{}, tableName string, s interface{}) (string, error) { 546 | reflectedValue, reflectedType, err := getReflectedValueAndType(v) 547 | if err != nil { 548 | return "", err 549 | } 550 | 551 | if reflectedType.Kind() != reflect.Slice { 552 | return "", ErrInvalidOrderByClause 553 | } 554 | 555 | if reflectedType.Elem() != reflect.TypeOf(Order{}) { 556 | return "", ErrInvalidOrderByClause 557 | } 558 | 559 | sReflectedValue, sReflectedType, err := getReflectedValueAndType(s) 560 | if err != nil { 561 | return "", err 562 | } 563 | 564 | if sReflectedType.Kind() != reflect.Struct { 565 | return "", ErrInvalidSelectColumnOrderByClause 566 | } 567 | 568 | columnMappings := make(map[string]string) 569 | 570 | err = mapSelectColumns(columnMappings, "", "", sReflectedValue.Interface()) 571 | if err != nil { 572 | return "", err 573 | } 574 | 575 | if len(columnMappings) == 0 { 576 | return "", ErrInvalidSelectColumnOrderByClause 577 | } 578 | 579 | var buff strings.Builder 580 | 581 | previousConditionExists := false 582 | for i := 0; i < reflectedValue.Len(); i++ { 583 | order := reflectedValue.Index(i).Interface().(Order) 584 | fieldName := order.Field 585 | if strings.TrimSpace(fieldName) == "" { 586 | return "", ErrInvalidOrderByClause 587 | } 588 | 589 | columnName, ok := columnMappings[fieldName] 590 | if !ok { 591 | return "", ErrInvalidOrderByClause 592 | } 593 | 594 | if tableName != "" { 595 | columnName = tableName + period + columnName 596 | } 597 | orderString := ascKeyword 598 | if order.IsDesc { 599 | orderString = descKeyword 600 | } 601 | partialClause := columnName + orderString 602 | if previousConditionExists { 603 | buff.WriteString(comma) 604 | } 605 | buff.WriteString(partialClause) 606 | previousConditionExists = true 607 | } 608 | return buff.String(), nil 609 | 610 | } 611 | 612 | // v is the limit value provided 613 | func marshalLimitClause(v interface{}) (string, error) { 614 | s, err := marshalIntValue(v) 615 | if err != nil { 616 | return "", ErrInvalidLimitClause 617 | } 618 | return s, nil 619 | } 620 | 621 | // v is the offset value provided 622 | func marshalOffsetClause(v interface{}) (string, error) { 623 | s, err := marshalIntValue(v) 624 | if err != nil { 625 | return "", ErrInvalidOffsetClause 626 | } 627 | return s, nil 628 | } 629 | 630 | func marshalIntValue(v interface{}) (string, error) { 631 | vPtr, ok := v.(*int) 632 | if !ok { 633 | return "", errors.New("invalid type") 634 | } 635 | if vPtr == nil { 636 | return "", nil 637 | } 638 | 639 | vInt := *vPtr 640 | if vInt < 0 { 641 | return "", errors.New("invalid value") 642 | } 643 | 644 | vString := strconv.Itoa(vInt) 645 | 646 | return vString, nil 647 | } 648 | 649 | // MarshalOrderByClause returns a string representing the SOQL order by clause. 650 | // Parameter v is a slice of the Order struct indicating the fields from 651 | // parameter s, which is the value of the select column struct, that should be 652 | // included in the clause and their respective ordering (i.e. ASC or DESC). 653 | // Consider the following struct containing the fields with selectColumn tags: 654 | // type SelectColumns struct { 655 | // HostName string `soql:"selectColumn,fieldName=Host_Name__c"` 656 | // RoleName string `soql:"selectColumn,fieldName=Role__r.Name"` 657 | // NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 658 | // } 659 | // s := SelectColumns{} 660 | // And with an Order slice as follows: 661 | // o := []Order{ Order{Field: "HostName", IsDesc: true}, 662 | // Order{Field: "NumOfCPUCores", IsDesc: false} } 663 | // By calling MarshalOrderByClause() like the following: 664 | // orderByClause, err := MarshalOrderByClause(o, s) 665 | // if err != nil { 666 | // log.Warn("Error in marshaling order by clause") 667 | // } 668 | // fmt.Println(orderByClause) 669 | // This will print the orderByClause as: 670 | // Host_Name__c DESC,Num_of_CPU_Cores__c ASC 671 | // For nested structs, specify the field name in the Order slice using a 672 | // . notation 673 | // For example, 674 | func MarshalOrderByClause(v interface{}, s interface{}) (string, error) { 675 | return marshalOrderByClause(v, "", s) 676 | } 677 | 678 | func marshalWhereClause(v interface{}, tableName, joiner string) (string, error) { 679 | var buff strings.Builder 680 | reflectedValue, reflectedType, err := getReflectedValueAndType(v) 681 | if err != nil { 682 | return "", err 683 | } 684 | previousConditionExists := false 685 | for i := 0; i < reflectedValue.NumField(); i++ { 686 | field := reflectedValue.Field(i) 687 | fieldType := reflectedType.Field(i) 688 | clauseTag := fieldType.Tag.Get(SoqlTag) 689 | if clauseTag == "" { 690 | continue 691 | } 692 | clauseKey := getClauseKey(clauseTag) 693 | var partialClause string 694 | if clauseKey == Subquery { 695 | if field.Kind() != reflect.Struct && field.Kind() != reflect.Ptr { 696 | return "", ErrInvalidTag 697 | } 698 | if field.Kind() == reflect.Ptr { 699 | if reflect.ValueOf(field.Interface()).IsNil() { 700 | continue 701 | } 702 | } 703 | joiner, err := getJoiner(clauseTag) 704 | if err != nil { 705 | return "", err 706 | } 707 | if joiner == inOperator || joiner == notInOperator { 708 | fieldName := getFieldName(clauseTag, "") 709 | if fieldName == "" { 710 | return "", ErrInvalidTag 711 | } 712 | partialJoinQuery, err := Marshal(field.Interface()) 713 | if err != nil { 714 | return "", err 715 | } 716 | 717 | var queryBuff strings.Builder 718 | queryBuff.WriteString(fieldName) 719 | queryBuff.WriteString(joiner) 720 | queryBuff.WriteString(openBrace) 721 | queryBuff.WriteString(partialJoinQuery) 722 | queryBuff.WriteString(closeBrace) 723 | partialClause = queryBuff.String() 724 | } else { 725 | partialClause, err = marshalWhereClause(field.Interface(), tableName, joiner) 726 | if err != nil { 727 | return "", err 728 | } 729 | partialClause = openBrace + partialClause + closeBrace 730 | } 731 | } else { 732 | fieldName := getFieldName(clauseTag, fieldType.Name) 733 | if fieldName == "" { 734 | return "", ErrInvalidTag 735 | } 736 | fn, ok := clauseBuilderMap[clauseKey] 737 | if !ok { 738 | return "", ErrInvalidTag 739 | } 740 | columnName := fieldName 741 | if tableName != "" { 742 | columnName = tableName + period + fieldName 743 | } 744 | partialClause, err = fn(field.Interface(), columnName, getTagParameterMap(clauseTag)) 745 | if err != nil { 746 | return "", err 747 | } 748 | } 749 | if partialClause != "" { 750 | if previousConditionExists { 751 | buff.WriteString(joiner) 752 | } 753 | buff.WriteString(partialClause) 754 | previousConditionExists = true 755 | } 756 | } 757 | return buff.String(), nil 758 | } 759 | 760 | // MarshalWhereClause returns the string with all conditions that applies for SOQL where clause. 761 | // As part of soql tag, you will need to specify the operator (one of the operators listed below) and 762 | // then specify the name of the field using fieldName parameter. 763 | // Following operators are currently supported: 764 | // 1. LIKE: Like operator. E.g. Host_Name__c LIKE '%-db%'. Use likeOperator in as soql tag 765 | // 2. NOT LIKE: Not like operator. E.g. (NOT Host_Name__c LIKE '%-db%'). Use notLikeOperator in soql tag 766 | // 3. EQUALS (=): Equals operator. E.g. Asset_Type_Asset_Type__c = 'SERVER'. Use equalsOperator in soql tag 767 | // 4. IN: In operator. E.g. Role__r.Name IN ('db','dbmgmt'). Use inOperator in soql tag 768 | // 5. NULL ( = null ): Null operator. E.g. Last_Discovered_Date__c = null. Use nullOperator in soql tag 769 | // 6. NOT NULL: Not null operator. E.g. Last_Discovered_Date__c != null. Use nullOperator in soql tag 770 | // 7. GREATER THAN: Greater than operator. E.g. Last_Discovered_Date__c > 2006-01-02T15:04:05.000-0700. Use greaterThanOperator in soql tag 771 | // 8. GREATER THAN OR EQUALS TO: Greater than or equals to operator. E.g. Num_of_CPU_Cores__c >= 16. Use greaterThanOrEqualsToOperator in soql tag 772 | // 9. LESS THAN: Less than operator. E.g. Last_Discovered_Date__c < 2006-01-02T15:04:05.000-0700. Use lessThanOperator in soql tag 773 | // 10. LESS THAN OR EQUALS TO: Less than or equals to operator. E.g. Num_of_CPU_Cores__c <= 16. Use lessThanOrEqualsToOperator in soql tag 774 | // Consider following go struct 775 | // type TestQueryCriteria struct { 776 | // IncludeNamePattern []string `soql:"likeOperator,fieldName=Host_Name__c"` 777 | // Roles []string `soql:"inOperator,fieldName=Role__r.Name"` 778 | // ExcludeNamePattern []string `soql:"notLikeOperator,fieldName=Host_Name__c"` 779 | // AssetType string `soql:"equalsOperator,fieldName=Tech_Asset__r.Asset_Type_Asset_Type__c"` 780 | // AllowNullLastDiscoveredDate *bool `soql:"nullOperator,fieldName=Last_Discovered_Date__c"` 781 | // NumOfCPUCores int `soql:"greaterThanOperator,fieldName=Num_of_CPU_Cores__c"` 782 | // } 783 | // allowNull := false 784 | // t := TestQueryCriteria{ 785 | // AssetType: "SERVER", 786 | // IncludeNamePattern: []string{"-db", "-dbmgmt"}, 787 | // Roles: []string{"db", "dbmgmt"}, 788 | // ExcludeNamePattern: []string{"-core", "-drp"}, 789 | // AllowNullLastDiscoveredDate: &allowNull, 790 | // NumOfCPUCores: 16, 791 | // } 792 | // whereClause, err := MarshalWhereClause(t) 793 | // if err != nil { 794 | // log.Warn("Error in marshaling where clause") 795 | // } 796 | // fmt.Println(whereClause) 797 | // This will print whereClause as: 798 | // (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') AND ((NOT Host_Name__c LIKE '%-core%') AND (NOT Host_Name__c LIKE '%-drp%')) AND Tech_Asset__r.Asset_Type_Asset_Type__c = 'SERVER' AND Last_Discovered_Date__c != null AND Num_of_CPU_Cores__c > 16 799 | func MarshalWhereClause(v interface{}) (string, error) { 800 | return marshalWhereClause(v, "", andCondition) 801 | } 802 | 803 | func getClauseKey(clauseTag string) string { 804 | tagItems := strings.Split(clauseTag, ",") 805 | return tagItems[0] 806 | } 807 | 808 | func getJoiner(clauseTag string) (string, error) { 809 | tag := getTagValue(clauseTag, Joiner, "") 810 | switch strings.ToLower(tag) { 811 | case "or": 812 | return orCondition, nil 813 | case "in": 814 | return inOperator, nil 815 | case "not in": 816 | return notInOperator, nil 817 | case "and", "": 818 | return andCondition, nil 819 | default: 820 | return "", ErrInvalidTag 821 | } 822 | } 823 | 824 | func parseTagString(tagString string) (string, string) { 825 | delimInd := strings.Index(tagString, "=") 826 | if delimInd == -1 { 827 | return tagString, "" 828 | } 829 | return tagString[:delimInd], tagString[delimInd+1:] 830 | } 831 | 832 | func getTagParameterMap(clauseTag string) map[string]string { 833 | tagMap := make(map[string]string) 834 | tagItems := strings.Split(clauseTag, ",") 835 | for _, tagItem := range tagItems { 836 | tagKey, tagValue := parseTagString(tagItem) 837 | if tagKey == "" { 838 | continue 839 | } 840 | tagMap[tagKey] = tagValue 841 | } 842 | return tagMap 843 | } 844 | 845 | func getDateFormat(tags map[string]string) string { 846 | if customFormat, ok := tags[Format]; ok { 847 | return customFormat 848 | } 849 | return DateTimeFormat 850 | } 851 | 852 | func getTagValue(clauseTag, key, defaultValue string) string { 853 | tagItems := strings.Split(clauseTag, ",") 854 | for _, tagItem := range tagItems { 855 | tagKey, tagValue := parseTagString(tagItem) 856 | if tagKey == key { 857 | return tagValue 858 | } 859 | } 860 | return defaultValue 861 | } 862 | 863 | func getFieldName(clauseTag, defaultFieldName string) string { 864 | return getTagValue(clauseTag, FieldName, defaultFieldName) 865 | } 866 | 867 | func getTableName(clauseTag, defaultTableName string) string { 868 | return getTagValue(clauseTag, TableName, defaultTableName) 869 | } 870 | 871 | // MarshalSelectClause returns fields to be included in select clause. Child to parent and parent to child 872 | // relationship is also supported. 873 | // Using selectColumn and fieldName in soql tag lets you specify that the field should be included as part of 874 | // select clause. It lets you specify the name of the field as it is named in SOQL object. 875 | // type NonNestedStruct struct { 876 | // Name string `soql:"selectColumn,fieldName=Name"` 877 | // SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 878 | // } 879 | // str, err := MarshalSelectClause(NonNestedStruct{}, "") 880 | // if err != nil { 881 | // log.Warn("Error in marshaling select clause") 882 | // } 883 | // fmt.Println(str) 884 | // This will print selectClause as: 885 | // Name,SomeValue__c 886 | // 887 | // Second argument to this function is the relationship name, typically used for parent relationships. 888 | // So call to this function with relatonship name will result in marshaling as follows: 889 | // str, err := MarshalSelectClause(NonNestedStruct{}, "NonNestedStruct__r") 890 | // if err != nil { 891 | // log.Warn("Error in marshaling select clause") 892 | // } 893 | // fmt.Println(str) 894 | // This will print selectClause as: 895 | // NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c 896 | // 897 | // You can specify parent relationships as nested structs and specify the relationship name as field name. 898 | // type NestedStruct struct { 899 | // ID string `soql:"selectColumn,fieldName=Id"` 900 | // Name string `soql:"selectColumn,fieldName=Name__c"` 901 | // NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 902 | // } 903 | // str, err := MarshalSelectClause(NestedStruct{}, "") 904 | // if err != nil { 905 | // log.Warn("Error in marshaling select clause") 906 | // } 907 | // fmt.Println(str) 908 | // This will print selectClause as: 909 | // Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c 910 | // 911 | // To specify child relationships in struct you will need to use selectChild tag as follows: 912 | // type ParentStruct struct { 913 | // ID string `soql:"selectColumn,fieldName=Id"` 914 | // Name string `soql:"selectColumn,fieldName=Name__c"` 915 | // ChildStruct TestChildStruct `soql:"selectChild,fieldName=Application_Versions__r"` 916 | // } 917 | // type TestChildStruct struct { 918 | // SelectClause ChildStruct `soql:"selectClause,tableName=SM_Application_Versions__c"` 919 | // } 920 | // type ChildStruct struct { 921 | // Version string `soql:"selectColumn,fieldName=Version__c"` 922 | // } 923 | // str, err := MarshalSelectClause(ParentStruct{}, "") 924 | // if err != nil { 925 | // log.Warn("Error in marshaling select clause") 926 | // } 927 | // fmt.Println(str) 928 | // This will print selectClause as: 929 | // Id,Name__c,(SELCT SM_Application_Versions__c.Version__c FROM Application_Versions__r) 930 | func MarshalSelectClause(v interface{}, relationShipName string) (string, error) { 931 | var buff strings.Builder 932 | prefix := relationShipName 933 | if prefix != "" { 934 | prefix += period 935 | } 936 | val, t, err := getReflectedValueAndType(v) 937 | if err != nil { 938 | return "", err 939 | } 940 | if t.Kind() == reflect.Struct { 941 | totalFields := t.NumField() 942 | for i := 0; i < totalFields; i++ { 943 | field := t.Field(i) 944 | clauseTag := field.Tag.Get(SoqlTag) 945 | if clauseTag == "" { 946 | continue 947 | } 948 | clauseKey := getClauseKey(clauseTag) 949 | isChildRelation := false 950 | switch clauseKey { 951 | case SelectColumn: 952 | isChildRelation = false 953 | case SelectChild: 954 | isChildRelation = true 955 | default: 956 | return "", ErrInvalidTag 957 | } 958 | fieldName := getFieldName(clauseTag, field.Name) 959 | if fieldName == "" { 960 | return "", ErrInvalidTag 961 | } 962 | if isChildRelation { 963 | subStr, err := marshal(val.Field(i), field.Type, prefix+fieldName) 964 | if err != nil { 965 | return "", err 966 | } 967 | buff.WriteString(subStr) 968 | } else { 969 | if field.Type.Kind() == reflect.Struct { 970 | v := reflect.New(field.Type) 971 | subStr, err := MarshalSelectClause(v.Elem().Interface(), prefix+fieldName) 972 | if err != nil { 973 | return "", err 974 | } 975 | buff.WriteString(subStr) 976 | } else { 977 | buff.WriteString(prefix) 978 | buff.WriteString(fieldName) 979 | } 980 | } 981 | buff.WriteString(comma) 982 | } 983 | } else { 984 | return "", ErrInvalidTag 985 | } 986 | return strings.TrimRight(buff.String(), comma), nil 987 | } 988 | 989 | func marshal(reflectedValue reflect.Value, reflectedType reflect.Type, childRelationName string) (string, error) { 990 | var buff strings.Builder 991 | if reflectedType.Kind() == reflect.Struct { 992 | totalFields := reflectedType.NumField() 993 | if totalFields == 0 { 994 | // Empty struct 995 | return "", nil 996 | } 997 | soqlTagPresent := false 998 | selectClausePresent := false 999 | whereClausePresent := false 1000 | orderByClausePresent := false 1001 | limitClausePresent := false 1002 | offsetClausePresent := false 1003 | var selectSubString strings.Builder 1004 | var selectValue interface{} 1005 | var whereValue interface{} 1006 | var whereJoiner string 1007 | var orderByValue interface{} 1008 | var limitValue interface{} 1009 | var offsetValue interface{} 1010 | tableName := "" 1011 | for i := 0; i < totalFields; i++ { 1012 | field := reflectedType.Field(i) 1013 | clauseTag := field.Tag.Get(SoqlTag) 1014 | if clauseTag == "" { 1015 | continue 1016 | } 1017 | soqlTagPresent = true 1018 | clauseKey := getClauseKey(clauseTag) 1019 | switch clauseKey { 1020 | case SelectClause: 1021 | if selectClausePresent { 1022 | return "", ErrMultipleSelectClause 1023 | } 1024 | selectClausePresent = true 1025 | selectValue = reflectedValue.Field(i).Interface() 1026 | tableName = getTableName(clauseTag, field.Name) 1027 | var relationName string 1028 | if childRelationName == "" { 1029 | relationName = "" 1030 | } else { 1031 | // This is child struct and we should use tableName as prefix for columns in select clause 1032 | relationName = tableName 1033 | } 1034 | subStr, err := MarshalSelectClause(reflectedValue.Field(i).Interface(), relationName) 1035 | if err != nil { 1036 | return "", err 1037 | } 1038 | selectSubString.WriteString(selectKeyword) 1039 | selectSubString.WriteString(subStr) 1040 | selectSubString.WriteString(fromKeyword) 1041 | if childRelationName == "" { 1042 | // This is not a child struct and we should use table name as FROM 1043 | selectSubString.WriteString(tableName) 1044 | } else { 1045 | // This is child struct and we should use relationship name as FROM 1046 | selectSubString.WriteString(childRelationName) 1047 | } 1048 | case WhereClause: 1049 | if whereClausePresent { 1050 | return "", ErrMultipleWhereClause 1051 | } 1052 | whereClausePresent = true 1053 | whereValue = reflectedValue.Field(i).Interface() 1054 | var err error 1055 | whereJoiner, err = getJoiner(clauseTag) 1056 | if err != nil { 1057 | return "", err 1058 | } 1059 | case OrderByClause: 1060 | if orderByClausePresent { 1061 | return "", ErrMultipleOrderByClause 1062 | } 1063 | orderByValue = reflectedValue.Field(i).Interface() 1064 | orderByClausePresent = true 1065 | case LimitClause: 1066 | if limitClausePresent { 1067 | return "", ErrMultipleLimitClause 1068 | } 1069 | limitValue = reflectedValue.Field(i).Interface() 1070 | limitClausePresent = true 1071 | case OffsetClause: 1072 | if offsetClausePresent { 1073 | return "", ErrMultipleOffsetClause 1074 | } 1075 | offsetValue = reflectedValue.Field(i).Interface() 1076 | offsetClausePresent = true 1077 | default: 1078 | return "", ErrInvalidTag 1079 | } 1080 | } 1081 | if !selectClausePresent && soqlTagPresent { 1082 | return "", ErrNoSelectClause 1083 | } 1084 | if childRelationName != "" { 1085 | buff.WriteString(openBrace) 1086 | } 1087 | buff.WriteString(selectSubString.String()) 1088 | if whereClausePresent { 1089 | relationName := "" 1090 | if childRelationName != "" { 1091 | // This is child struct and we should use tableName as prefix for columns in where clause 1092 | relationName = tableName 1093 | } 1094 | subStr, err := marshalWhereClause(whereValue, relationName, whereJoiner) 1095 | if err != nil { 1096 | return "", err 1097 | } 1098 | if subStr != "" { 1099 | buff.WriteString(whereKeyword) 1100 | buff.WriteString(subStr) 1101 | } 1102 | } 1103 | if orderByClausePresent { 1104 | relationName := "" 1105 | if childRelationName != "" { 1106 | // This is child struct and we should use tableName as prefix for columns in where clause 1107 | relationName = tableName 1108 | } 1109 | subStr, err := marshalOrderByClause(orderByValue, relationName, selectValue) 1110 | if err != nil { 1111 | return "", err 1112 | } 1113 | if subStr != "" { 1114 | buff.WriteString(orderByKeyword) 1115 | buff.WriteString(subStr) 1116 | } 1117 | } 1118 | if limitClausePresent { 1119 | subStr, err := marshalLimitClause(limitValue) 1120 | if err != nil { 1121 | return "", err 1122 | } 1123 | if subStr != "" { 1124 | buff.WriteString(limitKeyword) 1125 | buff.WriteString(subStr) 1126 | } 1127 | } 1128 | if offsetClausePresent { 1129 | subStr, err := marshalOffsetClause(offsetValue) 1130 | if err != nil { 1131 | return "", err 1132 | } 1133 | if subStr != "" { 1134 | buff.WriteString(offsetKeyword) 1135 | buff.WriteString(subStr) 1136 | } 1137 | } 1138 | if childRelationName != "" { 1139 | buff.WriteString(closeBrace) 1140 | } 1141 | } else if childRelationName != "" { 1142 | // Child relationship used for non struct member 1143 | return "", ErrInvalidTag 1144 | } 1145 | return buff.String(), nil 1146 | } 1147 | 1148 | // Marshal constructs the entire SOQL query based on the golang struct passed to it. 1149 | // Consider following example: 1150 | // type TestSoqlStruct struct { 1151 | // SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 1152 | // WhereClause TestQueryCriteria `soql:"whereClause"` 1153 | // } 1154 | // type TestQueryCriteria struct { 1155 | // IncludeNamePattern []string `soql:"likeOperator,fieldName=Host_Name__c"` 1156 | // Roles []string `soql:"inOperator,fieldName=Role__r.Name"` 1157 | // } 1158 | // type NonSoqlStruct struct { 1159 | // Key string 1160 | // Value string 1161 | // } 1162 | // type NonNestedStruct struct { 1163 | // Name string `soql:"selectColumn,fieldName=Name"` 1164 | // SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 1165 | // NonSoqlStruct NonSoqlStruct 1166 | // } 1167 | // type NestedStruct struct { 1168 | // ID string `soql:"selectColumn,fieldName=Id"` 1169 | // Name string `soql:"selectColumn,fieldName=Name__c"` 1170 | // NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 1171 | // } 1172 | // soqlStruct := TestSoqlStruct { 1173 | // SelectClause: NestedStruct{}, 1174 | // WhereClause: TestQueryCriteria{ 1175 | // IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1176 | // Roles: []string{"db"}, 1177 | // } 1178 | // } 1179 | // str, err := Marshal(soqlStruct) 1180 | // if err != nil { 1181 | // log.Warn("Error in marshaling soql") 1182 | // } 1183 | // fmt.Println(str) 1184 | // This will print soql query as: 1185 | // SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') 1186 | func Marshal(v interface{}) (string, error) { 1187 | rv, rt, err := getReflectedValueAndType(v) 1188 | if err != nil { 1189 | return "", err 1190 | } 1191 | return marshal(rv, rt, "") 1192 | } 1193 | -------------------------------------------------------------------------------- /marshaller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package soql_test 8 | 9 | import ( 10 | "time" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | 15 | . "github.com/forcedotcom/go-soql" 16 | ) 17 | 18 | var _ = Describe("Marshaller", func() { 19 | Describe("MarshalWhereClause", func() { 20 | var ( 21 | clause string 22 | expectedClause string 23 | err error 24 | ) 25 | Context("when non pointer value is passed as argument", func() { 26 | var ( 27 | critetria TestQueryCriteria 28 | ) 29 | 30 | JustBeforeEach(func() { 31 | clause, err = MarshalWhereClause(critetria) 32 | Expect(err).ToNot(HaveOccurred()) 33 | }) 34 | 35 | Context("when there are no fields populated", func() { 36 | It("returns empty where clause", func() { 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(clause).To(BeEmpty()) 39 | }) 40 | }) 41 | 42 | Context("when only like clause pattern is populated", func() { 43 | Context("when there is only one item in the like clause array", func() { 44 | BeforeEach(func() { 45 | critetria = TestQueryCriteria{ 46 | IncludeNamePattern: []string{"-db"}, 47 | } 48 | expectedClause = "Host_Name__c LIKE '%-db%'" 49 | }) 50 | 51 | It("returns where clause with only one condition", func() { 52 | Expect(clause).To(Equal(expectedClause)) 53 | }) 54 | }) 55 | 56 | Context("when there is more than one item in the like clause array", func() { 57 | BeforeEach(func() { 58 | critetria = TestQueryCriteria{ 59 | IncludeNamePattern: []string{"-db", "-dbmgmt", "-dgdb"}, 60 | } 61 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%' OR Host_Name__c LIKE '%-dgdb%')" 62 | }) 63 | 64 | It("returns where clause with OR condition", func() { 65 | Expect(clause).To(Equal(expectedClause)) 66 | }) 67 | }) 68 | 69 | Context("when there is single quote in values", func() { 70 | BeforeEach(func() { 71 | critetria = TestQueryCriteria{ 72 | IncludeNamePattern: []string{"-db'", "-dbmgmt", "-dgdb"}, 73 | } 74 | expectedClause = "(Host_Name__c LIKE '%-db\\'%' OR Host_Name__c LIKE '%-dbmgmt%' OR Host_Name__c LIKE '%-dgdb%')" 75 | }) 76 | 77 | It("returns appropriate where clause by escaping single quote", func() { 78 | Expect(clause).To(Equal(expectedClause)) 79 | }) 80 | }) 81 | 82 | Context("when there are special characters in values", func() { 83 | BeforeEach(func() { 84 | critetria = TestQueryCriteria{ 85 | IncludeNamePattern: []string{"-db'_%\\"}, 86 | AssetType: "-db'_%\\", 87 | } 88 | expectedClause = `Host_Name__c LIKE '%-db\'\_\%\\%' AND Tech_Asset__r.Asset_Type_Asset_Type__c = '-db\'_%\\'` 89 | }) 90 | 91 | It("returns appropriate where clause by escaping special characters", func() { 92 | Expect(clause).To(Equal(expectedClause)) 93 | }) 94 | }) 95 | }) 96 | 97 | Context("when only not like clause is populated", func() { 98 | Context("when there is only one item in the not like clause array", func() { 99 | BeforeEach(func() { 100 | critetria = TestQueryCriteria{ 101 | ExcludeNamePattern: []string{"-db"}, 102 | } 103 | expectedClause = "(NOT Host_Name__c LIKE '%-db%')" 104 | }) 105 | 106 | It("returns where clause with only one condition", func() { 107 | Expect(clause).To(Equal(expectedClause)) 108 | }) 109 | }) 110 | 111 | Context("when there is more than one item in the not like clause array", func() { 112 | BeforeEach(func() { 113 | critetria = TestQueryCriteria{ 114 | ExcludeNamePattern: []string{"-db", "-dbmgmt", "-dgdb"}, 115 | } 116 | expectedClause = "((NOT Host_Name__c LIKE '%-db%') AND (NOT Host_Name__c LIKE '%-dbmgmt%') AND (NOT Host_Name__c LIKE '%-dgdb%'))" 117 | }) 118 | 119 | It("returns where clause with OR condition", func() { 120 | Expect(clause).To(Equal(expectedClause)) 121 | }) 122 | }) 123 | 124 | Context("when there is single quote in values", func() { 125 | BeforeEach(func() { 126 | critetria = TestQueryCriteria{ 127 | ExcludeNamePattern: []string{"-d'b"}, 128 | } 129 | expectedClause = "(NOT Host_Name__c LIKE '%-d\\'b%')" 130 | }) 131 | 132 | It("returns appropriate where clause by escaping single quote", func() { 133 | Expect(clause).To(Equal(expectedClause)) 134 | }) 135 | }) 136 | }) 137 | 138 | Context("when only equalsClause is populated", func() { 139 | BeforeEach(func() { 140 | critetria = TestQueryCriteria{ 141 | AssetType: "SERVER", 142 | } 143 | expectedClause = "Tech_Asset__r.Asset_Type_Asset_Type__c = 'SERVER'" 144 | }) 145 | 146 | It("returns appropriate where clause", func() { 147 | Expect(clause).To(Equal(expectedClause)) 148 | }) 149 | 150 | Context("when value has single quote", func() { 151 | BeforeEach(func() { 152 | critetria = TestQueryCriteria{ 153 | AssetType: "SER'VER", 154 | } 155 | expectedClause = "Tech_Asset__r.Asset_Type_Asset_Type__c = 'SER\\'VER'" 156 | }) 157 | 158 | It("returns appropriate where clause by escaping single quote", func() { 159 | Expect(clause).To(Equal(expectedClause)) 160 | }) 161 | }) 162 | }) 163 | 164 | Context("when only notEqualsClause is populated", func() { 165 | BeforeEach(func() { 166 | critetria = TestQueryCriteria{ 167 | Status: "InActive", 168 | } 169 | expectedClause = "Status__c != 'InActive'" 170 | }) 171 | 172 | It("returns appropriate where clause", func() { 173 | Expect(clause).To(Equal(expectedClause)) 174 | }) 175 | 176 | Context("when value has single quote", func() { 177 | BeforeEach(func() { 178 | critetria = TestQueryCriteria{ 179 | Status: "In'Active", 180 | } 181 | expectedClause = "Status__c != 'In\\'Active'" 182 | }) 183 | 184 | It("returns appropriate where clause by escaping single quote", func() { 185 | Expect(clause).To(Equal(expectedClause)) 186 | }) 187 | }) 188 | }) 189 | 190 | Context("when only inClause is populated", func() { 191 | Context("when there is only one item in the inClause array", func() { 192 | BeforeEach(func() { 193 | critetria = TestQueryCriteria{ 194 | Roles: []string{"db"}, 195 | } 196 | expectedClause = "Role__r.Name IN ('db')" 197 | }) 198 | It("returns where clause with only one item in IN clause", func() { 199 | Expect(clause).To(Equal(expectedClause)) 200 | }) 201 | }) 202 | 203 | Context("when there is more than one item in the inClause array", func() { 204 | BeforeEach(func() { 205 | critetria = TestQueryCriteria{ 206 | Roles: []string{"db", "dbmgmt"}, 207 | } 208 | expectedClause = "Role__r.Name IN ('db','dbmgmt')" 209 | }) 210 | It("returns where clause with all the items in IN clause", func() { 211 | Expect(clause).To(Equal(expectedClause)) 212 | }) 213 | }) 214 | 215 | Context("when value has single quote", func() { 216 | BeforeEach(func() { 217 | critetria = TestQueryCriteria{ 218 | Roles: []string{"db", "db'mgmt"}, 219 | } 220 | expectedClause = "Role__r.Name IN ('db','db\\'mgmt')" 221 | }) 222 | It("returns appropriate where clause by escaping single quote", func() { 223 | Expect(clause).To(Equal(expectedClause)) 224 | }) 225 | }) 226 | }) 227 | 228 | Context("when only notInClause is populated", func() { 229 | Context("when there is only one item in the inClause array", func() { 230 | BeforeEach(func() { 231 | critetria = TestQueryCriteria{ 232 | ExcludeIDs: []string{"123"}, 233 | } 234 | expectedClause = "id NOT IN ('123')" 235 | }) 236 | It("returns where clause with only one item in NOT IN clause", func() { 237 | Expect(clause).To(Equal(expectedClause)) 238 | }) 239 | }) 240 | 241 | Context("when there is more than one item in the notInClause array", func() { 242 | BeforeEach(func() { 243 | critetria = TestQueryCriteria{ 244 | ExcludeIDs: []string{"123", "456"}, 245 | } 246 | expectedClause = "id NOT IN ('123','456')" 247 | }) 248 | It("returns where clause with all the items in NOT IN clause", func() { 249 | Expect(clause).To(Equal(expectedClause)) 250 | }) 251 | }) 252 | 253 | Context("when value has single quote", func() { 254 | BeforeEach(func() { 255 | critetria = TestQueryCriteria{ 256 | ExcludeIDs: []string{"123", "4'56"}, 257 | } 258 | expectedClause = "id NOT IN ('123','4\\'56')" 259 | }) 260 | It("returns appropriate where clause by escaping single quote", func() { 261 | Expect(clause).To(Equal(expectedClause)) 262 | }) 263 | }) 264 | }) 265 | 266 | Context("when only null clause is populated", func() { 267 | Context("when null is allowed", func() { 268 | BeforeEach(func() { 269 | allowNull := true 270 | critetria = TestQueryCriteria{ 271 | AllowNullLastDiscoveredDate: &allowNull, 272 | } 273 | 274 | expectedClause = "Last_Discovered_Date__c = null" 275 | }) 276 | 277 | It("returns appropriate where clause", func() { 278 | Expect(clause).To(Equal(expectedClause)) 279 | }) 280 | }) 281 | 282 | Context("when null is not allowed", func() { 283 | BeforeEach(func() { 284 | allowNull := false 285 | critetria = TestQueryCriteria{ 286 | AllowNullLastDiscoveredDate: &allowNull, 287 | } 288 | 289 | expectedClause = "Last_Discovered_Date__c != null" 290 | }) 291 | 292 | It("returns appropriate where clause", func() { 293 | Expect(clause).To(Equal(expectedClause)) 294 | }) 295 | }) 296 | }) 297 | 298 | Context("when likeOperator and inClause are populated", func() { 299 | BeforeEach(func() { 300 | critetria = TestQueryCriteria{ 301 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 302 | Roles: []string{"db"}, 303 | } 304 | 305 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db')" 306 | }) 307 | 308 | It("returns properly formed clause for name and role joined by AND clause", func() { 309 | Expect(clause).To(Equal(expectedClause)) 310 | }) 311 | }) 312 | 313 | Context("when likeOperator and equalsClause are populated", func() { 314 | BeforeEach(func() { 315 | critetria = TestQueryCriteria{ 316 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 317 | AssetType: "SERVER", 318 | } 319 | 320 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Tech_Asset__r.Asset_Type_Asset_Type__c = 'SERVER'" 321 | }) 322 | 323 | It("returns properly formed clause for likeOperator and inClause joined by AND clause", func() { 324 | Expect(clause).To(Equal(expectedClause)) 325 | }) 326 | }) 327 | 328 | Context("when both likeOperator and notlikeOperator are populated", func() { 329 | BeforeEach(func() { 330 | critetria = TestQueryCriteria{ 331 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 332 | ExcludeNamePattern: []string{"-core", "-drp"}, 333 | } 334 | 335 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND ((NOT Host_Name__c LIKE '%-core%') AND (NOT Host_Name__c LIKE '%-drp%'))" 336 | }) 337 | 338 | It("returns properly formed clause for likeOperator and notlikeOperator joined by AND clause", func() { 339 | Expect(clause).To(Equal(expectedClause)) 340 | }) 341 | }) 342 | 343 | Context("when all clauses are populated", func() { 344 | BeforeEach(func() { 345 | allowNull := false 346 | critetria = TestQueryCriteria{ 347 | AssetType: "SERVER", 348 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 349 | Roles: []string{"db", "dbmgmt"}, 350 | ExcludeNamePattern: []string{"-core", "-drp"}, 351 | AllowNullLastDiscoveredDate: &allowNull, 352 | ExcludeIDs: []string{"123", "456"}, 353 | } 354 | 355 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') AND ((NOT Host_Name__c LIKE '%-core%') AND (NOT Host_Name__c LIKE '%-drp%')) AND Tech_Asset__r.Asset_Type_Asset_Type__c = 'SERVER' AND Last_Discovered_Date__c != null AND id NOT IN ('123','456')" 356 | }) 357 | 358 | It("returns properly formed clause joined by AND clause", func() { 359 | Expect(clause).To(Equal(expectedClause)) 360 | }) 361 | }) 362 | }) 363 | 364 | Context("when pointer is passed as argument", func() { 365 | var ( 366 | critetria *TestQueryCriteria 367 | ) 368 | 369 | JustBeforeEach(func() { 370 | clause, err = MarshalWhereClause(critetria) 371 | }) 372 | 373 | Context("when nil is passed as argument", func() { 374 | It("returns empty where clause", func() { 375 | Expect(err).To(Equal(ErrNilValue)) 376 | Expect(clause).To(BeEmpty()) 377 | }) 378 | }) 379 | 380 | Context("when empty value is passed as argument", func() { 381 | BeforeEach(func() { 382 | critetria = &TestQueryCriteria{} 383 | }) 384 | 385 | It("returns empty where clause", func() { 386 | Expect(clause).To(BeEmpty()) 387 | }) 388 | }) 389 | 390 | Context("when all values are populated", func() { 391 | BeforeEach(func() { 392 | critetria = &TestQueryCriteria{ 393 | AssetType: "SERVER", 394 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 395 | Roles: []string{"db", "dbmgmt"}, 396 | ExcludeNamePattern: []string{"-core", "-drp"}, 397 | } 398 | 399 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') AND ((NOT Host_Name__c LIKE '%-core%') AND (NOT Host_Name__c LIKE '%-drp%')) AND Tech_Asset__r.Asset_Type_Asset_Type__c = 'SERVER'" 400 | }) 401 | 402 | It("returns properly formed clause joined by AND clause", func() { 403 | Expect(clause).To(Equal(expectedClause)) 404 | }) 405 | }) 406 | }) 407 | 408 | Context("When values for date literals are int", func() { 409 | var criteria QueryCriteriaDateLiteralsOperatorsInt 410 | BeforeEach(func() { 411 | criteria = QueryCriteriaDateLiteralsOperatorsInt{ 412 | CreatedDate: 5, 413 | ClosedDate: 10, 414 | } 415 | expectedClause = "CreatedDate > NEXT_N_DAYS:5 AND ClosedDate < NEXT_N_DAYS:10" 416 | }) 417 | It("returns appropriate where clause", func() { 418 | clause, err = MarshalWhereClause(criteria) 419 | Expect(clause).To(Equal(expectedClause)) 420 | }) 421 | }) 422 | 423 | Context("When values for date literals are uint", func() { 424 | var criteria QueryCriteriaDateLiteralsOperatorsUint 425 | BeforeEach(func() { 426 | criteria = QueryCriteriaDateLiteralsOperatorsUint{ 427 | CreatedDate: 5, 428 | ClosedDate: 10, 429 | } 430 | expectedClause = "CreatedDate > NEXT_N_DAYS:5 AND ClosedDate < NEXT_N_DAYS:10" 431 | }) 432 | It("returns appropriate where clause", func() { 433 | clause, err = MarshalWhereClause(criteria) 434 | Expect(clause).To(Equal(expectedClause)) 435 | }) 436 | }) 437 | 438 | Context("When values for date literals are wrong type", func() { 439 | type QueryCriteriaDateLiteralsOperatorsWrong struct { 440 | CreatedDate string `soql:"greaterNextNDaysOperator,fieldName=CreatedDate"` 441 | } 442 | var criteria QueryCriteriaDateLiteralsOperatorsWrong 443 | BeforeEach(func() { 444 | criteria = QueryCriteriaDateLiteralsOperatorsWrong{ 445 | CreatedDate: "5", 446 | } 447 | }) 448 | It("returns error", func() { 449 | clause, err = MarshalWhereClause(criteria) 450 | Expect(err).To(Equal(ErrInvalidTag)) 451 | }) 452 | }) 453 | 454 | Context("When values for date literals are pointers", func() { 455 | var criteria QueryCriteriaDateLiteralsOperatorsPtr 456 | Context("When there is only greaterNextNDaysOperator operator", func() { 457 | BeforeEach(func() { 458 | v := 5 459 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 460 | CreatedDate: &v, 461 | } 462 | expectedClause = "CreatedDate > NEXT_N_DAYS:5" 463 | }) 464 | It("returns appropriate where clause", func() { 465 | clause, err = MarshalWhereClause(criteria) 466 | Expect(clause).To(Equal(expectedClause)) 467 | }) 468 | }) 469 | Context("When there is only greaterOrEqualNextNDaysOperator operator", func() { 470 | BeforeEach(func() { 471 | v := 5 472 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 473 | DeliveredDate: &v, 474 | } 475 | expectedClause = "DeliveredDate >= NEXT_N_DAYS:5" 476 | }) 477 | It("returns appropriate where clause", func() { 478 | clause, err = MarshalWhereClause(criteria) 479 | Expect(clause).To(Equal(expectedClause)) 480 | }) 481 | }) 482 | Context("when there is only greaterLastNDaysOperator operator", func() { 483 | BeforeEach(func() { 484 | v0 := 5 485 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 486 | OtherDate: &v0, 487 | } 488 | expectedClause = "OtherDate = NEXT_N_DAYS:5" 489 | }) 490 | It("returns appropriate where clause", func() { 491 | clause, err = MarshalWhereClause(criteria) 492 | Expect(clause).To(Equal(expectedClause)) 493 | }) 494 | }) 495 | Context("When there is only lessNextNDaysOperator operator", func() { 496 | BeforeEach(func() { 497 | v := 10 498 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 499 | ClosedDate: &v, 500 | } 501 | expectedClause = "ClosedDate < NEXT_N_DAYS:10" 502 | }) 503 | It("returns appropriate where clause", func() { 504 | clause, err = MarshalWhereClause(criteria) 505 | Expect(clause).To(Equal(expectedClause)) 506 | }) 507 | }) 508 | Context("When there is only lessOrEqualNextNDaysOperator operator", func() { 509 | BeforeEach(func() { 510 | v := 10 511 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 512 | ScheduledDate: &v, 513 | } 514 | expectedClause = "ScheduledDate <= NEXT_N_DAYS:10" 515 | }) 516 | It("returns appropriate where clause", func() { 517 | clause, err = MarshalWhereClause(criteria) 518 | Expect(clause).To(Equal(expectedClause)) 519 | }) 520 | }) 521 | Context("When there are all operators", func() { 522 | BeforeEach(func() { 523 | v0 := 5 524 | v1 := 10 525 | v2 := 15 526 | v3 := 20 527 | v4 := 25 528 | criteria = QueryCriteriaDateLiteralsOperatorsPtr{ 529 | CreatedDate: &v0, 530 | OtherDate: &v2, 531 | ClosedDate: &v1, 532 | ScheduledDate: &v3, 533 | DeliveredDate: &v4, 534 | } 535 | expectedClause = "CreatedDate > NEXT_N_DAYS:5 AND OtherDate = NEXT_N_DAYS:15 AND ClosedDate < NEXT_N_DAYS:10 AND ScheduledDate <= NEXT_N_DAYS:20 AND DeliveredDate >= NEXT_N_DAYS:25" 536 | }) 537 | It("returns appropriate where clause", func() { 538 | clause, err = MarshalWhereClause(criteria) 539 | Expect(clause).To(Equal(expectedClause)) 540 | }) 541 | }) 542 | }) 543 | 544 | Context("when last_n_days literals are present", func() { 545 | var criteria QueryCriteriaDateLastNDaysLiteralsOperatorsPtr 546 | Context("when there is only lessLastNDaysOperator operator", func() { 547 | BeforeEach(func() { 548 | v1 := 10 549 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 550 | ClosedDate: &v1, 551 | } 552 | expectedClause = "ClosedDate < LAST_N_DAYS:10" 553 | }) 554 | It("returns appropriate where clause", func() { 555 | clause, err = MarshalWhereClause(criteria) 556 | Expect(clause).To(Equal(expectedClause)) 557 | }) 558 | Context("when there is only lessOrEqualLastNDaysOperator operator", func() { 559 | BeforeEach(func() { 560 | v1 := 10 561 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 562 | ScheduledDate: &v1, 563 | } 564 | expectedClause = "ScheduledDate <= LAST_N_DAYS:10" 565 | }) 566 | It("returns appropriate where clause", func() { 567 | clause, err = MarshalWhereClause(criteria) 568 | Expect(clause).To(Equal(expectedClause)) 569 | }) 570 | }) 571 | Context("when there is only greaterLastNDaysOperator operator", func() { 572 | BeforeEach(func() { 573 | v0 := 5 574 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 575 | CreatedDate: &v0, 576 | } 577 | expectedClause = "CreatedDate > LAST_N_DAYS:5" 578 | }) 579 | It("returns appropriate where clause", func() { 580 | clause, err = MarshalWhereClause(criteria) 581 | Expect(clause).To(Equal(expectedClause)) 582 | }) 583 | }) 584 | Context("when there is only greaterOrEqualLastNDaysOperator operator", func() { 585 | BeforeEach(func() { 586 | v0 := 5 587 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 588 | DeliveredDate: &v0, 589 | } 590 | expectedClause = "DeliveredDate >= LAST_N_DAYS:5" 591 | }) 592 | It("returns appropriate where clause", func() { 593 | clause, err = MarshalWhereClause(criteria) 594 | Expect(clause).To(Equal(expectedClause)) 595 | }) 596 | }) 597 | Context("when there is only equalsLastNDaysOperator operator", func() { 598 | BeforeEach(func() { 599 | v0 := 5 600 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 601 | OtherDate: &v0, 602 | } 603 | expectedClause = "OtherDate = LAST_N_DAYS:5" 604 | }) 605 | It("returns appropriate where clause", func() { 606 | clause, err = MarshalWhereClause(criteria) 607 | Expect(clause).To(Equal(expectedClause)) 608 | }) 609 | }) 610 | Context("when there are greaterLastNDaysOperator, lessLastNDaysOperator and equalsLastNDaysOperator operators", func() { 611 | BeforeEach(func() { 612 | v0 := 5 613 | v1 := 10 614 | v2 := 15 615 | v3 := 20 616 | v4 := 25 617 | criteria = QueryCriteriaDateLastNDaysLiteralsOperatorsPtr{ 618 | CreatedDate: &v0, 619 | OtherDate: &v2, 620 | ClosedDate: &v1, 621 | ScheduledDate: &v3, 622 | DeliveredDate: &v4, 623 | } 624 | expectedClause = "CreatedDate > LAST_N_DAYS:5 AND OtherDate = LAST_N_DAYS:15 AND ClosedDate < LAST_N_DAYS:10 AND ScheduledDate <= LAST_N_DAYS:20 AND DeliveredDate >= LAST_N_DAYS:25" 625 | }) 626 | It("returns appropriate where clause", func() { 627 | clause, err = MarshalWhereClause(criteria) 628 | Expect(clause).To(Equal(expectedClause)) 629 | }) 630 | }) 631 | }) 632 | }) 633 | 634 | Context("when all clauses are signed integer data types", func() { 635 | var criteria QueryCriteriaWithIntegerTypes 636 | BeforeEach(func() { 637 | criteria = QueryCriteriaWithIntegerTypes{ 638 | NumOfCPUCores: 16, 639 | PhysicalCPUCount: 4, 640 | NumOfSuccessivePuppetRunFailures: -1, 641 | NumOfCoolanLogFiles: 1024, 642 | PvtTestFailCount: 9223372036854775807, 643 | } 644 | 645 | expectedClause = "Num_of_CPU_Cores__c = 16 AND Physical_CPU_Count__c = 4 AND Number_Of_Successive_Puppet_Run_Failures__c = -1 AND Num_Of_Coolan_Log_Files__c = 1024 AND Pvt_Test_Fail_Count__c = 9223372036854775807" 646 | }) 647 | 648 | It("returns properly formed clause joined by AND clause", func() { 649 | clause, err = MarshalWhereClause(criteria) 650 | Expect(err).ToNot(HaveOccurred()) 651 | Expect(clause).To(Equal(expectedClause)) 652 | }) 653 | }) 654 | 655 | Context("when all clauses are unsigned integer data types", func() { 656 | var criteria QueryCriteriaWithUnsignedIntegerTypes 657 | BeforeEach(func() { 658 | criteria = QueryCriteriaWithUnsignedIntegerTypes{ 659 | NumOfCPUCores: 16, 660 | PhysicalCPUCount: 4, 661 | NumOfSuccessivePuppetRunFailures: 0, 662 | NumOfCoolanLogFiles: 1024, 663 | PvtTestFailCount: 9223372036854775807, 664 | } 665 | 666 | expectedClause = "Num_of_CPU_Cores__c = 16 AND Physical_CPU_Count__c = 4 AND Number_Of_Successive_Puppet_Run_Failures__c = 0 AND Num_Of_Coolan_Log_Files__c = 1024 AND Pvt_Test_Fail_Count__c = 9223372036854775807" 667 | }) 668 | 669 | It("returns properly formed clause joined by AND clause", func() { 670 | clause, err = MarshalWhereClause(criteria) 671 | Expect(err).ToNot(HaveOccurred()) 672 | Expect(clause).To(Equal(expectedClause)) 673 | }) 674 | }) 675 | 676 | Context("when all clauses are float data types", func() { 677 | var criteria QueryCriteriaWithFloatTypes 678 | BeforeEach(func() { 679 | criteria = QueryCriteriaWithFloatTypes{ 680 | NumOfCPUCores: 16.00000000, 681 | PhysicalCPUCount: -4.12345678, 682 | } 683 | 684 | expectedClause = "Num_of_CPU_Cores__c = 16 AND Physical_CPU_Count__c = -4.12345678" 685 | }) 686 | 687 | It("returns properly formed clause joined by AND clause", func() { 688 | clause, err = MarshalWhereClause(criteria) 689 | Expect(err).ToNot(HaveOccurred()) 690 | Expect(clause).To(Equal(expectedClause)) 691 | }) 692 | }) 693 | 694 | Context("when all clauses are *float data types", func() { 695 | var criteria QueryCriteriaWithFloatPtrTypes 696 | BeforeEach(func() { 697 | numCores := 16.0 698 | criteria = QueryCriteriaWithFloatPtrTypes{ 699 | NumOfCPUCores: &numCores, 700 | } 701 | 702 | expectedClause = "Num_of_CPU_Cores__c = 16" 703 | }) 704 | 705 | It("returns properly formed clause joined by skipping nil values", func() { 706 | clause, err = MarshalWhereClause(criteria) 707 | Expect(err).ToNot(HaveOccurred()) 708 | Expect(clause).To(Equal(expectedClause)) 709 | }) 710 | }) 711 | 712 | Context("when all clauses are boolean data types", func() { 713 | var criteria QueryCriteriaWithBooleanType 714 | BeforeEach(func() { 715 | criteria = QueryCriteriaWithBooleanType{ 716 | NUMAEnabled: true, 717 | DisableAlerts: false, 718 | } 719 | 720 | expectedClause = "NUMA_Enabled__c = true AND Disable_Alerts__c = false" 721 | }) 722 | 723 | It("returns properly formed clause joined by AND clause", func() { 724 | clause, err = MarshalWhereClause(criteria) 725 | Expect(err).ToNot(HaveOccurred()) 726 | Expect(clause).To(Equal(expectedClause)) 727 | }) 728 | }) 729 | 730 | Context("when data type is boolean pointer", func() { 731 | var criteria QueryCriteriaWithBooleanPtrType 732 | BeforeEach(func() { 733 | numEnabled := true 734 | criteria = QueryCriteriaWithBooleanPtrType{ 735 | NUMAEnabled: &numEnabled, 736 | } 737 | 738 | expectedClause = "NUMA_Enabled__c = true" 739 | }) 740 | 741 | It("returns properly formed clause by skipping nil values", func() { 742 | clause, err = MarshalWhereClause(criteria) 743 | Expect(err).ToNot(HaveOccurred()) 744 | Expect(clause).To(Equal(expectedClause)) 745 | }) 746 | }) 747 | 748 | Context("when some clauses have soql tags and don't", func() { 749 | var criteria QueryCriteriaWithNoSoqlTag 750 | BeforeEach(func() { 751 | criteria = QueryCriteriaWithNoSoqlTag{ 752 | NUMAEnabled: true, 753 | DisableAlerts: false, 754 | } 755 | 756 | expectedClause = "NUMA_Enabled__c = true" 757 | }) 758 | 759 | It("returns properly formed clause without error§q", func() { 760 | clause, err = MarshalWhereClause(criteria) 761 | Expect(err).ToNot(HaveOccurred()) 762 | Expect(clause).To(Equal(expectedClause)) 763 | }) 764 | }) 765 | 766 | Context("when all clauses are date time data types", func() { 767 | var criteria QueryCriteriaWithDateTimeType 768 | var currentTime time.Time 769 | BeforeEach(func() { 770 | currentTime = time.Now() 771 | criteria = QueryCriteriaWithDateTimeType{ 772 | CreatedDate: currentTime, 773 | } 774 | 775 | expectedClause = "CreatedDate = " + currentTime.Format(DateTimeFormat) 776 | }) 777 | 778 | It("returns properly formed clause", func() { 779 | clause, err = MarshalWhereClause(criteria) 780 | Expect(err).ToNot(HaveOccurred()) 781 | Expect(clause).To(Equal(expectedClause)) 782 | }) 783 | }) 784 | 785 | Context("when all clauses are pointers to date time data types", func() { 786 | var criteria QueryCriteriaWithPtrDateTimeType 787 | var currentTime time.Time 788 | BeforeEach(func() { 789 | currentTime = time.Now() 790 | criteria = QueryCriteriaWithPtrDateTimeType{ 791 | CreatedDate: ¤tTime, 792 | ResolvedDate: nil, 793 | } 794 | 795 | expectedClause = "CreatedDate = " + currentTime.Format(DateTimeFormat) 796 | }) 797 | 798 | It("returns properly formed clause", func() { 799 | clause, err = MarshalWhereClause(criteria) 800 | Expect(err).ToNot(HaveOccurred()) 801 | Expect(clause).To(Equal(expectedClause)) 802 | }) 803 | }) 804 | 805 | Context("when all clauses are mixed data types and operators", func() { 806 | var criteria QueryCriteriaWithMixedDataTypesAndOperators 807 | var currentTime time.Time 808 | BeforeEach(func() { 809 | currentTime = time.Now() 810 | numHardDrives := 2 811 | criteria = QueryCriteriaWithMixedDataTypesAndOperators{ 812 | BIOSType: "98.7.654a", 813 | NumOfCPUCores: 32, 814 | NUMAEnabled: true, 815 | PvtTestFailCount: 256, 816 | PhysicalCPUCount: 4, 817 | CreatedDate: currentTime, 818 | UpdatedDate: ¤tTime, 819 | DisableAlerts: false, 820 | AllocationLatency: 10.5, 821 | MajorOSVersion: "20", 822 | NumOfSuccessivePuppetRunFailures: 0, 823 | LastRestart: currentTime, 824 | ClosedDate: 5, 825 | NumHardDrives: &numHardDrives, 826 | } 827 | 828 | expectedClause = "BIOS_Type__c = '98.7.654a' AND Num_of_CPU_Cores__c > 32 AND NUMA_Enabled__c = true AND Pvt_Test_Fail_Count__c <= 256 AND Physical_CPU_Count__c >= 4 AND CreatedDate = " + currentTime.Format(TestDateFormat) + " AND UpdatedDate = " + currentTime.Format(TestDateFormat) + " AND Disable_Alerts__c = false AND Allocation_Latency__c < 10.5 AND Major_OS_Version__c = '20' AND Number_Of_Successive_Puppet_Run_Failures__c = 0 AND Last_Restart__c > " + currentTime.Format(DateTimeFormat) + " AND NumHardDrives__c = 2 AND ClosedDate > NEXT_N_DAYS:5" 829 | }) 830 | 831 | It("returns properly formed clause", func() { 832 | clause, err = MarshalWhereClause(criteria) 833 | Expect(err).ToNot(HaveOccurred()) 834 | Expect(clause).To(Equal(expectedClause)) 835 | }) 836 | }) 837 | 838 | Context("when clauses contains gt, gte, lt and lte operators", func() { 839 | var criteria QueryCriteriaNumericComparisonOperators 840 | BeforeEach(func() { 841 | criteria = QueryCriteriaNumericComparisonOperators{ 842 | NumOfCPUCores: 16, 843 | PhysicalCPUCount: 4, 844 | NumOfSuccessivePuppetRunFailures: 0, 845 | NumOfCoolanLogFiles: 1024, 846 | } 847 | 848 | expectedClause = "Num_of_CPU_Cores__c > 16 AND Physical_CPU_Count__c < 4 AND Number_Of_Successive_Puppet_Run_Failures__c >= 0 AND Num_Of_Coolan_Log_Files__c <= 1024" 849 | }) 850 | 851 | It("returns properly formed clause", func() { 852 | clause, err = MarshalWhereClause(criteria) 853 | Expect(err).ToNot(HaveOccurred()) 854 | Expect(clause).To(Equal(expectedClause)) 855 | }) 856 | }) 857 | 858 | Context("when no fieldName parameter is specified in tag", func() { 859 | var defaultFieldNameCriteria DefaultFieldNameQueryCriteria 860 | BeforeEach(func() { 861 | defaultFieldNameCriteria = DefaultFieldNameQueryCriteria{ 862 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 863 | Role: []string{"foo", "bar"}, 864 | } 865 | expectedClause = "(Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role IN ('foo','bar')" 866 | }) 867 | 868 | It("returns properly formed clause joined by AND clause", func() { 869 | clause, err = MarshalWhereClause(defaultFieldNameCriteria) 870 | Expect(err).ToNot(HaveOccurred()) 871 | Expect(clause).To(Equal(expectedClause)) 872 | }) 873 | }) 874 | 875 | Context("when tag is invalid", func() { 876 | Context("when struct has invalid tag key", func() { 877 | type InvalidCriteriaStruct struct { 878 | SomePattern []string `soql:"likeOperator,fieldName=Some_Pattern__c"` 879 | SomeOtherPattern string `soql:"invalidClause,fieldName=Some_Other_Field"` 880 | } 881 | 882 | It("returns ErrInvalidTag error", func() { 883 | str, err := MarshalWhereClause(InvalidCriteriaStruct{}) 884 | Expect(err).To(Equal(ErrInvalidTag)) 885 | Expect(str).To(BeEmpty()) 886 | }) 887 | }) 888 | 889 | Context("when struct has missing fieldName", func() { 890 | type MissingFieldName struct { 891 | SomePattern []string `soql:"likeOperator,fieldName=Some_Pattern__c"` 892 | SomeOtherPattern string `soql:"equalsOperator,fieldName="` 893 | } 894 | 895 | It("returns ErrInvalidTag error", func() { 896 | str, err := MarshalWhereClause(MissingFieldName{ 897 | SomePattern: []string{"test"}, 898 | SomeOtherPattern: "foo", 899 | }) 900 | Expect(err).To(Equal(ErrInvalidTag)) 901 | Expect(str).To(BeEmpty()) 902 | }) 903 | }) 904 | 905 | Context("when struct has invalid type for likeOperator", func() { 906 | type QueryCriteriaWithInvalidLikeOperator struct { 907 | IncludeNamePattern []bool `soql:"likeOperator,fieldName=Host_Name__c"` 908 | } 909 | It("returns error", func() { 910 | _, err := MarshalWhereClause(QueryCriteriaWithInvalidLikeOperator{}) 911 | Expect(err).To(Equal(ErrInvalidTag)) 912 | }) 913 | }) 914 | 915 | Context("when struct has invalid type for likeOperator", func() { 916 | type QueryCriteriaWithInvalidNotLikeOperator struct { 917 | ExcludeNamePattern []bool `soql:"notLikeOperator,fieldName=Host_Name__c"` 918 | } 919 | It("returns error", func() { 920 | _, err := MarshalWhereClause(QueryCriteriaWithInvalidNotLikeOperator{}) 921 | Expect(err).To(Equal(ErrInvalidTag)) 922 | }) 923 | }) 924 | 925 | Context("when struct has invalid type for inOperator", func() { 926 | type QueryCriteriaWithInvalidInOperator struct { 927 | Roles int `soql:"inOperator,fieldName=Role__c"` 928 | } 929 | It("returns error", func() { 930 | _, err := MarshalWhereClause(QueryCriteriaWithInvalidInOperator{}) 931 | Expect(err).To(Equal(ErrInvalidTag)) 932 | }) 933 | }) 934 | 935 | Context("when struct has invalid type for comparison operators", func() { 936 | type QueryCriteriaWithInvalidComparisonOperator struct { 937 | Roles []int `soql:"lessThanOperator,fieldName=Role__c"` 938 | } 939 | It("returns error", func() { 940 | _, err := MarshalWhereClause(QueryCriteriaWithInvalidComparisonOperator{}) 941 | Expect(err).To(Equal(ErrInvalidTag)) 942 | }) 943 | }) 944 | }) 945 | }) 946 | 947 | Describe("MarshalOrderByClause", func() { 948 | Context("when valid Order slice passed as argument", func() { 949 | Context("when an empty Order by slice is passed", func() { 950 | It("returns empty order by clause", func() { 951 | clause, err := MarshalOrderByClause([]Order{}, struct { 952 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 953 | }{}) 954 | Expect(err).ToNot(HaveOccurred()) 955 | Expect(clause).To(BeEmpty()) 956 | }) 957 | }) 958 | 959 | Context("when an Order slice with single order by column desc is passed", func() { 960 | It("returns a column desc partial clause", func() { 961 | desc := Order{Field: "NumOfCPUCores", IsDesc: true} 962 | clause, err := MarshalOrderByClause([]Order{desc}, struct { 963 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 964 | }{}) 965 | Expect(err).ToNot(HaveOccurred()) 966 | Expect(clause).To(Equal("Num_of_CPU_Cores__c DESC")) 967 | }) 968 | }) 969 | 970 | Context("when an Order slice with single order by column asc is passed", func() { 971 | It("returns a column asc partial clause", func() { 972 | asc := Order{Field: "NumOfCPUCores", IsDesc: false} 973 | clause, err := MarshalOrderByClause([]Order{asc}, struct { 974 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 975 | }{}) 976 | Expect(err).ToNot(HaveOccurred()) 977 | Expect(clause).To(Equal("Num_of_CPU_Cores__c ASC")) 978 | }) 979 | }) 980 | 981 | Context("when an Order slice with multiple order by column ASC is passed", func() { 982 | It("returns multiple columns asc partial clause", func() { 983 | col1 := Order{Field: "MajorOSVersion", IsDesc: false} 984 | col2 := Order{Field: "NumOfCPUCores", IsDesc: false} 985 | col3 := Order{Field: "PhysicalCPUCount", IsDesc: false} 986 | clause, err := MarshalOrderByClause([]Order{col1, col2, col3}, struct { 987 | MajorOSVersion string `soql:"selectColumn,fieldName=Major_OS_Version__c"` 988 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 989 | PhysicalCPUCount uint8 `soql:"selectColumn,fieldName=Physical_CPU_Count__c"` 990 | LastRestart time.Time `soql:"selectColumn,fieldName=Last_Restart__c"` 991 | }{}) 992 | Expect(err).ToNot(HaveOccurred()) 993 | Expect(clause).To(Equal("Major_OS_Version__c ASC,Num_of_CPU_Cores__c ASC,Physical_CPU_Count__c ASC")) 994 | }) 995 | }) 996 | 997 | Context("when an Order slice with multiple order by column DESC is passed", func() { 998 | It("returns multiple columns asc partial clause", func() { 999 | col1 := Order{Field: "MajorOSVersion", IsDesc: true} 1000 | col2 := Order{Field: "NumOfCPUCores", IsDesc: true} 1001 | col3 := Order{Field: "LastRestart", IsDesc: true} 1002 | clause, err := MarshalOrderByClause([]Order{col1, col2, col3}, struct { 1003 | MajorOSVersion string `soql:"selectColumn,fieldName=Major_OS_Version__c"` 1004 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 1005 | PhysicalCPUCount uint8 `soql:"selectColumn,fieldName=Physical_CPU_Count__c"` 1006 | LastRestart time.Time `soql:"selectColumn,fieldName=Last_Restart__c"` 1007 | }{}) 1008 | Expect(err).ToNot(HaveOccurred()) 1009 | Expect(clause).To(Equal("Major_OS_Version__c DESC,Num_of_CPU_Cores__c DESC,Last_Restart__c DESC")) 1010 | }) 1011 | }) 1012 | 1013 | Context("when an Order slice with multiple order by column with mixed order is passed", func() { 1014 | It("returns a valid partial clause", func() { 1015 | col1 := Order{Field: "MajorOSVersion", IsDesc: true} 1016 | col2 := Order{Field: "NumOfCPUCores", IsDesc: false} 1017 | col3 := Order{Field: "PhysicalCPUCount", IsDesc: true} 1018 | col4 := Order{Field: "LastRestart", IsDesc: false} 1019 | clause, err := MarshalOrderByClause([]Order{col1, col2, col3, col4}, struct { 1020 | MajorOSVersion string `soql:"selectColumn,fieldName=Major_OS_Version__c"` 1021 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 1022 | PhysicalCPUCount uint8 `soql:"selectColumn,fieldName=Physical_CPU_Count__c"` 1023 | LastRestart time.Time `soql:"selectColumn,fieldName=Last_Restart__c"` 1024 | }{}) 1025 | Expect(err).ToNot(HaveOccurred()) 1026 | Expect(clause).To(Equal("Major_OS_Version__c DESC,Num_of_CPU_Cores__c ASC,Physical_CPU_Count__c DESC,Last_Restart__c ASC")) 1027 | }) 1028 | }) 1029 | }) 1030 | 1031 | Context("when invalid order by is passed as argument", func() { 1032 | Context("when a slice that is not of Order type is passed as argument", func() { 1033 | It("returns error", func() { 1034 | _, err := MarshalOrderByClause([]string{"test"}, struct { 1035 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 1036 | }{}) 1037 | Expect(err).To(Equal(ErrInvalidOrderByClause)) 1038 | }) 1039 | }) 1040 | 1041 | Context("when an Order slice containing incorrect field name is passed as argument", func() { 1042 | It("returns error", func() { 1043 | col1 := Order{Field: "MajorOSVersion", IsDesc: true} 1044 | _, err := MarshalOrderByClause([]Order{col1}, struct { 1045 | NumOfCPUCores int `soql:"selectColumn,fieldName=Num_of_CPU_Cores__c"` 1046 | }{}) 1047 | Expect(err).To(Equal(ErrInvalidOrderByClause)) 1048 | }) 1049 | }) 1050 | }) 1051 | 1052 | Context("when invalid selectColumn struct is passed as argument", func() { 1053 | Context("when a struct with no selectColumn is passed as argument", func() { 1054 | It("returns error", func() { 1055 | col1 := Order{Field: "MajorOSVersion", IsDesc: true} 1056 | _, err := MarshalOrderByClause([]Order{col1}, struct { 1057 | NumOfCPUCores int `soql:"fieldName=Num_of_CPU_Cores__c"` 1058 | }{}) 1059 | Expect(err).To(Equal(ErrInvalidSelectColumnOrderByClause)) 1060 | }) 1061 | }) 1062 | 1063 | Context("when a non-struct is passed as argument", func() { 1064 | It("returns error", func() { 1065 | col1 := Order{Field: "MajorOSVersion", IsDesc: true} 1066 | _, err := MarshalOrderByClause([]Order{col1}, "dummy") 1067 | Expect(err).To(Equal(ErrInvalidSelectColumnOrderByClause)) 1068 | }) 1069 | }) 1070 | }) 1071 | }) 1072 | 1073 | Describe("MarshalSelectClause", func() { 1074 | Context("when non pointer value is passed as argument", func() { 1075 | Context("when no relationship name is passed", func() { 1076 | Context("when no nested struct is passed", func() { 1077 | It("returns just the json tag names of fields concatenanted by comma", func() { 1078 | str, err := MarshalSelectClause(NonNestedStruct{}, "") 1079 | Expect(err).ToNot(HaveOccurred()) 1080 | Expect(str).To(Equal("Name,SomeValue__c")) 1081 | }) 1082 | }) 1083 | 1084 | Context("when no fieldName parameter is specified in tag", func() { 1085 | It("returns propery resolved list of field names by using defaults", func() { 1086 | str, err := MarshalSelectClause(DefaultFieldNameStruct{}, "") 1087 | Expect(err).ToNot(HaveOccurred()) 1088 | Expect(str).To(Equal("DefaultName,Description__c")) 1089 | }) 1090 | }) 1091 | 1092 | Context("when nested struct is passed", func() { 1093 | It("returns properly resolved list of field names", func() { 1094 | str, err := MarshalSelectClause(NestedStruct{}, "") 1095 | Expect(err).ToNot(HaveOccurred()) 1096 | Expect(str).To(Equal("Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c")) 1097 | }) 1098 | }) 1099 | }) 1100 | 1101 | Context("when relationship name is passed", func() { 1102 | Context("when no nested struct is passed", func() { 1103 | It("returns just the json tag names of fields concatenanted by comma and prefixed by relationship name", func() { 1104 | str, err := MarshalSelectClause(NonNestedStruct{}, "Role__r") 1105 | Expect(err).ToNot(HaveOccurred()) 1106 | Expect(str).To(Equal("Role__r.Name,Role__r.SomeValue__c")) 1107 | }) 1108 | }) 1109 | }) 1110 | 1111 | Context("when struct has invalid tag key", func() { 1112 | type InvalidStruct struct { 1113 | Id string `soql:"selectColumn,fieldName=Id"` 1114 | Foo string `soql:"invalidClause,fieldName=Foo"` 1115 | } 1116 | 1117 | It("returns ErrInvalidTag error", func() { 1118 | str, err := MarshalSelectClause(InvalidStruct{}, "") 1119 | Expect(err).To(Equal(ErrInvalidTag)) 1120 | Expect(str).To(BeEmpty()) 1121 | }) 1122 | }) 1123 | 1124 | Context("when struct has missing fieldName", func() { 1125 | type MissingFieldName struct { 1126 | SomePattern []string `soql:"selectColumn,fieldName=Some_Pattern__c"` 1127 | SomeOtherPattern string `soql:"selectColumn,fieldName="` 1128 | } 1129 | 1130 | It("returns ErrInvalidTag error", func() { 1131 | str, err := MarshalSelectClause(MissingFieldName{}, "") 1132 | Expect(err).To(Equal(ErrInvalidTag)) 1133 | Expect(str).To(BeEmpty()) 1134 | }) 1135 | }) 1136 | 1137 | Context("when struct has child relationship", func() { 1138 | Context("when child struct has select clause only", func() { 1139 | It("returns properly constructed select clause", func() { 1140 | str, err := MarshalSelectClause(ParentStruct{}, "") 1141 | Expect(err).ToNot(HaveOccurred()) 1142 | Expect(str).To(Equal("Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c,(SELECT SM_Application_Versions__c.Version__c FROM Application_Versions__r)")) 1143 | }) 1144 | }) 1145 | 1146 | Context("when child struct has select clause and where clause", func() { 1147 | It("returns properly constructed select clause", func() { 1148 | str, err := MarshalSelectClause(ParentStruct{ 1149 | ChildStruct: TestChildStruct{ 1150 | WhereClause: ChildQueryCriteria{ 1151 | Name: "sfdc-release", 1152 | }, 1153 | }, 1154 | }, "") 1155 | Expect(err).ToNot(HaveOccurred()) 1156 | Expect(str).To(Equal("Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c,(SELECT SM_Application_Versions__c.Version__c FROM Application_Versions__r WHERE SM_Application_Versions__c.Name__c = 'sfdc-release')")) 1157 | }) 1158 | }) 1159 | 1160 | Context("when selectChild tag does not have fieldName parameter", func() { 1161 | It("returns properly constructed select clause", func() { 1162 | str, err := MarshalSelectClause(DefaultFieldNameParentStruct{ 1163 | ChildStruct: TestChildStruct{ 1164 | WhereClause: ChildQueryCriteria{ 1165 | Name: "sfdc-release", 1166 | }, 1167 | }, 1168 | }, "") 1169 | Expect(err).ToNot(HaveOccurred()) 1170 | Expect(str).To(Equal("Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c,(SELECT SM_Application_Versions__c.Version__c FROM ChildStruct WHERE SM_Application_Versions__c.Name__c = 'sfdc-release')")) 1171 | }) 1172 | }) 1173 | 1174 | Context("when child struct does not have select clause", func() { 1175 | It("returns error", func() { 1176 | _, err := MarshalSelectClause(InvalidParentStruct{}, "") 1177 | Expect(err).To(Equal(ErrNoSelectClause)) 1178 | }) 1179 | }) 1180 | 1181 | Context("when selectChild is used on non struct member", func() { 1182 | It("returns error", func() { 1183 | _, err := MarshalSelectClause(InvalidSelectChildClause{}, "") 1184 | Expect(err).To(Equal(ErrInvalidTag)) 1185 | }) 1186 | }) 1187 | 1188 | Context("when selectChild tag is applied to non struct member", func() { 1189 | It("returns error", func() { 1190 | _, err := MarshalSelectClause(ChildTagToNonStruct{}, "") 1191 | Expect(err).To(Equal(ErrInvalidTag)) 1192 | }) 1193 | }) 1194 | }) 1195 | }) 1196 | 1197 | Context("when pointer value is passed as argument", func() { 1198 | Context("when nil is passed", func() { 1199 | It("returns ErrNilValue error", func() { 1200 | var r *NestedStruct 1201 | str, err := MarshalSelectClause(r, "") 1202 | Expect(err).To(Equal(ErrNilValue)) 1203 | Expect(str).To(BeEmpty()) 1204 | }) 1205 | }) 1206 | 1207 | Context("when nested struct is passed", func() { 1208 | It("returns properly resolved list of field names", func() { 1209 | str, err := MarshalSelectClause(&NestedStruct{}, "") 1210 | Expect(err).ToNot(HaveOccurred()) 1211 | Expect(str).To(Equal("Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c")) 1212 | }) 1213 | }) 1214 | }) 1215 | }) 1216 | 1217 | Describe("Marshal", func() { 1218 | var ( 1219 | soqlStruct interface{} 1220 | expectedQuery string 1221 | actualQuery string 1222 | err error 1223 | ) 1224 | 1225 | JustBeforeEach(func() { 1226 | actualQuery, err = Marshal(soqlStruct) 1227 | }) 1228 | 1229 | Context("when empty struct is passed as argument", func() { 1230 | BeforeEach(func() { 1231 | soqlStruct = EmptyStruct{} 1232 | }) 1233 | 1234 | It("returns empty string", func() { 1235 | Expect(err).ToNot(HaveOccurred()) 1236 | Expect(actualQuery).To(BeEmpty()) 1237 | }) 1238 | }) 1239 | 1240 | Context("when valid value is passed as argument", func() { 1241 | BeforeEach(func() { 1242 | soqlStruct = TestSoqlStruct{ 1243 | SelectClause: NestedStruct{}, 1244 | WhereClause: TestQueryCriteria{ 1245 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1246 | Roles: []string{"db", "dbmgmt"}, 1247 | }, 1248 | } 1249 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt')" 1250 | }) 1251 | 1252 | It("returns properly constructed soql query", func() { 1253 | Expect(err).ToNot(HaveOccurred()) 1254 | Expect(actualQuery).To(Equal(expectedQuery)) 1255 | }) 1256 | }) 1257 | 1258 | Context("when value with single quotes is passed as argument", func() { 1259 | BeforeEach(func() { 1260 | soqlStruct = TestSoqlStruct{ 1261 | SelectClause: NestedStruct{}, 1262 | WhereClause: TestQueryCriteria{ 1263 | IncludeNamePattern: []string{"Blips 'n' Chitz", "Michaels"}, 1264 | }, 1265 | } 1266 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%Blips \\'n\\' Chitz%' OR Host_Name__c LIKE '%Michaels%')" 1267 | }) 1268 | 1269 | It("returns properly constructed soql query", func() { 1270 | Expect(err).ToNot(HaveOccurred()) 1271 | Expect(actualQuery).To(Equal(expectedQuery)) 1272 | }) 1273 | }) 1274 | 1275 | Context("when valid value with mixed data type and operator is passed as argument", func() { 1276 | BeforeEach(func() { 1277 | currentTime := time.Now() 1278 | soqlStruct = TestSoqlMixedDataAndOperatorStruct{ 1279 | SelectClause: NestedStruct{}, 1280 | WhereClause: QueryCriteriaWithMixedDataTypesAndOperators{ 1281 | BIOSType: "98.7.654a", 1282 | NumOfCPUCores: 32, 1283 | NUMAEnabled: true, 1284 | PvtTestFailCount: 256, 1285 | PhysicalCPUCount: 4, 1286 | CreatedDate: currentTime, 1287 | UpdatedDate: ¤tTime, 1288 | DisableAlerts: false, 1289 | AllocationLatency: 10.5, 1290 | MajorOSVersion: "20", 1291 | NumOfSuccessivePuppetRunFailures: 0, 1292 | ClosedDate: 5, 1293 | LastRestart: currentTime, 1294 | }, 1295 | } 1296 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE BIOS_Type__c = '98.7.654a' AND Num_of_CPU_Cores__c > 32 AND NUMA_Enabled__c = true AND Pvt_Test_Fail_Count__c <= 256 AND Physical_CPU_Count__c >= 4 AND CreatedDate = " + currentTime.Format(TestDateFormat) + " AND UpdatedDate = " + currentTime.Format(TestDateFormat) + " AND Disable_Alerts__c = false AND Allocation_Latency__c < 10.5 AND Major_OS_Version__c = '20' AND Number_Of_Successive_Puppet_Run_Failures__c = 0 AND Last_Restart__c > " + currentTime.Format(DateTimeFormat) + " AND ClosedDate > NEXT_N_DAYS:5" 1297 | }) 1298 | 1299 | It("returns properly constructed soql query", func() { 1300 | Expect(err).ToNot(HaveOccurred()) 1301 | Expect(actualQuery).To(Equal(expectedQuery)) 1302 | }) 1303 | }) 1304 | 1305 | Context("when valid pointer is passed as argument", func() { 1306 | BeforeEach(func() { 1307 | soqlStruct = &TestSoqlStruct{ 1308 | SelectClause: NestedStruct{}, 1309 | WhereClause: TestQueryCriteria{ 1310 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1311 | Roles: []string{"db", "dbmgmt"}, 1312 | }, 1313 | } 1314 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt')" 1315 | }) 1316 | 1317 | It("returns properly constructed soql query", func() { 1318 | Expect(err).ToNot(HaveOccurred()) 1319 | Expect(actualQuery).To(Equal(expectedQuery)) 1320 | }) 1321 | }) 1322 | 1323 | Context("when struct with no soql tags is passed", func() { 1324 | BeforeEach(func() { 1325 | soqlStruct = NonSoqlStruct{} 1326 | }) 1327 | 1328 | It("returns emptyString", func() { 1329 | Expect(err).ToNot(HaveOccurred()) 1330 | Expect(actualQuery).To(BeEmpty()) 1331 | }) 1332 | }) 1333 | 1334 | Context("when struct with multiple selectClause is passed", func() { 1335 | BeforeEach(func() { 1336 | soqlStruct = MultipleSelectClause{} 1337 | }) 1338 | 1339 | It("returns error", func() { 1340 | Expect(err).To(Equal(ErrMultipleSelectClause)) 1341 | }) 1342 | }) 1343 | 1344 | Context("when selectClause is used on non struct members", func() { 1345 | BeforeEach(func() { 1346 | soqlStruct = InvalidSelectClause{} 1347 | }) 1348 | 1349 | It("returns error", func() { 1350 | Expect(err).To(Equal(ErrInvalidTag)) 1351 | }) 1352 | }) 1353 | 1354 | Context("when struct with multiple whereClause is passed", func() { 1355 | BeforeEach(func() { 1356 | soqlStruct = MultipleWhereClause{} 1357 | }) 1358 | 1359 | It("returns error", func() { 1360 | Expect(err).To(Equal(ErrMultipleWhereClause)) 1361 | }) 1362 | }) 1363 | 1364 | Context("when struct with only whereClause is passed", func() { 1365 | BeforeEach(func() { 1366 | soqlStruct = OnlyWhereClause{ 1367 | WhereClause: TestQueryCriteria{ 1368 | AssetType: "SERVER", 1369 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1370 | Roles: []string{"db", "dbmgmt"}, 1371 | }, 1372 | } 1373 | }) 1374 | 1375 | It("returns error", func() { 1376 | Expect(err).To(Equal(ErrNoSelectClause)) 1377 | }) 1378 | }) 1379 | 1380 | Context("when struct with multiple whereClause is passed", func() { 1381 | BeforeEach(func() { 1382 | soqlStruct = MultipleWhereClause{ 1383 | WhereClause1: ChildQueryCriteria{ 1384 | Name: "foo", 1385 | }, 1386 | WhereClause2: ChildQueryCriteria{ 1387 | Name: "bar", 1388 | }, 1389 | } 1390 | }) 1391 | 1392 | It("returns error", func() { 1393 | Expect(err).To(HaveOccurred()) 1394 | }) 1395 | }) 1396 | 1397 | Context("when nil pointer is passed", func() { 1398 | BeforeEach(func() { 1399 | var ptr *TestSoqlStruct 1400 | soqlStruct = ptr 1401 | }) 1402 | 1403 | It("returns ErrNilValue error", func() { 1404 | Expect(err).To(Equal(ErrNilValue)) 1405 | }) 1406 | }) 1407 | 1408 | Context("when struct with invalid tag is passed", func() { 1409 | BeforeEach(func() { 1410 | soqlStruct = InvalidTagInStruct{} 1411 | }) 1412 | 1413 | It("returns error", func() { 1414 | Expect(err).To(Equal(ErrInvalidTag)) 1415 | }) 1416 | }) 1417 | 1418 | Context("when no table name is specified for selectClause", func() { 1419 | BeforeEach(func() { 1420 | soqlStruct = DefaultTableNameStruct{ 1421 | SomeTableName: NestedStruct{}, 1422 | WhereClause: TestQueryCriteria{ 1423 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1424 | Roles: []string{"db", "dbmgmt"}, 1425 | }, 1426 | } 1427 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SomeTableName WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt')" 1428 | }) 1429 | 1430 | It("uses name of the field as table name and returns properly constructed soql query", func() { 1431 | Expect(err).ToNot(HaveOccurred()) 1432 | Expect(actualQuery).To(Equal(expectedQuery)) 1433 | }) 1434 | }) 1435 | 1436 | Context("when struct with multiple orderByClause is passed", func() { 1437 | BeforeEach(func() { 1438 | soqlStruct = MultipleOrderByClause{} 1439 | }) 1440 | 1441 | It("returns error", func() { 1442 | Expect(err).To(Equal(ErrMultipleOrderByClause)) 1443 | }) 1444 | }) 1445 | 1446 | Context("when struct with only orderByClause is passed", func() { 1447 | BeforeEach(func() { 1448 | soqlStruct = OnlyOrderByClause{} 1449 | }) 1450 | 1451 | It("returns error", func() { 1452 | Expect(err).To(Equal(ErrNoSelectClause)) 1453 | }) 1454 | }) 1455 | 1456 | Context("when a struct with mixed order by columns at top query level is passed", func() { 1457 | BeforeEach(func() { 1458 | soqlStruct = TestSoqlOrderByStruct{OrderByClause: []Order{ 1459 | Order{Field: "ID", IsDesc: false}, 1460 | Order{Field: "Name", IsDesc: true}, 1461 | Order{Field: "NonNestedStruct.SomeValue", IsDesc: false}, 1462 | }} 1463 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c ORDER BY Id ASC,Name__c DESC,NonNestedStruct__r.SomeValue__c ASC" 1464 | }) 1465 | 1466 | It("returns properly constructed soql query", func() { 1467 | Expect(err).ToNot(HaveOccurred()) 1468 | Expect(actualQuery).To(Equal(expectedQuery)) 1469 | }) 1470 | }) 1471 | 1472 | Context("when a struct with order by inside a child relation is passed", func() { 1473 | BeforeEach(func() { 1474 | col1 := Order{Field: "Version", IsDesc: true} 1475 | soqlStruct = TestSoqlChildRelationOrderByStruct{ 1476 | SelectClause: OrderByParentStruct{ 1477 | ChildStruct: TestChildWithOrderByStruct{ 1478 | OrderByClause: []Order{col1}, 1479 | }, 1480 | }, 1481 | } 1482 | expectedQuery = "SELECT Id,Name__c,(SELECT SM_Application_Versions__c.Version__c FROM Application_Versions__r ORDER BY SM_Application_Versions__c.Version__c DESC) FROM SM_Logical_Host__c" 1483 | }) 1484 | 1485 | It("returns properly constructed soql query", func() { 1486 | Expect(err).ToNot(HaveOccurred()) 1487 | Expect(actualQuery).To(Equal(expectedQuery)) 1488 | }) 1489 | }) 1490 | 1491 | Context("when a struct with order by clause in top level struct and child relation is passed", func() { 1492 | BeforeEach(func() { 1493 | col1 := Order{Field: "Version", IsDesc: true} 1494 | col2 := Order{Field: "ID", IsDesc: true} 1495 | col3 := Order{Field: "Name", IsDesc: false} 1496 | soqlStruct = TestSoqlChildRelationOrderByStruct{ 1497 | SelectClause: OrderByParentStruct{ 1498 | ChildStruct: TestChildWithOrderByStruct{ 1499 | OrderByClause: []Order{col1}, 1500 | }, 1501 | }, 1502 | OrderByClause: []Order{col2, col3}, 1503 | } 1504 | expectedQuery = "SELECT Id,Name__c,(SELECT SM_Application_Versions__c.Version__c FROM Application_Versions__r ORDER BY SM_Application_Versions__c.Version__c DESC) FROM SM_Logical_Host__c ORDER BY Id DESC,Name__c ASC" 1505 | }) 1506 | 1507 | It("returns properly constructed soql query", func() { 1508 | Expect(err).ToNot(HaveOccurred()) 1509 | Expect(actualQuery).To(Equal(expectedQuery)) 1510 | }) 1511 | }) 1512 | 1513 | Context("when a struct with limit value greater than 0 is passed", func() { 1514 | BeforeEach(func() { 1515 | input := 5 1516 | soqlStruct = TestSoqlLimitStruct{ 1517 | SelectClause: NestedStruct{}, 1518 | WhereClause: TestQueryCriteria{ 1519 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1520 | Roles: []string{"db", "dbmgmt"}, 1521 | }, 1522 | Limit: &input, 1523 | } 1524 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') LIMIT 5" 1525 | }) 1526 | 1527 | It("returns properly constructed soql query", func() { 1528 | Expect(err).ToNot(HaveOccurred()) 1529 | Expect(actualQuery).To(Equal(expectedQuery)) 1530 | }) 1531 | }) 1532 | 1533 | Context("when a struct with limit value equal to 0 is passed", func() { 1534 | BeforeEach(func() { 1535 | input := 0 1536 | soqlStruct = TestSoqlLimitStruct{ 1537 | SelectClause: NestedStruct{}, 1538 | WhereClause: TestQueryCriteria{ 1539 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1540 | Roles: []string{"db", "dbmgmt"}, 1541 | }, 1542 | Limit: &input, 1543 | } 1544 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') LIMIT 0" 1545 | }) 1546 | 1547 | It("returns properly constructed soql query", func() { 1548 | Expect(err).ToNot(HaveOccurred()) 1549 | Expect(actualQuery).To(Equal(expectedQuery)) 1550 | }) 1551 | }) 1552 | 1553 | Context("when a struct without limit value is passed", func() { 1554 | BeforeEach(func() { 1555 | soqlStruct = TestSoqlLimitStruct{ 1556 | SelectClause: NestedStruct{}, 1557 | WhereClause: TestQueryCriteria{ 1558 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1559 | Roles: []string{"db", "dbmgmt"}, 1560 | }, 1561 | } 1562 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt')" 1563 | }) 1564 | 1565 | It("returns properly constructed soql query", func() { 1566 | Expect(err).ToNot(HaveOccurred()) 1567 | Expect(actualQuery).To(Equal(expectedQuery)) 1568 | }) 1569 | }) 1570 | 1571 | Context("when a struct with invalid limit type is passed", func() { 1572 | BeforeEach(func() { 1573 | soqlStruct = TestSoqlInvalidLimitStruct{ 1574 | SelectClause: NestedStruct{}, 1575 | WhereClause: TestQueryCriteria{ 1576 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1577 | Roles: []string{"db", "dbmgmt"}, 1578 | }, 1579 | Limit: "5", 1580 | } 1581 | }) 1582 | 1583 | It("returns error", func() { 1584 | Expect(err).To(Equal(ErrInvalidLimitClause)) 1585 | }) 1586 | }) 1587 | 1588 | Context("when a struct with invalid limit value is passed", func() { 1589 | BeforeEach(func() { 1590 | input := -5 1591 | soqlStruct = TestSoqlLimitStruct{ 1592 | SelectClause: NestedStruct{}, 1593 | WhereClause: TestQueryCriteria{ 1594 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1595 | Roles: []string{"db", "dbmgmt"}, 1596 | }, 1597 | Limit: &input, 1598 | } 1599 | }) 1600 | 1601 | It("returns error", func() { 1602 | Expect(err).To(Equal(ErrInvalidLimitClause)) 1603 | }) 1604 | }) 1605 | 1606 | Context("when a struct with multiple limit values is passed", func() { 1607 | BeforeEach(func() { 1608 | input := 5 1609 | soqlStruct = TestSoqlMultipleLimitStruct{ 1610 | SelectClause: NestedStruct{}, 1611 | WhereClause: TestQueryCriteria{ 1612 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1613 | Roles: []string{"db", "dbmgmt"}, 1614 | }, 1615 | Limit: &input, 1616 | AlsoLimit: &input, 1617 | } 1618 | }) 1619 | 1620 | It("returns error", func() { 1621 | Expect(err).To(Equal(ErrMultipleLimitClause)) 1622 | }) 1623 | }) 1624 | 1625 | Context("when a struct with limit inside a child relation is passed", func() { 1626 | BeforeEach(func() { 1627 | input := 1 1628 | soqlStruct = TestSoqlChildRelationLimitStruct{ 1629 | SelectClause: ParentLimitStruct{ 1630 | ChildStruct: ChildLimitStruct{ 1631 | Limit: &input, 1632 | }, 1633 | }, 1634 | } 1635 | expectedQuery = "SELECT Id,Name__c,(SELECT Application_Versions__c.Id,Application_Versions__c.Version__c FROM Application_Versions__r LIMIT 1) FROM SM_Logical_Host__c" 1636 | }) 1637 | 1638 | It("returns properly constructed soql query", func() { 1639 | Expect(err).ToNot(HaveOccurred()) 1640 | Expect(actualQuery).To(Equal(expectedQuery)) 1641 | }) 1642 | }) 1643 | 1644 | Context("when a struct with limit inside a child relation is passed", func() { 1645 | BeforeEach(func() { 1646 | inputChild := 10 1647 | inputParent := 25 1648 | soqlStruct = TestSoqlChildRelationLimitStruct{ 1649 | SelectClause: ParentLimitStruct{ 1650 | ChildStruct: ChildLimitStruct{ 1651 | Limit: &inputChild, 1652 | }, 1653 | }, 1654 | Limit: &inputParent, 1655 | } 1656 | expectedQuery = "SELECT Id,Name__c,(SELECT Application_Versions__c.Id,Application_Versions__c.Version__c FROM Application_Versions__r LIMIT 10) FROM SM_Logical_Host__c LIMIT 25" 1657 | }) 1658 | 1659 | It("returns properly constructed soql query", func() { 1660 | Expect(err).ToNot(HaveOccurred()) 1661 | Expect(actualQuery).To(Equal(expectedQuery)) 1662 | }) 1663 | }) 1664 | 1665 | Context("when a struct with offset value is passed", func() { 1666 | BeforeEach(func() { 1667 | input := 5 1668 | soqlStruct = TestSoqlOffsetStruct{ 1669 | SelectClause: NestedStruct{}, 1670 | WhereClause: TestQueryCriteria{ 1671 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1672 | Roles: []string{"db", "dbmgmt"}, 1673 | }, 1674 | Offset: &input, 1675 | } 1676 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') OFFSET 5" 1677 | }) 1678 | 1679 | It("returns properly constructed soql query", func() { 1680 | Expect(err).ToNot(HaveOccurred()) 1681 | Expect(actualQuery).To(Equal(expectedQuery)) 1682 | }) 1683 | }) 1684 | 1685 | Context("when a struct without offset value is passed", func() { 1686 | BeforeEach(func() { 1687 | soqlStruct = TestSoqlOffsetStruct{ 1688 | SelectClause: NestedStruct{}, 1689 | WhereClause: TestQueryCriteria{ 1690 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1691 | Roles: []string{"db", "dbmgmt"}, 1692 | }, 1693 | } 1694 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt')" 1695 | }) 1696 | 1697 | It("returns properly constructed soql query", func() { 1698 | Expect(err).ToNot(HaveOccurred()) 1699 | Expect(actualQuery).To(Equal(expectedQuery)) 1700 | }) 1701 | }) 1702 | 1703 | Context("when a struct with offset value of 0 is passed", func() { 1704 | BeforeEach(func() { 1705 | input := 0 1706 | soqlStruct = TestSoqlOffsetStruct{ 1707 | SelectClause: NestedStruct{}, 1708 | WhereClause: TestQueryCriteria{ 1709 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1710 | Roles: []string{"db", "dbmgmt"}, 1711 | }, 1712 | Offset: &input, 1713 | } 1714 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') OFFSET 0" 1715 | }) 1716 | 1717 | It("returns properly constructed soql query", func() { 1718 | Expect(err).ToNot(HaveOccurred()) 1719 | Expect(actualQuery).To(Equal(expectedQuery)) 1720 | }) 1721 | }) 1722 | 1723 | Context("when a struct with invalid offset type is passed", func() { 1724 | BeforeEach(func() { 1725 | soqlStruct = TestSoqlInvalidOffsetStruct{ 1726 | SelectClause: NestedStruct{}, 1727 | WhereClause: TestQueryCriteria{ 1728 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1729 | Roles: []string{"db", "dbmgmt"}, 1730 | }, 1731 | Offset: "5", 1732 | } 1733 | }) 1734 | 1735 | It("returns error", func() { 1736 | Expect(err).To(Equal(ErrInvalidOffsetClause)) 1737 | }) 1738 | }) 1739 | 1740 | Context("when a struct with invalid offset value is passed", func() { 1741 | BeforeEach(func() { 1742 | input := -5 1743 | soqlStruct = TestSoqlOffsetStruct{ 1744 | SelectClause: NestedStruct{}, 1745 | WhereClause: TestQueryCriteria{ 1746 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1747 | Roles: []string{"db", "dbmgmt"}, 1748 | }, 1749 | Offset: &input, 1750 | } 1751 | }) 1752 | 1753 | It("returns error", func() { 1754 | Expect(err).To(Equal(ErrInvalidOffsetClause)) 1755 | }) 1756 | }) 1757 | 1758 | Context("when a struct with multiple offset values is passed", func() { 1759 | BeforeEach(func() { 1760 | input := 5 1761 | soqlStruct = TestSoqlMultipleOffsetStruct{ 1762 | SelectClause: NestedStruct{}, 1763 | WhereClause: TestQueryCriteria{ 1764 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1765 | Roles: []string{"db", "dbmgmt"}, 1766 | }, 1767 | Offset: &input, 1768 | AlsoOffset: &input, 1769 | } 1770 | }) 1771 | 1772 | It("returns error", func() { 1773 | Expect(err).To(Equal(ErrMultipleOffsetClause)) 1774 | }) 1775 | }) 1776 | 1777 | Context("when a struct with offset value and limit value is passed", func() { 1778 | BeforeEach(func() { 1779 | inputLimit := 15 1780 | inputOffset := 5 1781 | soqlStruct = TestSoqlLimitAndOffsetStruct{ 1782 | SelectClause: NestedStruct{}, 1783 | WhereClause: TestQueryCriteria{ 1784 | IncludeNamePattern: []string{"-db", "-dbmgmt"}, 1785 | Roles: []string{"db", "dbmgmt"}, 1786 | }, 1787 | Limit: &inputLimit, 1788 | Offset: &inputOffset, 1789 | } 1790 | expectedQuery = "SELECT Id,Name__c,NonNestedStruct__r.Name,NonNestedStruct__r.SomeValue__c FROM SM_Logical_Host__c WHERE (Host_Name__c LIKE '%-db%' OR Host_Name__c LIKE '%-dbmgmt%') AND Role__r.Name IN ('db','dbmgmt') LIMIT 15 OFFSET 5" 1791 | }) 1792 | 1793 | It("returns properly constructed soql query", func() { 1794 | Expect(err).ToNot(HaveOccurred()) 1795 | Expect(actualQuery).To(Equal(expectedQuery)) 1796 | }) 1797 | }) 1798 | 1799 | Context("when a struct with where clause with joiner=OR passed", func() { 1800 | BeforeEach(func() { 1801 | soqlStruct = orSOQLQuery{ 1802 | WhereClause: positionOrDeptCriteria{ 1803 | Title: "Purchasing Manager", 1804 | Department: "Accounting", 1805 | }, 1806 | } 1807 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Title = 'Purchasing Manager' OR Department = 'Accounting'" 1808 | }) 1809 | 1810 | It("returns properly constructed soql query", func() { 1811 | Expect(err).ToNot(HaveOccurred()) 1812 | Expect(actualQuery).To(Equal(expectedQuery)) 1813 | }) 1814 | }) 1815 | 1816 | Context("when a struct with where clause with joiner=or passed", func() { 1817 | BeforeEach(func() { 1818 | soqlStruct = orLowerSOQLQuery{ 1819 | WhereClause: positionOrDeptCriteria{ 1820 | Title: "Purchasing Manager", 1821 | Department: "Accounting", 1822 | }, 1823 | } 1824 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Title = 'Purchasing Manager' OR Department = 'Accounting'" 1825 | }) 1826 | 1827 | It("returns properly constructed soql query", func() { 1828 | Expect(err).ToNot(HaveOccurred()) 1829 | Expect(actualQuery).To(Equal(expectedQuery)) 1830 | }) 1831 | }) 1832 | 1833 | Context("when a struct with where clause with joiner=AND passed", func() { 1834 | BeforeEach(func() { 1835 | soqlStruct = andSOQLQuery{ 1836 | WhereClause: positionOrDeptCriteria{ 1837 | Title: "Purchasing Manager", 1838 | Department: "Accounting", 1839 | }, 1840 | } 1841 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Title = 'Purchasing Manager' AND Department = 'Accounting'" 1842 | }) 1843 | 1844 | It("returns properly constructed soql query", func() { 1845 | Expect(err).ToNot(HaveOccurred()) 1846 | Expect(actualQuery).To(Equal(expectedQuery)) 1847 | }) 1848 | }) 1849 | 1850 | Context("when a struct with where clause with joiner=ELSE (an invalid value) passed", func() { 1851 | BeforeEach(func() { 1852 | soqlStruct = invalidJoinerSOQLQuery{ 1853 | WhereClause: positionOrDeptCriteria{ 1854 | Title: "Purchasing Manager", 1855 | Department: "Accounting", 1856 | }, 1857 | } 1858 | }) 1859 | 1860 | It("returns error", func() { 1861 | Expect(err).To(Equal(ErrInvalidTag)) 1862 | }) 1863 | }) 1864 | 1865 | Context("when a struct with where clause without a joiner passed", func() { 1866 | BeforeEach(func() { 1867 | soqlStruct = noJoinerSOQLQuery{ 1868 | WhereClause: positionOrDeptCriteria{ 1869 | Title: "Purchasing Manager", 1870 | Department: "Accounting", 1871 | }, 1872 | } 1873 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Title = 'Purchasing Manager' AND Department = 'Accounting'" 1874 | }) 1875 | 1876 | It("returns properly constructed soql query", func() { 1877 | Expect(err).ToNot(HaveOccurred()) 1878 | Expect(actualQuery).To(Equal(expectedQuery)) 1879 | }) 1880 | }) 1881 | 1882 | Context("when a struct with where clause with invalid subfilters passed in", func() { 1883 | BeforeEach(func() { 1884 | soqlStruct = soqlSubQueryInvalidTypeTestStruct{ 1885 | WhereClause: invalidSubqueryCriteria{ 1886 | Position: "Purchasing Manager", 1887 | Contactable: contactableCriteria{ 1888 | EmailOK: emailCheck{ 1889 | Email: false, 1890 | EmailOptedOut: false, 1891 | }, 1892 | PhoneOK: phoneCheck{ 1893 | Phone: false, 1894 | DoNotCall: false, 1895 | }, 1896 | }, 1897 | }, 1898 | } 1899 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE (Title = 'Purchasing Manager' OR (Department = 'Accounting' AND Title LIKE '%Manager%')) AND ((Email != null AND HasOptedOutOfEmail = false) OR (Phone != null AND DoNotCall = false))" 1900 | }) 1901 | 1902 | It("returns error", func() { 1903 | Expect(err).To(Equal(ErrInvalidTag)) 1904 | }) 1905 | }) 1906 | 1907 | Context("when a struct with null pointer subquery passed in", func() { 1908 | BeforeEach(func() { 1909 | soqlStruct = soqlSubQueryPtrTestStruct{ 1910 | WhereClause: ptrSubqueryCriteria{ 1911 | Contactable: &contactableCriteria{ 1912 | EmailOK: emailCheck{ 1913 | Email: false, 1914 | EmailOptedOut: false, 1915 | }, 1916 | PhoneOK: phoneCheck{ 1917 | Phone: false, 1918 | DoNotCall: false, 1919 | }, 1920 | }, 1921 | }, 1922 | } 1923 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE ((Email != null AND HasOptedOutOfEmail = false) OR (Phone != null AND DoNotCall = false))" 1924 | }) 1925 | 1926 | It("returns properly constructed soql query", func() { 1927 | Expect(err).ToNot(HaveOccurred()) 1928 | Expect(actualQuery).To(Equal(expectedQuery)) 1929 | }) 1930 | }) 1931 | 1932 | Context("when a struct with where clause with subfilters passed in", func() { 1933 | BeforeEach(func() { 1934 | soqlStruct = soqlSubQueryTestStruct{ 1935 | WhereClause: queryCriteria{ 1936 | Position: positionCriteria{ 1937 | Title: "Purchasing Manager", 1938 | DepartmentManager: deptManagerCriteria{ 1939 | Department: "Accounting", 1940 | Title: []string{"Manager"}, 1941 | }, 1942 | }, 1943 | Contactable: contactableCriteria{ 1944 | EmailOK: emailCheck{ 1945 | Email: false, 1946 | EmailOptedOut: false, 1947 | }, 1948 | PhoneOK: phoneCheck{ 1949 | Phone: false, 1950 | DoNotCall: false, 1951 | }, 1952 | }, 1953 | }, 1954 | } 1955 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE (Title = 'Purchasing Manager' OR (Department = 'Accounting' AND Title LIKE '%Manager%')) AND ((Email != null AND HasOptedOutOfEmail = false) OR (Phone != null AND DoNotCall = false))" 1956 | }) 1957 | 1958 | It("returns properly constructed soql query", func() { 1959 | Expect(err).ToNot(HaveOccurred()) 1960 | Expect(actualQuery).To(Equal(expectedQuery)) 1961 | }) 1962 | }) 1963 | Context("when a struct with where clause with subquery and joiner=not in passed", func() { 1964 | BeforeEach(func() { 1965 | soqlStruct = soqlSubQueryInTestStruct{ 1966 | WhereClause: inSubqueryCriteria{ 1967 | Type: "Client", 1968 | NotInFraudTable: &soqlFraudStruct{ 1969 | WhereClause: fraudCriteria{ 1970 | IsFraud: true, 1971 | }, 1972 | }, 1973 | Country: "US", 1974 | }, 1975 | } 1976 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Type = 'Client' AND Name NOT IN (SELECT Name,Email,Phone FROM Fraud WHERE isFraud = true) AND Country = 'US'" 1977 | }) 1978 | 1979 | It("returns properly constructed soql query", func() { 1980 | Expect(err).ToNot(HaveOccurred()) 1981 | Expect(actualQuery).To(Equal(expectedQuery)) 1982 | }) 1983 | }) 1984 | Context("when a struct with where clause with subquery and joiner=in passed", func() { 1985 | BeforeEach(func() { 1986 | soqlStruct = soqlSubQueryInTestStruct{ 1987 | WhereClause: inSubqueryCriteria{ 1988 | Type: "Client", 1989 | InFraudTable: &soqlFraudStruct{ 1990 | WhereClause: fraudCriteria{ 1991 | IsFraud: true, 1992 | }, 1993 | }, 1994 | Country: "US", 1995 | }, 1996 | } 1997 | expectedQuery = "SELECT Name,Email,Phone FROM Contact WHERE Type = 'Client' AND Name IN (SELECT Name,Email,Phone FROM Fraud WHERE isFraud = true) AND Country = 'US'" 1998 | }) 1999 | 2000 | It("returns properly constructed soql query", func() { 2001 | Expect(err).ToNot(HaveOccurred()) 2002 | Expect(actualQuery).To(Equal(expectedQuery)) 2003 | }) 2004 | }) 2005 | Context("when a struct with where clause with subquery and joiner=in and without fieldName passed", func() { 2006 | BeforeEach(func() { 2007 | soqlStruct = soqlSubQueryInTestStruct{ 2008 | WhereClause: inSubqueryCriteria{ 2009 | Type: "Client", 2010 | InFraudTableWithoutFieldName: &soqlFraudStruct{ 2011 | WhereClause: fraudCriteria{ 2012 | IsFraud: true, 2013 | }, 2014 | }, 2015 | }, 2016 | } 2017 | }) 2018 | 2019 | It("returns ErrInvalidTag", func() { 2020 | Expect(err).To(Equal(ErrInvalidTag)) 2021 | }) 2022 | }) 2023 | }) 2024 | }) 2025 | -------------------------------------------------------------------------------- /soql_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package soql_test 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | 16 | . "github.com/forcedotcom/go-soql" 17 | ) 18 | 19 | func TestSoql(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "Soql Suite") 22 | } 23 | 24 | type TestSoqlStruct struct { 25 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 26 | WhereClause TestQueryCriteria `soql:"whereClause"` 27 | } 28 | 29 | type TestSoqlMixedDataAndOperatorStruct struct { 30 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 31 | WhereClause QueryCriteriaWithMixedDataTypesAndOperators `soql:"whereClause"` 32 | } 33 | 34 | type TestQueryCriteria struct { 35 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Host_Name__c"` 36 | Roles []string `soql:"inOperator,fieldName=Role__r.Name"` 37 | ExcludeNamePattern []string `soql:"notLikeOperator,fieldName=Host_Name__c"` 38 | AssetType string `soql:"equalsOperator,fieldName=Tech_Asset__r.Asset_Type_Asset_Type__c"` 39 | Status string `soql:"notEqualsOperator,fieldName=Status__c"` 40 | AllowNullLastDiscoveredDate *bool `soql:"nullOperator,fieldName=Last_Discovered_Date__c"` 41 | ExcludeIDs []string `soql:"notInOperator,fieldName=id"` 42 | } 43 | 44 | type NonSoqlStruct struct { 45 | Key string 46 | Value string 47 | } 48 | 49 | type NonNestedStruct struct { 50 | Name string `soql:"selectColumn,fieldName=Name"` 51 | SomeValue string `soql:"selectColumn,fieldName=SomeValue__c"` 52 | NonSoqlStruct NonSoqlStruct 53 | } 54 | 55 | type NestedStruct struct { 56 | ID string `soql:"selectColumn,fieldName=Id"` 57 | Name string `soql:"selectColumn,fieldName=Name__c"` 58 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 59 | } 60 | 61 | type TestChildStruct struct { 62 | SelectClause ChildStruct `soql:"selectClause,tableName=SM_Application_Versions__c"` 63 | WhereClause ChildQueryCriteria `soql:"whereClause"` 64 | } 65 | 66 | type TestChildWithOrderByStruct struct { 67 | SelectClause ChildStruct `soql:"selectClause,tableName=SM_Application_Versions__c"` 68 | OrderByClause []Order `soql:"orderByClause"` 69 | } 70 | 71 | type ChildStruct struct { 72 | Version string `soql:"selectColumn,fieldName=Version__c"` 73 | } 74 | 75 | type ChildQueryCriteria struct { 76 | Name string `soql:"equalsOperator,fieldName=Name__c"` 77 | } 78 | 79 | type ParentStruct struct { 80 | ID string `soql:"selectColumn,fieldName=Id"` 81 | Name string `soql:"selectColumn,fieldName=Name__c"` 82 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 83 | ChildStruct TestChildStruct `soql:"selectChild,fieldName=Application_Versions__r"` 84 | SomeNonSoqlMember string `json:"some_nonsoql_member"` 85 | } 86 | 87 | type OrderByParentStruct struct { 88 | ID string `soql:"selectColumn,fieldName=Id"` 89 | Name string `soql:"selectColumn,fieldName=Name__c"` 90 | ChildStruct TestChildWithOrderByStruct `soql:"selectChild,fieldName=Application_Versions__r"` 91 | } 92 | 93 | type DefaultFieldNameParentStruct struct { 94 | ID string `soql:"selectColumn,fieldName=Id"` 95 | Name string `soql:"selectColumn,fieldName=Name__c"` 96 | NonNestedStruct NonNestedStruct `soql:"selectColumn,fieldName=NonNestedStruct__r"` 97 | ChildStruct TestChildStruct `soql:"selectChild"` 98 | SomeNonSoqlMember string `json:"some_nonsoql_member"` 99 | } 100 | 101 | type InvalidTestChildStruct struct { 102 | WhereClause ChildQueryCriteria `soql:"whereClause"` 103 | } 104 | type InvalidParentStruct struct { 105 | ID string `soql:"selectColumn,fieldName=Id"` 106 | Name string `soql:"selectColumn,fieldName=Name__c"` 107 | ChildStruct InvalidTestChildStruct `soql:"selectChild,fieldName=Application_Versions__r"` 108 | } 109 | 110 | type InvalidSelectChildClause struct { 111 | ID string `soql:"selectColumn,fieldName=Id"` 112 | Name string `soql:"selectColumn,fieldName=Name__c"` 113 | ChildStruct int `soql:"selectChild,fieldName=Application_Versions__r"` 114 | } 115 | 116 | type ChildTagToNonStruct struct { 117 | ID string `soql:"selectColumn,fieldName=Id"` 118 | Name string `soql:"selectColumn,fieldName=Name__c"` 119 | ChildStruct string `soql:"selectChild,fieldName=Application_Versions__r"` 120 | } 121 | 122 | type MultipleSelectClause struct { 123 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 124 | ParentStruct ParentStruct `soql:"selectClause,tableName=SM_Table__c"` 125 | } 126 | 127 | type MultipleWhereClause struct { 128 | WhereClause1 ChildQueryCriteria `soql:"whereClause"` 129 | WhereClause2 ChildQueryCriteria `soql:"whereClause"` 130 | } 131 | 132 | type MultipleOrderByClause struct { 133 | OrderByClause1 []Order `soql:"orderByClause"` 134 | OrderByClause2 []Order `soql:"orderByClause"` 135 | } 136 | 137 | type OnlyWhereClause struct { 138 | WhereClause TestQueryCriteria `soql:"whereClause"` 139 | } 140 | 141 | type OnlyOrderByClause struct { 142 | OrderByClause []Order `soql:"orderByClause"` 143 | } 144 | 145 | type EmptyStruct struct { 146 | } 147 | 148 | type InvalidTagInStruct struct { 149 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 150 | WhereClause ChildQueryCriteria `soql:"whereClause"` 151 | AnotherMember NestedStruct `soql:"invalidClause,tableName=SM_Logical_Host__c"` 152 | } 153 | 154 | type DefaultFieldNameStruct struct { 155 | DefaultName string `soql:"selectColumn"` 156 | Description string `soql:"selectColumn,fieldName=Description__c"` 157 | } 158 | 159 | type DefaultTableNameStruct struct { 160 | SomeTableName NestedStruct `soql:"selectClause"` 161 | WhereClause TestQueryCriteria `soql:"whereClause"` 162 | } 163 | 164 | type DefaultFieldNameQueryCriteria struct { 165 | IncludeNamePattern []string `soql:"likeOperator,fieldName=Host_Name__c"` 166 | Role []string `soql:"inOperator"` 167 | } 168 | 169 | type QueryCriteriaWithIntegerTypes struct { 170 | NumOfCPUCores int `soql:"equalsOperator,fieldName=Num_of_CPU_Cores__c"` 171 | PhysicalCPUCount int8 `soql:"equalsOperator,fieldName=Physical_CPU_Count__c"` 172 | NumOfSuccessivePuppetRunFailures int16 `soql:"equalsOperator,fieldName=Number_Of_Successive_Puppet_Run_Failures__c"` 173 | NumOfCoolanLogFiles int32 `soql:"equalsOperator,fieldName=Num_Of_Coolan_Log_Files__c"` 174 | PvtTestFailCount int64 `soql:"equalsOperator,fieldName=Pvt_Test_Fail_Count__c"` 175 | } 176 | 177 | type QueryCriteriaWithUnsignedIntegerTypes struct { 178 | NumOfCPUCores uint `soql:"equalsOperator,fieldName=Num_of_CPU_Cores__c"` 179 | PhysicalCPUCount uint8 `soql:"equalsOperator,fieldName=Physical_CPU_Count__c"` 180 | NumOfSuccessivePuppetRunFailures uint16 `soql:"equalsOperator,fieldName=Number_Of_Successive_Puppet_Run_Failures__c"` 181 | NumOfCoolanLogFiles uint32 `soql:"equalsOperator,fieldName=Num_Of_Coolan_Log_Files__c"` 182 | PvtTestFailCount uint64 `soql:"equalsOperator,fieldName=Pvt_Test_Fail_Count__c"` 183 | } 184 | 185 | type QueryCriteriaWithFloatTypes struct { 186 | NumOfCPUCores float32 `soql:"equalsOperator,fieldName=Num_of_CPU_Cores__c"` 187 | PhysicalCPUCount float64 `soql:"equalsOperator,fieldName=Physical_CPU_Count__c"` 188 | } 189 | 190 | type QueryCriteriaWithFloatPtrTypes struct { 191 | NumOfCPUCores *float64 `soql:"equalsOperator,fieldName=Num_of_CPU_Cores__c"` 192 | PhysicalCPUCount *float64 `soql:"equalsOperator,fieldName=Physical_CPU_Count__c"` 193 | } 194 | 195 | type QueryCriteriaWithBooleanType struct { 196 | NUMAEnabled bool `soql:"equalsOperator,fieldName=NUMA_Enabled__c"` 197 | DisableAlerts bool `soql:"equalsOperator,fieldName=Disable_Alerts__c"` 198 | } 199 | 200 | type QueryCriteriaWithNoSoqlTag struct { 201 | NUMAEnabled bool `soql:"equalsOperator,fieldName=NUMA_Enabled__c"` 202 | DisableAlerts bool `json:"random_value"` 203 | } 204 | 205 | type QueryCriteriaWithBooleanPtrType struct { 206 | NUMAEnabled *bool `soql:"equalsOperator,fieldName=NUMA_Enabled__c"` 207 | DisableAlerts *bool `soql:"equalsOperator,fieldName=Disable_Alerts__c"` 208 | } 209 | 210 | type QueryCriteriaWithDateTimeType struct { 211 | CreatedDate time.Time `soql:"equalsOperator,fieldName=CreatedDate"` 212 | } 213 | 214 | type QueryCriteriaWithPtrDateTimeType struct { 215 | CreatedDate *time.Time `soql:"equalsOperator,fieldName=CreatedDate"` 216 | ResolvedDate *time.Time `soql:"equalsOperator,fieldName=ResolvedDate"` 217 | } 218 | 219 | type QueryCriteriaNumericComparisonOperators struct { 220 | NumOfCPUCores int `soql:"greaterThanOperator,fieldName=Num_of_CPU_Cores__c"` 221 | PhysicalCPUCount int `soql:"lessThanOperator,fieldName=Physical_CPU_Count__c"` 222 | NumOfSuccessivePuppetRunFailures int `soql:"greaterThanOrEqualsToOperator,fieldName=Number_Of_Successive_Puppet_Run_Failures__c"` 223 | NumOfCoolanLogFiles int `soql:"lessThanOrEqualsToOperator,fieldName=Num_Of_Coolan_Log_Files__c"` 224 | } 225 | 226 | type QueryCriteriaDateLiteralsOperatorsInt struct { 227 | CreatedDate int `soql:"greaterNextNDaysOperator,fieldName=CreatedDate"` 228 | ClosedDate int `soql:"lessNextNDaysOperator,fieldName=ClosedDate"` 229 | } 230 | 231 | type QueryCriteriaDateLiteralsOperatorsUint struct { 232 | CreatedDate uint `soql:"greaterNextNDaysOperator,fieldName=CreatedDate"` 233 | ClosedDate uint `soql:"lessNextNDaysOperator,fieldName=ClosedDate"` 234 | } 235 | 236 | type QueryCriteriaDateLiteralsOperatorsPtr struct { 237 | CreatedDate *int `soql:"greaterNextNDaysOperator,fieldName=CreatedDate"` 238 | OtherDate *int `soql:"equalsNextNDaysOperator,fieldName=OtherDate"` 239 | ClosedDate *int `soql:"lessNextNDaysOperator,fieldName=ClosedDate"` 240 | ScheduledDate *int `soql:"lessOrEqualNextNDaysOperator,fieldName=ScheduledDate"` 241 | DeliveredDate *int `soql:"greaterOrEqualNextNDaysOperator,fieldName=DeliveredDate"` 242 | } 243 | 244 | type QueryCriteriaDateLastNDaysLiteralsOperatorsPtr struct { 245 | CreatedDate *int `soql:"greaterLastNDaysOperator,fieldName=CreatedDate"` 246 | OtherDate *int `soql:"equalsLastNDaysOperator,fieldName=OtherDate"` 247 | ClosedDate *int `soql:"lessLastNDaysOperator,fieldName=ClosedDate"` 248 | ScheduledDate *int `soql:"lessOrEqualLastNDaysOperator,fieldName=ScheduledDate"` 249 | DeliveredDate *int `soql:"greaterOrEqualLastNDaysOperator,fieldName=DeliveredDate"` 250 | } 251 | 252 | var TestDateFormat = "2006-01-02" 253 | 254 | type QueryCriteriaWithMixedDataTypesAndOperators struct { 255 | BIOSType string `soql:"equalsOperator,fieldName=BIOS_Type__c"` 256 | NumOfCPUCores int `soql:"greaterThanOperator,fieldName=Num_of_CPU_Cores__c"` 257 | NUMAEnabled bool `soql:"equalsOperator,fieldName=NUMA_Enabled__c"` 258 | PvtTestFailCount int64 `soql:"lessThanOrEqualsToOperator,fieldName=Pvt_Test_Fail_Count__c"` 259 | PhysicalCPUCount uint8 `soql:"greaterThanOrEqualsToOperator,fieldName=Physical_CPU_Count__c"` 260 | CreatedDate time.Time `soql:"equalsOperator,fieldName=CreatedDate,format=2006-01-02"` 261 | UpdatedDate *time.Time `soql:"equalsOperator,fieldName=UpdatedDate,format=2006-01-02"` 262 | DisableAlerts bool `soql:"equalsOperator,fieldName=Disable_Alerts__c"` 263 | AllocationLatency float64 `soql:"lessThanOperator,fieldName=Allocation_Latency__c"` 264 | MajorOSVersion string `soql:"equalsOperator,fieldName=Major_OS_Version__c"` 265 | NumOfSuccessivePuppetRunFailures uint32 `soql:"equalsOperator,fieldName=Number_Of_Successive_Puppet_Run_Failures__c"` 266 | LastRestart time.Time `soql:"greaterThanOperator,fieldName=Last_Restart__c"` 267 | Memory *float64 `soql:"equalsOperator,fieldName=Memory__c"` 268 | NumHardDrives *int `soql:"equalsOperator,fieldName=NumHardDrives__c"` 269 | ClosedDate int `soql:"greaterNextNDaysOperator,fieldName=ClosedDate"` 270 | } 271 | 272 | type InvalidSelectClause struct { 273 | SelectClause string `soql:"selectClause,tableName=SM_Logical_Host__c"` 274 | } 275 | 276 | type TestSoqlOrderByStruct struct { 277 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 278 | OrderByClause []Order `soql:"orderByClause"` 279 | } 280 | 281 | type TestSoqlChildRelationOrderByStruct struct { 282 | SelectClause OrderByParentStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 283 | OrderByClause []Order `soql:"orderByClause"` 284 | } 285 | 286 | type TestSoqlLimitStruct struct { 287 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 288 | WhereClause TestQueryCriteria `soql:"whereClause"` 289 | Limit *int `soql:"limitClause"` 290 | } 291 | 292 | type TestSoqlInvalidLimitStruct struct { 293 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 294 | WhereClause TestQueryCriteria `soql:"whereClause"` 295 | Limit string `soql:"limitClause"` 296 | } 297 | 298 | type TestSoqlMultipleLimitStruct struct { 299 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 300 | WhereClause TestQueryCriteria `soql:"whereClause"` 301 | Limit *int `soql:"limitClause"` 302 | AlsoLimit *int `soql:"limitClause"` 303 | } 304 | 305 | type ParentLimitStruct struct { 306 | ID string `soql:"selectColumn,fieldName=Id"` 307 | Name string `soql:"selectColumn,fieldName=Name__c"` 308 | ChildStruct ChildLimitStruct `soql:"selectChild,fieldName=Application_Versions__r"` 309 | } 310 | 311 | type ChildLimitStruct struct { 312 | SelectClause TestChildLimitSelect `soql:"selectClause,tableName=Application_Versions__c"` 313 | Limit *int `soql:"limitClause"` 314 | } 315 | 316 | type TestChildLimitSelect struct { 317 | ID string `soql:"selectColumn,fieldName=Id"` 318 | Version string `soql:"selectColumn,fieldName=Version__c"` 319 | } 320 | 321 | type TestSoqlChildRelationLimitStruct struct { 322 | SelectClause ParentLimitStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 323 | Limit *int `soql:"limitClause"` 324 | } 325 | 326 | type TestSoqlOffsetStruct struct { 327 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 328 | WhereClause TestQueryCriteria `soql:"whereClause"` 329 | Offset *int `soql:"offsetClause"` 330 | } 331 | 332 | type TestSoqlInvalidOffsetStruct struct { 333 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 334 | WhereClause TestQueryCriteria `soql:"whereClause"` 335 | Offset string `soql:"offsetClause"` 336 | } 337 | 338 | type TestSoqlMultipleOffsetStruct struct { 339 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 340 | WhereClause TestQueryCriteria `soql:"whereClause"` 341 | Offset *int `soql:"offsetClause"` 342 | AlsoOffset *int `soql:"offsetClause"` 343 | } 344 | 345 | type TestSoqlLimitAndOffsetStruct struct { 346 | SelectClause NestedStruct `soql:"selectClause,tableName=SM_Logical_Host__c"` 347 | WhereClause TestQueryCriteria `soql:"whereClause"` 348 | Limit *int `soql:"limitClause"` 349 | Offset *int `soql:"offsetClause"` 350 | } 351 | 352 | // setups for OR and subfilter tests 353 | 354 | type orSOQLQuery struct { 355 | SelectClause contact `soql:"selectClause,tableName=Contact"` 356 | WhereClause positionOrDeptCriteria `soql:"whereClause,joiner=OR"` 357 | } 358 | 359 | type orLowerSOQLQuery struct { 360 | SelectClause contact `soql:"selectClause,tableName=Contact"` 361 | WhereClause positionOrDeptCriteria `soql:"whereClause,joiner=or"` 362 | } 363 | 364 | type andSOQLQuery struct { 365 | SelectClause contact `soql:"selectClause,tableName=Contact"` 366 | WhereClause positionOrDeptCriteria `soql:"whereClause,joiner=AND"` 367 | } 368 | 369 | type invalidJoinerSOQLQuery struct { 370 | SelectClause contact `soql:"selectClause,tableName=Contact"` 371 | WhereClause positionOrDeptCriteria `soql:"whereClause,joiner=not-a-real-value"` 372 | } 373 | 374 | type noJoinerSOQLQuery struct { 375 | SelectClause contact `soql:"selectClause,tableName=Contact"` 376 | WhereClause positionOrDeptCriteria `soql:"whereClause"` 377 | } 378 | 379 | type positionOrDeptCriteria struct { 380 | Title string `soql:"equalsOperator,fieldName=Title"` 381 | Department string `soql:"equalsOperator,fieldName=Department"` 382 | } 383 | 384 | type contact struct { 385 | Name string `soql:"selectColumn,fieldName=Name" json:"Name"` 386 | Email string `soql:"selectColumn,fieldName=Email" json:"Email"` 387 | Phone string `soql:"selectColumn,fieldName=Phone" json:"Phone"` 388 | } 389 | 390 | type queryCriteria struct { 391 | Position positionCriteria `soql:"subquery,joiner=OR"` 392 | Contactable contactableCriteria `soql:"subquery,joiner=OR"` 393 | } 394 | 395 | type contactableCriteria struct { 396 | EmailOK emailCheck `soql:"subquery,joiner=and"` 397 | PhoneOK phoneCheck `soql:"subquery,joiner=and"` 398 | } 399 | 400 | type emailCheck struct { 401 | Email bool `soql:"nullOperator,fieldName=Email"` 402 | EmailOptedOut bool `soql:"equalsOperator,fieldName=HasOptedOutOfEmail"` 403 | } 404 | 405 | type phoneCheck struct { 406 | Phone bool `soql:"nullOperator,fieldName=Phone"` 407 | DoNotCall bool `soql:"equalsOperator,fieldName=DoNotCall"` 408 | } 409 | 410 | type positionCriteria struct { 411 | Title string `soql:"equalsOperator,fieldName=Title"` 412 | DepartmentManager deptManagerCriteria `soql:"subquery"` 413 | } 414 | 415 | type deptManagerCriteria struct { 416 | Department string `soql:"equalsOperator,fieldName=Department"` 417 | Title []string `soql:"likeOperator,fieldName=Title"` 418 | } 419 | 420 | type soqlSubQueryTestStruct struct { 421 | SelectClause contact `soql:"selectClause,tableName=Contact"` 422 | WhereClause queryCriteria `soql:"whereClause"` 423 | } 424 | 425 | type soqlSubQueryInvalidTypeTestStruct struct { 426 | SelectClause contact `soql:"selectClause,tableName=Contact"` 427 | WhereClause invalidSubqueryCriteria `soql:"whereClause"` 428 | } 429 | 430 | type invalidSubqueryCriteria struct { 431 | Position string `soql:"subquery,joiner=OR"` 432 | Contactable contactableCriteria `soql:"subquery,joiner=OR"` 433 | } 434 | 435 | type soqlSubQueryPtrTestStruct struct { 436 | SelectClause contact `soql:"selectClause,tableName=Contact"` 437 | WhereClause ptrSubqueryCriteria `soql:"whereClause"` 438 | } 439 | 440 | type ptrSubqueryCriteria struct { 441 | Position *positionCriteria `soql:"subquery,joiner=OR"` 442 | Contactable *contactableCriteria `soql:"subquery,joiner=OR"` 443 | } 444 | 445 | type soqlSubQueryInTestStruct struct { 446 | SelectClause contact `soql:"selectClause,tableName=Contact"` 447 | WhereClause inSubqueryCriteria `soql:"whereClause"` 448 | } 449 | 450 | type inSubqueryCriteria struct { 451 | Type string `soql:"equalsOperator,fieldName=Type"` 452 | NotInFraudTable *soqlFraudStruct `soql:"subquery,joiner=NOT IN,fieldName=Name"` 453 | InFraudTable *soqlFraudStruct `soql:"subquery,joiner=IN,fieldName=Name"` 454 | InFraudTableWithoutFieldName *soqlFraudStruct `soql:"subquery,joiner=IN"` 455 | Country string `soql:"equalsOperator,fieldName=Country"` 456 | } 457 | 458 | type fraudCriteria struct { 459 | IsFraud bool `soql:"equalsOperator,fieldName=isFraud"` 460 | } 461 | 462 | type soqlFraudStruct struct { 463 | SelectClause contact `soql:"selectClause,tableName=Fraud"` 464 | WhereClause fraudCriteria `soql:"whereClause"` 465 | } 466 | --------------------------------------------------------------------------------