├── .formatter.exs ├── .github └── workflows │ ├── ci.yml │ └── conformance.yml ├── .gitignore ├── .iex.exs ├── CHANGELOG.md ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── README.md ├── bench.exs ├── benchmarks ├── v0.1.0.md └── v0.2.0.md ├── lib ├── adapters │ ├── ansi.ex │ ├── mysql.ex │ ├── postgres.ex │ └── tds.ex ├── bnf.ex ├── formatter.ex ├── lexer.ex ├── mix │ └── tasks │ │ ├── sql.gen.parser.ex │ │ └── sql.gen.test.ex ├── parser.ex ├── sql.ex ├── string.ex └── token.ex ├── mix.exs ├── mix.lock └── test ├── adapters ├── ansi_test.exs ├── mysql_test.exs ├── postgres_test.exs └── tds_test.exs ├── bnf_test.exs ├── formatter_test.exs ├── sql_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | # Used by "mix format" 5 | [ 6 | plugins: [SQL.MixFormatter], 7 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 8 | ] 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | jobs: 5 | test: 6 | runs-on: ubuntu-22.04 7 | name: OTP 27 / Elixir 1.18 8 | services: 9 | postgres: 10 | image: postgres:latest 11 | env: 12 | POSTGRES_PASSWORD: postgres 13 | ports: ["5432:5432"] 14 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: erlef/setup-beam@v1 18 | with: 19 | otp-version: 27 20 | elixir-version: 1.18 21 | - uses: actions/cache@v4 22 | with: 23 | path: | 24 | deps 25 | _build 26 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-mix- 29 | - run: mix deps.get && mix test 30 | -------------------------------------------------------------------------------- /.github/workflows/conformance.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | jobs: 5 | test: 6 | runs-on: ubuntu-22.04 7 | name: OTP 27 / Elixir 1.18 8 | services: 9 | postgres: 10 | image: postgres:latest 11 | env: 12 | POSTGRES_PASSWORD: postgres 13 | ports: ["5432:5432"] 14 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 15 | steps: 16 | - name: Checkout ${{github.repository}} 17 | uses: actions/checkout@v4 18 | - name: Checkout sqltest 19 | uses: actions/checkout@v4 20 | with: 21 | path: sqltest 22 | repository: elliotchance/sqltest 23 | - uses: erlef/setup-beam@v1 24 | with: 25 | otp-version: 27 26 | elixir-version: 1.18 27 | - uses: actions/cache@v4 28 | with: 29 | path: | 30 | deps 31 | _build 32 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-mix- 35 | - run: mix deps.get && mix sql.gen.test sqltest/standards/2016 && mix test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | # The directory Mix will write compiled artifacts to. 5 | /_build/ 6 | 7 | # If you run "mix test --cover", coverage assets end up here. 8 | /cover/ 9 | 10 | # The directory Mix downloads your dependencies sources to. 11 | /deps/ 12 | 13 | # Where third-party dependencies like ExDoc output generated docs. 14 | /doc/ 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ecto_sigil-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.iex.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Repo do 5 | use Ecto.Repo, otp_app: :sql, adapter: Ecto.Adapters.Postgres 6 | end 7 | Application.put_env(:sql, :ecto_repos, [SQL.Repo]) 8 | Application.put_env(:sql, SQL.Repo, username: "postgres", password: "postgres", hostname: "localhost", database: "sql_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: 10) 9 | Mix.Tasks.Ecto.Create.run(["-r", "SQL.Repo"]) 10 | SQL.Repo.start_link() 11 | import SQL 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Changelog 7 | 8 | ## v0.2.0 (2025-05-04) 9 | 10 | ### Enhancement 11 | - SQL 2016 conformance [#6](https://github.com/elixir-dbvisor/sql/pull/6). 12 | - Lexer and Parser generated from the [SQL 2023 BNF](https://standards.iso.org/iso-iec/9075/-2/ed-6/en/) [#5](https://github.com/elixir-dbvisor/sql/pull/5). 13 | - Added SQL.Token behaviour used to implement adapters [#5](https://github.com/elixir-dbvisor/sql/pull/5). 14 | - ANSI adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5). 15 | - MySQL adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5). 16 | - PostgreSQL adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5). 17 | - TDS adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5). 18 | - Improve SQL generation with 57-344x compared to Ecto [#7](https://github.com/elixir-dbvisor/sql/pull/7) [#4](https://github.com/elixir-dbvisor/sql/pull/4). 19 | - Ensure inspect follows the standard [representation](https://hexdocs.pm/elixir/Inspect.html#module-inspect-representation) [#4](https://github.com/elixir-dbvisor/sql/pull/4). 20 | - Ensure storage is setup when running benchmarks [#5](https://github.com/elixir-dbvisor/sql/pull/5). 21 | 22 | ### Deprecation 23 | - token_to_sql/2 is deprecated in favor of SQL.Token behaviour token_to_string/2 [#11](https://github.com/elixir-dbvisor/sql/pull/11). 24 | 25 | ## v0.1.0 (2025-03-01) 26 | 27 | Initial release. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # SQL 7 | 8 | 9 | 10 | Brings an extensible SQL parser and sigil to Elixir, confidently write SQL with automatic parameterized queries. 11 | 12 | - Lower the barrier for DBAs to contribute in your codebase, without having to translate SQL to Ecto.Query. 13 | - Composable queries, no need for you to remember, when to start with select or from. 14 | - Interpolation-al queries, don't fiddle with fragments and `?`. 15 | 16 | ## Examples 17 | 18 | ```elixir 19 | iex(1)> email = "john@example.com" 20 | "john@example.com" 21 | iex(2)> ~SQL[from users] |> ~SQL[where email = {{email}}] |> ~SQL"select id, email" 22 | ~SQL""" 23 | where email = {{email}} from users select id, email 24 | """ 25 | iex(3)> sql = ~SQL[from users where email = {{email}} select id, email] 26 | ~SQL""" 27 | from users where email = {{email}} select id, email 28 | """ 29 | iex(4)> to_sql(sql) 30 | {"select id, email from users where email = ?", ["john@example.com"]} 31 | iex(5)> to_string(sql) 32 | "select id, email from users where email = ?" 33 | iex(6)> inspect(sql) 34 | "~SQL\"\"\"\nfrom users where email = {{email}} select id, email\n\"\"\"" 35 | ``` 36 | 37 | ### Leverage the Enumerable protocol in your repository 38 | 39 | ```elixir 40 | defmodule MyApp.Repo do 41 | use Ecto.Repo, otp_app: :myapp, adapter: Ecto.Adapters.Postgres 42 | use SQL, adapter: SQL.Adapters.Postgres 43 | 44 | defimpl Enumerable, for: SQL do 45 | def count(_enumerable) do 46 | {:error, __MODULE__} 47 | end 48 | def member?(_enumerable, _element) do 49 | {:error, __MODULE__} 50 | end 51 | def reduce(%SQL{} = enumerable, _acc, _fun) do 52 | {sql, params} = SQL.to_sql(enumerable) 53 | result = __MODULE__.query!(sql, params) 54 | {:done, Enum.map(result.rows, &Map.new(Enum.zip(result.columns, &1)))} 55 | end 56 | def slice(_enumerable) do 57 | {:error, __MODULE__} 58 | end 59 | end 60 | end 61 | 62 | iex(1)> Enum.map(~SQL[from users select *], &IO.inspect/1) 63 | %{"id" => 1, "email" => "john@example.com"} 64 | %{"id" => 2, "email" => "jane@example.com"} 65 | [%{"id" => 1, "email" => "john@example.com"}, %{"id" => 2, "email" => "jane@example.com"}] 66 | ``` 67 | 68 | ## Benchmark 69 | You can find benchmark results [here](https://github.com/elixir-dbvisor/sql/benchmarks) or run `mix sql.bench` 70 | 71 | ## Installation 72 | 73 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 74 | by adding `sql` to your list of dependencies in `mix.exs`: 75 | 76 | ```elixir 77 | def deps do 78 | [ 79 | {:sql, "~> 0.2.0"} 80 | ] 81 | end 82 | ``` 83 | 84 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 85 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 86 | be found at . 87 | -------------------------------------------------------------------------------- /bench.exs: -------------------------------------------------------------------------------- 1 | import SQL 2 | import Ecto.Query 3 | defmodule SQL.Repo do 4 | use Ecto.Repo, otp_app: :sql, adapter: Ecto.Adapters.Postgres 5 | end 6 | Application.put_env(:sql, :ecto_repos, [SQL.Repo]) 7 | Application.put_env(:sql, SQL.Repo, username: "postgres", password: "postgres", hostname: "localhost", database: "sql_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: 10) 8 | SQL.Repo.__adapter__().storage_up(SQL.Repo.config()) 9 | SQL.Repo.start_link() 10 | range = 1..10_000 11 | Benchee.run( 12 | %{ 13 | "to_string" => fn -> for _ <- range, do: to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) end, 14 | "to_sql" => fn -> for _ <- range, do: SQL.to_sql(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) end, 15 | "inspect" => fn -> for _ <- range, do: inspect(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) end, 16 | "ecto" => fn -> for _ <- range, do: SQL.Repo.to_sql(:all, "temp" |> recursive_ctes(true) |> with_cte("temp", as: ^union_all(select("temp", [t], %{n: 0, fact: 1}), ^where(select("temp", [t], [t.n+1, t.n+1*t.fact]), [t], t.n < 9))) |> select([t], [t.n])) end 17 | }, 18 | time: 10, 19 | memory_time: 2 20 | ) 21 | -------------------------------------------------------------------------------- /benchmarks/v0.1.0.md: -------------------------------------------------------------------------------- 1 | ➜ sql git:(benchmark) ✗ mix bench 2 | Operating System: macOS 3 | CPU Information: Apple M1 Max 4 | Number of Available Cores: 10 5 | Available memory: 64 GB 6 | Elixir 1.18.0 7 | Erlang 27.2 8 | JIT enabled: true 9 | 10 | Benchmark suite executing with the following configuration: 11 | warmup: 2 s 12 | time: 10 s 13 | memory time: 2 s 14 | reduction time: 0 ns 15 | parallel: 1 16 | inputs: none specified 17 | Estimated total run time: 42 s 18 | 19 | Benchmarking inspect ... 20 | Benchmarking to_sql ... 21 | Benchmarking to_string ... 22 | Calculating statistics... 23 | Formatting results... 24 | 25 | Name ips average deviation median 99th % 26 | to_string 1.40 713.82 ms ±2.01% 711.52 ms 742.02 ms 27 | to_sql 1.37 729.90 ms ±1.90% 733.32 ms 749.88 ms 28 | inspect 1.35 742.32 ms ±1.93% 745.09 ms 764.21 ms 29 | 30 | Comparison: 31 | to_string 1.40 32 | to_sql 1.37 - 1.02x slower +16.08 ms 33 | inspect 1.35 - 1.04x slower +28.50 ms 34 | 35 | Memory usage statistics: 36 | 37 | Name Memory usage 38 | to_string 1.02 GB 39 | to_sql 1.02 GB - 1.00x memory usage -0.00025 GB 40 | inspect 1.02 GB - 1.01x memory usage +0.00515 GB 41 | 42 | **All measurements for memory usage were the same** 43 | -------------------------------------------------------------------------------- /benchmarks/v0.2.0.md: -------------------------------------------------------------------------------- 1 | ➜ sql git:(main) ✗ mix sql.bench 2 | Operating System: macOS 3 | CPU Information: Apple M1 Max 4 | Number of Available Cores: 10 5 | Available memory: 64 GB 6 | Elixir 1.18.0 7 | Erlang 27.2 8 | JIT enabled: true 9 | 10 | Benchmark suite executing with the following configuration: 11 | warmup: 2 s 12 | time: 10 s 13 | memory time: 2 s 14 | reduction time: 0 ns 15 | parallel: 1 16 | inputs: none specified 17 | Estimated total run time: 56 s 18 | 19 | Benchmarking ecto ... 20 | Benchmarking inspect ... 21 | Benchmarking to_sql ... 22 | Benchmarking to_string ... 23 | Calculating statistics... 24 | Formatting results... 25 | 26 | Name ips average deviation median 99th % 27 | to_sql 2.66 K 0.38 ms ±2.66% 0.37 ms 0.42 ms 28 | to_string 1.75 K 0.57 ms ±31.77% 0.47 ms 1.10 ms 29 | inspect 0.25 K 3.94 ms ±4.64% 3.91 ms 4.35 ms 30 | ecto 0.00772 K 129.54 ms ±1.20% 129.14 ms 132.51 ms 31 | 32 | Comparison: 33 | to_sql 2.66 K 34 | to_string 1.75 K - 1.52x slower +0.196 ms 35 | inspect 0.25 K - 10.47x slower +3.56 ms 36 | ecto 0.00772 K - 344.27x slower +129.16 ms 37 | 38 | Memory usage statistics: 39 | 40 | Name Memory usage 41 | to_sql 0.69 MB 42 | to_string 0.46 MB - 0.67x memory usage -0.22888 MB 43 | inspect 6.71 MB - 9.78x memory usage +6.03 MB 44 | ecto 179.35 MB - 261.19x memory usage +178.67 MB 45 | 46 | **All measurements for memory usage were the same** 47 | 48 | ➜ sql git:(optimize-sql-generation) ✗ mix sql.bench 49 | Operating System: macOS 50 | CPU Information: Apple M1 Max 51 | Number of Available Cores: 10 52 | Available memory: 64 GB 53 | Elixir 1.18.0 54 | Erlang 27.2 55 | JIT enabled: true 56 | 57 | Benchmark suite executing with the following configuration: 58 | warmup: 2 s 59 | time: 10 s 60 | memory time: 2 s 61 | reduction time: 0 ns 62 | parallel: 1 63 | inputs: none specified 64 | Estimated total run time: 56 s 65 | 66 | Benchmarking ecto ... 67 | Benchmarking inspect ... 68 | Benchmarking to_sql ... 69 | Benchmarking to_string ... 70 | Calculating statistics... 71 | Formatting results... 72 | 73 | Name ips average deviation median 99th % 74 | to_sql 394.70 2.53 ms ±3.88% 2.51 ms 2.82 ms 75 | to_string 391.12 2.56 ms ±1.82% 2.55 ms 2.70 ms 76 | inspect 132.40 7.55 ms ±6.37% 7.69 ms 8.44 ms 77 | ecto 6.89 145.10 ms ±1.22% 144.41 ms 149.21 ms 78 | 79 | Comparison: 80 | to_sql 394.70 81 | to_string 391.12 - 1.01x slower +0.0232 ms 82 | inspect 132.40 - 2.98x slower +5.02 ms 83 | ecto 6.89 - 57.27x slower +142.56 ms 84 | 85 | Memory usage statistics: 86 | 87 | Name Memory usage 88 | to_sql 4.35 MB 89 | to_string 4.12 MB - 0.95x memory usage -0.22913 MB 90 | inspect 10.37 MB - 2.39x memory usage +6.03 MB 91 | ecto 202.87 MB - 46.67x memory usage +198.52 MB 92 | 93 | **All measurements for memory usage were the same** 94 | -------------------------------------------------------------------------------- /lib/adapters/ansi.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.ANSI do 5 | @moduledoc """ 6 | A SQL adapter for [ANSI](https://blog.ansi.org/sql-standard-iso-iec-9075-2023-ansi-x3-135/). 7 | """ 8 | @moduledoc since: "0.2.0" 9 | 10 | use SQL.Token 11 | 12 | @doc false 13 | def token_to_string(value, mod \\ __MODULE__) 14 | def token_to_string(value, mod) when is_struct(value) do 15 | to_string(%{value | module: mod}) 16 | end 17 | def token_to_string({tag, _, [{:parens, _, _} = value]}, mod) when tag in ~w[integer float update]a do 18 | "#{mod.token_to_string(tag)}#{mod.token_to_string(value)}" 19 | end 20 | def token_to_string({tag, _, value}, _mod) when tag in ~w[ident integer float]a do 21 | "#{value}" 22 | end 23 | def token_to_string({tag, _}, mod) do 24 | mod.token_to_string(tag) 25 | end 26 | def token_to_string({:comment, _, value}, _mod) do 27 | "-- #{value}" 28 | end 29 | def token_to_string({:comments, _, value}, _mod) do 30 | "\\* #{value} *\\" 31 | end 32 | def token_to_string({:double_quote, _, value}, _mod) do 33 | "\"#{value}\"" 34 | end 35 | def token_to_string({:quote, _, value}, _mod) do 36 | "'#{value}'" 37 | end 38 | def token_to_string({:parens, _, value}, mod) do 39 | "(#{mod.token_to_string(value)})" 40 | end 41 | def token_to_string({:bracket, _, value}, mod) do 42 | "[#{mod.token_to_string(value)}]" 43 | end 44 | def token_to_string({:colon, _, value}, mod) do 45 | "; #{mod.token_to_string(value)}" 46 | end 47 | def token_to_string({:comma, _, value}, mod) do 48 | ", #{mod.token_to_string(value)}" 49 | end 50 | def token_to_string({tag, _, []}, mod) do 51 | mod.token_to_string(tag) 52 | end 53 | def token_to_string({tag, _, [[_ | _] = left, right]}, mod) when tag in ~w[join]a do 54 | "#{mod.token_to_string(left)} #{mod.token_to_string(tag)} #{mod.token_to_string(right)}" 55 | end 56 | def token_to_string({tag, _, [{:with = t, _, [left, right]}]}, mod) when tag in ~w[to]a do 57 | "#{mod.token_to_string(tag)} #{mod.token_to_string(left)} #{mod.token_to_string(t)} #{mod.token_to_string(right)}" 58 | end 59 | def token_to_string({tag, _, value}, mod) when tag in ~w[select from fetch limit where order offset group having with join by distinct create type drop insert alter table add into delete update start grant revoke set declare open close commit rollback references recursive]a do 60 | "#{mod.token_to_string(tag)} #{mod.token_to_string(value)}" 61 | end 62 | def token_to_string({:on = tag, _, [source, as, value]}, mod) do 63 | "#{mod.token_to_string(source)} #{mod.token_to_string(as)} #{mod.token_to_string(tag)} #{mod.token_to_string(value)}" 64 | end 65 | def token_to_string({tag, _, [left, [{:all = t, _, right}]]}, mod) when tag in ~w[union except intersect]a do 66 | "#{mod.token_to_string(left)} #{mod.token_to_string(tag)} #{mod.token_to_string(t)} #{mod.token_to_string(right)}" 67 | end 68 | def token_to_string({:between = tag, _, [{:not = t, _, right}, left]}, mod) do 69 | "#{mod.token_to_string(right)} #{mod.token_to_string(t)} #{mod.token_to_string(tag)} #{mod.token_to_string(left)}" 70 | end 71 | def token_to_string({tag, _, [left, right]}, mod) when tag in ~w[:: [\] <> <= >= != || + - ^ * / % < > = like ilike as union except intersect between and or on is not in cursor for to]a do 72 | "#{mod.token_to_string(left)} #{mod.token_to_string(tag)} #{mod.token_to_string(right)}" 73 | end 74 | def token_to_string({tag, _, [{:parens, _, _} = value]}, mod) when tag not in ~w[in on]a do 75 | "#{mod.token_to_string(tag)}#{mod.token_to_string(value)}" 76 | end 77 | def token_to_string({tag, _, values}, mod) when tag in ~w[not all between symmetric absolute relative forward backward on in for without]a do 78 | "#{mod.token_to_string(tag)} #{mod.token_to_string(values)}" 79 | end 80 | def token_to_string({tag, _, [left, right]}, mod) when tag in ~w[.]a do 81 | "#{mod.token_to_string(left)}.#{mod.token_to_string(right)}" 82 | end 83 | def token_to_string({tag, _, [left]}, mod) when tag in ~w[not]a do 84 | "#{mod.token_to_string(left)} #{mod.token_to_string(tag)}" 85 | end 86 | def token_to_string({tag, _, [left]}, mod) when tag in ~w[asc desc isnull notnull]a do 87 | "#{mod.token_to_string(left)} #{mod.token_to_string(tag)}" 88 | end 89 | def token_to_string({:binding, _, [idx]}, _mod) when is_integer(idx) do 90 | "?" 91 | end 92 | def token_to_string({:binding, _, value}, _mod) do 93 | "{{#{value}}}" 94 | end 95 | def token_to_string(:asterisk, _mod) do 96 | "*" 97 | end 98 | def token_to_string(value, _mod) when is_atom(value) do 99 | "#{value}" 100 | end 101 | def token_to_string(value, _mod) when is_binary(value) do 102 | "'#{value}'" 103 | end 104 | def token_to_string(values, mod) when is_list(values) do 105 | values 106 | |> Enum.reduce([], fn 107 | token, [] = acc -> [acc | mod.token_to_string(token)] 108 | {:comma, _, _} = token, acc -> [acc | mod.token_to_string(token)] 109 | token, acc -> [acc, " " | mod.token_to_string(token)] 110 | end) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/adapters/mysql.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.MySQL do 5 | @moduledoc """ 6 | A SQL adapter for [MySQL](https://www.mysql.com). 7 | """ 8 | @moduledoc since: "0.2.0" 9 | 10 | use SQL.Token 11 | 12 | @doc false 13 | def token_to_string(value, mod \\ __MODULE__) 14 | def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod) 15 | end 16 | -------------------------------------------------------------------------------- /lib/adapters/postgres.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.Postgres do 5 | @moduledoc """ 6 | A SQL adapter for [PostgreSQL](https://www.postgresql.org). 7 | """ 8 | @moduledoc since: "0.2.0" 9 | 10 | use SQL.Token 11 | 12 | @doc false 13 | def token_to_string(value, mod \\ __MODULE__) 14 | def token_to_string({:not, _, [left, {:in, _, [{:binding, _, _} = right]}]}, mod), do: "#{mod.token_to_string(left)} != ANY(#{mod.token_to_string(right)})" 15 | def token_to_string({:in, _, [left, {:binding, _, _} = right]}, mod), do: "#{mod.token_to_string(left)} = ANY(#{mod.token_to_string(right)})" 16 | def token_to_string({:binding, _, [idx]}, _mod) when is_integer(idx), do: "$#{idx}" 17 | def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod) 18 | end 19 | -------------------------------------------------------------------------------- /lib/adapters/tds.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.TDS do 5 | @moduledoc """ 6 | A SQL adapter for [TDS](https://www.microsoft.com/en-ca/sql-server). 7 | """ 8 | @moduledoc since: "0.2.0" 9 | 10 | use SQL.Token 11 | 12 | @doc false 13 | def token_to_string(value, mod \\ __MODULE__) 14 | def token_to_string({:binding, _, [idx]}, _mod) when is_integer(idx), do: "@#{idx}" 15 | def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod) 16 | end 17 | -------------------------------------------------------------------------------- /lib/bnf.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | # https://standards.iso.org/iso-iec/9075/-2/ed-6/en/ 4 | # https://standards.iso.org/ittf/PubliclyAvailableStandards/ISO_IEC_9075-1_2023_ed_6_-_id_76583_Publication_PDF_(en).zip 5 | # 0. \w(?![^<]*>) 6 | # 1. <[^>]*>\.{3} repeat non-terminal rule 7 | # 2. ({.+}...) repeat group 8 | # 3. <[^>]*> - non-terminal 9 | # 4. \[[^\]]*] - optional 10 | # 5. \|(?![^\[]*\]) - or 11 | 12 | defmodule SQL.BNF do 13 | @moduledoc false 14 | 15 | def parse() do 16 | File.cwd!() 17 | |> Path.join("standard/ISO_IEC_9075-2(E)_Foundation.bnf.txt") 18 | |> File.read!() 19 | |> parse() 20 | end 21 | 22 | def parse(binary) do 23 | Map.new(parse(binary, :symbol, [], [], [], [], [])) 24 | end 25 | 26 | defp parse(<<>>, _type, data, acc, symbol, expr, rules) do 27 | merge(rules, symbol, expr ++ merge(acc, data)) 28 | end 29 | defp parse(<>, :symbol = type, symbol, _acc, _data, _expr, rules) do 30 | parse(rest, type, [], [], symbol, [], rules) 31 | end 32 | defp parse(<>, _type, data, acc, symbol, expr, rules) do 33 | parse(<>, :symbol, [], [], [], [], merge(rules, symbol, expr ++ merge(acc, data))) 34 | end 35 | defp parse(<>, _type, data, acc, symbol, expr, rules) do 36 | parse(rest, :expr, [], [], String.trim("#{data}"), [], merge(rules, symbol, expr ++ acc)) 37 | end 38 | defp parse(<>, type, [?!, ?! | _] = data, acc, symbol, expr, rules) do 39 | parse(rest, type, [], merge(acc, "#{data ++ [?.]}"), symbol, expr, rules) 40 | end 41 | defp parse(<>, type, data, acc, symbol, expr, rules) do 42 | parse(rest, type, data ++ [?., ?., ?.], acc, symbol, expr, rules) 43 | end 44 | defp parse(<>, type, data, acc, symbol, expr, rules) do 45 | parse(rest, type, data ++ [?|], acc, symbol, expr, rules) 46 | end 47 | defp parse(<>, type, [] = data, acc, symbol, expr, rules) when b in [?\s, ?\t, ?\r, ?\n, ?\f] do 48 | parse(rest, type, data, acc, symbol, expr, rules) 49 | end 50 | defp parse(<>, type, data, acc, symbol, expr, rules) when b in [?\n] do 51 | parse(rest, type, data, acc, symbol, expr, rules) 52 | end 53 | defp parse(<>, type, data, acc, symbol, expr, rules) do 54 | parse(rest, type, data ++ [b], acc, symbol, expr, rules) 55 | end 56 | 57 | defp merge([], []), do: [] 58 | defp merge(rules, []), do: rules 59 | defp merge(rules, data), do: rules ++ [data] 60 | defp merge(rules, [], []), do: rules 61 | defp merge(rules, rule, expr) when is_list(rule), do: merge(rules, "#{rule}", expr) 62 | defp merge(rules, rule, expr) when is_list(expr), do: merge(rules, rule, "#{expr}") 63 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, ["\u0020"]}] # 32 \u0020 64 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] # "Lu", "Ll", "Lt", "Lm", "Lo", or "Nl" Unicode.Set.match?(<>, "[[:Lu:], [:Ll:], [:Lt:], [:Lm:], [:Lo:], [:Nl:]]") 65 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] # 183 \u00B7 or "Mn", "Mc", "Nd", "Pc", or "Cf" Unicode.Set.match?(<>, "[[:Mn:], [:Mc:], [:Nd:], [:Pc:], [:Cf:]]") 66 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, ["\\u"]}] 67 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 68 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, ["\u0009", "\u000D", "\u00A0", "\u00A0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202F", "\u205F", "\u3000", "\u180E", "\u200B", "\u200C", "\u200D", "\u2060", "\uFEFF"]}] 69 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 70 | defp merge(rules, "" = symbol, _expr), do: rules ++ [{symbol, [:ignore]}] 71 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, ["\u000A", "\u000B", "\u000C", "\u000D", "\u0085", "\u2028", "\u2029"]}] 72 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 73 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 74 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 75 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 76 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 77 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 78 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 79 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 80 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 81 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 82 | defp merge(rules, "" = symbol, _expr), do: rules ++ [{symbol, [:ignore]}] 83 | defp merge(rules, "" = symbol, _expr), do: rules ++ [{symbol, [:ignore]}] 84 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 85 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 86 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 87 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 88 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 89 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 90 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 91 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 92 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 93 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 94 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 95 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 96 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 97 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 98 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 99 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 100 | defp merge(rules, "" = symbol, "!! See the Syntax Rules."), do: rules ++ [{symbol, [:ignore]}] 101 | defp merge(_rules, symbol, "!! See the Syntax Rules."), do: raise "Please apply rules for #{symbol} by referencing the PDF or https://github.com/ronsavage/SQL/blob/master/Syntax.rules.txt" 102 | defp merge(rules, symbol, expr), do: rules ++ [{symbol, expr}] 103 | end 104 | -------------------------------------------------------------------------------- /lib/formatter.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.MixFormatter do 5 | @moduledoc false 6 | @behaviour Mix.Tasks.Format 7 | 8 | @impl Mix.Tasks.Format 9 | def features(opts), do: [sigils: [:SQL], extensions: get_in(opts, [:sql, :extensions])] 10 | 11 | @impl Mix.Tasks.Format 12 | def format(source, _opts), do: "#{SQL.parse(source)}" 13 | end 14 | -------------------------------------------------------------------------------- /lib/mix/tasks/sql.gen.parser.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule Mix.Tasks.Sql.Gen.Parser do 5 | use Mix.Task 6 | import Mix.Generator 7 | @moduledoc since: "0.2.0" 8 | 9 | @shortdoc "Generates a lexer and parser from the BNF rules" 10 | def run(_args) do 11 | rules = SQL.BNF.parse() 12 | space = Enum.map(rules[""], fn <> -> c end) 13 | whitespace = Enum.map(rules[""], fn <> -> c end) 14 | newline = Enum.map(rules[""], fn <> -> c end) 15 | 16 | keywords = String.split(rules[""], "|") ++ String.split(rules[""], "|") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) 17 | keywords = keywords ++ ~w[LIMIT ILIKE BACKWARD FORWARD ISNULL NOTNULL] 18 | create_file("lib/lexer.ex", lexer_template([mod: SQL.Lexer, keywords: keywords, space: space, whitespace: whitespace, newline: newline])) 19 | create_file("lib/parser.ex", parser_template([mod: SQL.Parser, keywords: Enum.map(keywords, &String.to_atom(String.downcase(&1)))])) 20 | end 21 | 22 | def guard(keyword) do 23 | {value, _n} = for <>, reduce: {[], 1} do 24 | {[], n} -> {{:in, [context: Elixir, imports: [{2, Kernel}]], [{:"b#{n}", [], Elixir}, Enum.uniq(~c"#{<>}#{String.upcase(<>)}")]}, n+1} 25 | {left, n} -> {{:and, [context: Elixir, imports: [{2, Kernel}]], [left, {:in, [context: Elixir, imports: [{2, Kernel}]], [{:"b#{n}", [], Elixir}, Enum.uniq(~c"#{<>}#{String.upcase(<>)}")]}]}, n+1} 26 | end 27 | Macro.to_string(value) 28 | end 29 | 30 | embed_template(:parser, """ 31 | # SPDX-License-Identifier: Apache-2.0 32 | # SPDX-FileCopyrightText: 2025 DBVisor 33 | 34 | defmodule <%= inspect @mod %> do 35 | @moduledoc false 36 | @compile {:inline, parse: 1, parse: 5, predicate: 1, insert_node: 5} 37 | 38 | import Kernel, except: [is_boolean: 1] 39 | 40 | defguard is_and(node) when elem(node, 0) == :and 41 | defguard is_between(node) when elem(node, 0) == :between 42 | defguard is_boolean(node) when elem(node, 0) in ~w[and or <> <= >= != < > !< !> = true false unknown like ilike in all any is isnull notnull between]a 43 | defguard is_combinator(node) when elem(node, 0) in ~w[except intersect union]a and elem(node, 2) == [] 44 | defguard is_comma(node) when elem(node, 0) == :comma 45 | defguard is_comment(node) when elem(node, 0) in ~w[comment comments]a 46 | defguard is_conditional(node) when elem(node, 0) in ~w[and or]a and elem(node, 2) == [] 47 | defguard is_colon(node) when elem(node, 0) == :colon 48 | defguard is_distinct(node) when elem(node, 0) == :distinct 49 | defguard is_declare(node) when elem(node, 0) == :declare 50 | defguard is_data_type(node) when elem(node, 0) in ~w[integer float ident quote double_quote backtick bracket parens . binding]a 51 | defguard is_fetch(node) when elem(node, 0) == :fetch 52 | defguard is_fetch_dir(node) when elem(node, 0) in ~w[absolute backward forward relative]a 53 | defguard is_from(node) when elem(node, 0) == :from 54 | defguard is_for(node) when elem(node, 0) == :for 55 | defguard is_grant(node) when elem(node, 0) == :grant 56 | defguard is_revoke(node) when elem(node, 0) == :revoke 57 | defguard is_keyword(node) when elem(node, 0) in <%= inspect(@keywords, limit: :infinity) %> 58 | defguard is_not(node) when elem(node, 0) == :not and elem(node, 2) == [] 59 | defguard is_join(node) when elem(node, 0) == :join 60 | defguard is_parens(node) when elem(node, 0) == :parens 61 | defguard is_operator(node) when elem(node, 0) in ~w[operator :: + - * / ^ % & += -= *= /= %= &= ^-= |*= <=> || as <> <= >= != < > !< !> = like ilike in all any is isnull notnull between]a 62 | defguard is_of(node) when elem(node, 0) == :of 63 | defguard is_is(node) when elem(node, 0) == :is 64 | defguard is_on(node) when elem(node, 0) == :on 65 | defguard is_select(node) when elem(node, 0) == :select 66 | 67 | def predicate([l, c, r]) when is_boolean(l) and is_conditional(c) and is_boolean(r) do 68 | {elem(c, 0), elem(c, 1), [l, r]} 69 | end 70 | def predicate([l, b]) when is_boolean(b) do 71 | [{elem(b, 0), elem(b, 1), [l | elem(b, 2)]}] 72 | end 73 | def predicate([l, b, r | rest]) when is_boolean(b) or is_operator(b) do 74 | predicate([{elem(b, 0), elem(b, 1), [l, r]} | rest]) 75 | end 76 | def predicate([{_, _, _}, node | _] = unit) when is_comma(node) do 77 | unit 78 | end 79 | def predicate([l, b, r, c | rest]) when is_comma(c) and (is_boolean(b) or is_operator(b)) do 80 | [{elem(b, 0), elem(b, 1), [l, r]}, c | rest] 81 | end 82 | def predicate([l, c, r, c2 | rest]) when is_boolean(l) and is_conditional(c) and is_boolean(r) and is_conditional(c2) do 83 | predicate([{elem(c, 0), elem(c, 1), [l, r]}, c2 | rest]) 84 | end 85 | def predicate([f, c, l, b, r, c2 | rest]) when is_boolean(b) and is_conditional(c) and is_conditional(c2) do 86 | predicate([f, c, {elem(b, 0), elem(b, 1), [l, r]}, c2 | rest]) 87 | end 88 | def predicate([f, c, l, b, r]) when is_boolean(b) and is_conditional(c) do 89 | predicate([f, c, {elem(b, 0), elem(b, 1), [l, r]}]) 90 | end 91 | def predicate([l, b, r, c | rest]) when is_boolean(b) and is_conditional(c) do 92 | predicate([{elem(b, 0), elem(b, 1), [l, r]}, c | rest]) 93 | end 94 | def predicate(unit) do 95 | unit 96 | end 97 | 98 | 99 | def insert_node(node, unit, acc, context, root) when is_parens(node) do 100 | {[{elem(node, 0), elem(node, 1), parse(elem(node, 2))} | unit], acc, context, root} 101 | end 102 | def insert_node(node, [{:in = tag, meta, []}, right, {:using, _, _} = using | unit], acc, context, root) do 103 | {[{tag, meta, [node, [right, using | unit]]}], acc, context, root} 104 | end 105 | def insert_node({:in, _, _} = node, [_, {:using, _, _}|_] = unit, acc, context, root) do 106 | {[node | unit], acc, context, root} 107 | end 108 | def insert_node({:into = tag, meta, _}, [_] = unit, acc, context, root) do 109 | {[{tag, meta, unit}], acc, context, root} 110 | end 111 | def insert_node(node, [n, b, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_not(n) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 112 | {[{elem(b, 0), elem(b, 1), [{elem(n, 0), elem(n, 1), [node]}, {elem(c, 0), elem(c, 1), [r, l]}]} | unit], acc, context, root} 113 | end 114 | def insert_node(node, [n, b, s, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_not(n) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 115 | {[{elem(b, 0), elem(b, 1), [{elem(n, 0), elem(n, 1), [node]}, {elem(s, 0), elem(s, 1), [{elem(c, 0), elem(c, 1), [r, l]}]}]} | unit], acc, context, root} 116 | end 117 | def insert_node(node, [b, s, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 118 | {[{elem(b, 0), elem(b, 1), [node, {elem(s, 0), elem(s, 1), [{elem(c, 0), elem(c, 1), [r, l]}]}]} | unit], acc, context, root} 119 | end 120 | def insert_node(node, [b, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 121 | {[{elem(b, 0), elem(b, 1), [node, {elem(c, 0), elem(c, 1), [r, l]}]} | unit], acc, context, root} 122 | end 123 | def insert_node(node, [b, l, c | unit], acc, context, root) when is_data_type(node) and is_operator(b) and is_data_type(l) and is_conditional(c) do 124 | {[{elem(b, 0), elem(b, 1), [node, l]}, c | unit], acc, context, root} 125 | end 126 | def insert_node(node, [r, b, l | unit], acc, context, root) when is_conditional(node) and is_data_type(r) and is_operator(b) and is_data_type(l) do 127 | {[node, {elem(b, 0), elem(b, 1), [r, l]} | unit], acc, context, root} 128 | end 129 | def insert_node(node, [o, l], acc, context, root) when is_data_type(node) and is_operator(o) and is_data_type(l) do 130 | {[{elem(o, 0), elem(o, 1), [node, l]}], acc, context, root} 131 | end 132 | def insert_node(node, [u | unit], acc, context, root) when is_not(node) and elem(u, 0) in ~w[false true unknown null]a do 133 | {[{elem(node, 0), elem(node, 1), [u]} | unit], acc, context, root} 134 | end 135 | def insert_node(node, [u | unit], acc, context, root) when is_not(u) and is_data_type(node) do 136 | {[{elem(u, 0), elem(u, 1), [node | unit]}], acc, context, root} 137 | end 138 | def insert_node({:into = tag, meta, []}, [ident, parens, values], acc, context, root) do 139 | {[], [{tag, meta, [ident, parens, values]} | acc], context, root} 140 | end 141 | def insert_node({tag, meta, []}, [ident, parens], acc, context, root) when tag in ~w[into table]a do 142 | {[], [{tag, meta, [ident, parens]} | acc], context, root} 143 | end 144 | def insert_node({:add = tag, meta, []}, [ident, type], acc, context, root) do 145 | {[], [{tag, meta, [ident, type]} | acc], context, root} 146 | end 147 | def insert_node({:type = tag, meta, []}, [ident, as, type], acc, context, root) do 148 | {[], [{tag, meta, [{elem(as, 0), elem(as, 1), [ident, type]}]} | acc], context, root} 149 | end 150 | def insert_node({tag, meta, []}, [ident], acc, context, root) when tag in ~w[type table]a do 151 | {[], [{tag, meta, [ident]} | acc], context, root} 152 | end 153 | def insert_node({:with = tag, meta, []}, [{:recursive = t, m, []}, {:ident, _, _} = l, {:parens, _, _} = r, {:as = t2, m2, a}], [], context, root) do 154 | {[], [], context, root ++ [{tag, meta, [{t2, m2, [{t, m, [l, r]} | a]}]}]} 155 | end 156 | def insert_node({:with = tag, meta, []}, [{:ident, _, _} = l, {:parens, _, _} = r, {:as = t2, m2, a}], [], context, root) do 157 | {[], [], context, root ++ [{tag, meta, [{t2, m2, [[l, r] | a]}]}]} 158 | end 159 | def insert_node({:with = tag, meta, []}, [{:ident, _, _}, {:as, _, _}] = unit, acc, context, root) do 160 | {[], [], context, root ++ [{tag, meta, unit ++ acc}]} 161 | end 162 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[by in references]a do 163 | {[{tag, meta, predicate(unit ++ acc)}], [], context, root} 164 | end 165 | def insert_node(node, [n|_] = unit, acc, context, root) when (is_on(n) or is_of(n)) and elem(node, 0) in ~w[select insert update delete truncate references trigger create connect temporary execute usage set alter system maintain]a do 166 | {[node|unit], acc, context, root} 167 | end 168 | def insert_node(node, [_, n|_] = unit, acc, context, root) when is_for(n) and is_from(node) do 169 | {[node|unit], acc, context, root} 170 | end 171 | def insert_node(node, [_, _, _, n|_] = unit, acc, context, root) when is_for(n) and is_select(node) do 172 | {[node|unit], acc, context, root} 173 | end 174 | def insert_node(node, [] = unit, [] = acc, [] = context, root) when elem(node, 0) in ~w[create drop insert alter update delete start set open close commit rollback]a do 175 | {[node | unit], acc, context, root} 176 | end 177 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[create drop insert alter update delete start set open close commit rollback]a do 178 | {[], [], context, [{tag, meta, List.wrap(predicate(unit ++ acc))} | root]} 179 | end 180 | def insert_node(node, [n |_] = unit, acc, context, root) when is_grant(node) and elem(n, 0) == :option do 181 | {[node | unit], acc, context, root} 182 | end 183 | def insert_node(node, unit, acc, context, root) when is_grant(node) or is_revoke(node) or is_declare(node) do 184 | {[], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]} 185 | end 186 | def insert_node({:distinct = tag, meta, []}, [{:on, _, _} = on | unit], acc, context, root) do 187 | {[{tag, meta, [on]} | unit], acc, context, root} 188 | end 189 | def insert_node(node, [u | unit], acc, context, root) when is_fetch_dir(node) and elem(u, 0) != :in do 190 | {[{elem(node, 0), elem(node, 1), [u]}], unit++acc, context, root} 191 | end 192 | def insert_node(node, [u | unit], acc, context, root) when is_fetch(node) do 193 | {[], [], context, [{elem(node, 0), elem(node, 1), [u]} | unit ++ acc ++ root]} 194 | end 195 | def insert_node(node, [on], [], context, root) when is_join(node) and is_on(on) do 196 | {[], [], context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [on]} | root]} 197 | end 198 | def insert_node(node, [ident, on], [] = acc, context, root) when is_join(node) and is_on(on) do 199 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [ident | elem(on, 2)]}]} | root]} 200 | end 201 | def insert_node(node, [ident, as, on | unit], [] = acc, context, root) when is_join(node) and is_on(on) do 202 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [[ident, as]] ++ elem(on, 2) ++ unit}]} | root]} 203 | end 204 | def insert_node(node, [ident, on | unit], [] = acc, context, root) when is_join(node) and is_on(on) do 205 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [ident] ++ elem(on, 2) ++ unit}]} | root]} 206 | end 207 | def insert_node(node, unit, acc, context, root) when is_join(node) do 208 | a = elem(node, 2) 209 | acc = unit ++ acc 210 | acc = if a == [], do: acc, else: a ++ [acc] 211 | {[], [], context, [{elem(node, 0), elem(node, 1), acc} | root]} 212 | end 213 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[select from where group having order limit offset]a do 214 | {[], [], context, [{tag, meta, List.wrap(predicate(unit ++ acc))} | root]} 215 | end 216 | def insert_node(node, unit, acc, context, {:colon, meta, []}) do 217 | {unit, acc, context, {:colon, meta, [node]}} 218 | end 219 | def insert_node(node, [parens | unit], acc, context, root) when is_parens(parens) and is_keyword(node) do 220 | {[{elem(node, 0), elem(node, 1), [parens]} | unit], acc, context, root} 221 | end 222 | def insert_node(node, unit, acc, context, root) do 223 | {[node | unit], acc, context, root} 224 | end 225 | 226 | def parse(tokens) do 227 | parse(tokens, [], [], [], []) 228 | end 229 | def parse([], [], [], [], root) do 230 | root 231 | end 232 | def parse([], unit, acc, [], []) do 233 | predicate(unit ++ acc) 234 | end 235 | def parse([], unit, acc, [], root) do 236 | predicate(unit ++ acc) ++ root 237 | end 238 | def parse([], unit, acc, context, root) when is_tuple(context) do 239 | [{elem(context, 0), elem(context, 1), [unit ++ acc ++ root, elem(context, 2)]}] 240 | end 241 | def parse([node | rest], unit, acc, context, root) when is_comment(node) do 242 | parse(rest, unit, acc, context, [node | root]) 243 | end 244 | def parse([{:all, m, _}, node | rest], unit, acc, [], root) when is_combinator(node) do 245 | parse(rest, [], [], {elem(node, 0), elem(node, 1), [{:all, m, unit ++ acc ++ root}]}, []) 246 | end 247 | def parse([node | rest], unit, acc, [], root) when is_combinator(node) do 248 | parse(rest, [], [], {elem(node, 0), elem(node, 1), unit ++ acc ++ root}, []) 249 | end 250 | def parse([node | rest], unit, acc, context, root) when is_colon(node) do 251 | parse(rest, [], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]) 252 | end 253 | def parse([ident, from, distinct, n, is, left | rest], unit, acc, context, root) when is_is(is) and is_from(from) and is_distinct(distinct) do 254 | node = {elem(is, 0), elem(is, 1), [left, {elem(n, 0), elem(n, 1), [{elem(distinct, 0), elem(distinct, 1), [{elem(from, 0), elem(from, 1), [ident]}]}]}]} 255 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 256 | parse(rest, unit, acc, context, root) 257 | end 258 | def parse([ident, from, distinct, is, left | rest], unit, acc, context, root) when is_is(is) and is_from(from) and is_distinct(distinct) do 259 | node = {elem(is, 0), elem(is, 1), [left, {elem(distinct, 0), elem(distinct, 1), [{elem(from, 0), elem(from, 1), [ident]}]}]} 260 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 261 | parse(rest, unit, acc, context, root) 262 | end 263 | def parse([node | rest], unit, acc, context, root) when is_colon(node) do 264 | parse(rest, [], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]) 265 | end 266 | def parse([parens, node | rest], unit, acc, [], root) when is_parens(parens) and is_combinator(node) do 267 | parse(rest, unit, acc, {elem(node, 0), elem(node, 1), [{elem(parens, 0), elem(parens, 1), parse(elem(parens, 2))}]}, root) 268 | end 269 | def parse([node | rest], unit, acc, context, root) when is_comma(node) do 270 | parse(rest, [], [{elem(node, 0), elem(node, 1), predicate(unit)} | acc], context, root) 271 | end 272 | def parse([node | rest], unit, acc, context, root) do 273 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 274 | parse(rest, unit, acc, context, root) 275 | end 276 | end 277 | """) 278 | 279 | 280 | embed_template(:lexer, """ 281 | # SPDX-License-Identifier: Apache-2.0 282 | # SPDX-FileCopyrightText: 2025 DBVisor 283 | 284 | defmodule <%= inspect @mod %> do 285 | @moduledoc false 286 | @compile {:inline, lex: 9, lex: 4, meta: 3, merge: 3, type: 2, node: 5} 287 | 288 | defguard is_data_type(node) when elem(node, 0) in ~w[integer float ident quote double_quote backtick bracket parens .]a 289 | defguard is_newline(b) when b in <%= inspect(@newline) %> 290 | defguard is_space(b) when b in <%= inspect(@space) %> 291 | defguard is_whitespace(b) when b in <%= inspect(@whitespace) %> 292 | 293 | def opening_delimiter(:parens), do: :"(" 294 | def opening_delimiter(:bracket), do: :"[" 295 | def opening_delimiter(:double_quote), do: :"\\"" 296 | def opening_delimiter(:quote), do: :"'" 297 | def opening_delimiter(:backtick), do: :"`" 298 | def opening_delimiter(type) when type in ~w[var code braces]a, do: :"{" 299 | 300 | def expected_delimiter(:parens), do: :")" 301 | def expected_delimiter(:bracket), do: :"]" 302 | def expected_delimiter(:double_quote), do: :"\\"" 303 | def expected_delimiter(:quote), do: :"'" 304 | def expected_delimiter(:backtick), do: :"`" 305 | def expected_delimiter(type) when type in ~w[var code braces]a, do: :"}" 306 | 307 | def lex(binary, file, params \\\\ 0, opts \\\\ [metadata: true]) do 308 | case lex(binary, binary, [{:binding, []}, {:params, params}, {:file, file} | opts], 0, 0, nil, [], [], 0) do 309 | {"", _binary, opts, line, column, nil = type, data, acc, _n} -> 310 | {:ok, opts, line, column, type, data, acc} 311 | 312 | {"", binary, _opts, end_line, end_column, type, _data, [{_, [line: line, column: column, file: file], _}|_], _n} when type in ~w[parens bracket double_quote quote backtick var code]a -> 313 | raise TokenMissingError, file: file, snippet: binary, end_line: end_line, end_column: end_column, line: line, column: column, opening_delimiter: opening_delimiter(type), expected_delimiter: expected_delimiter(type) 314 | 315 | {"", _binary, opts, line, column, type, data, acc, _n} -> 316 | {:ok, opts, line, column, type, data, insert_node(node(ident(type, data), line, column, data, opts), acc)} 317 | end 318 | end 319 | def lex("" = rest, binary, opts, line, column, type, data, acc, n) do 320 | {rest, binary, opts, line, column, type, data, acc, n} 321 | end 322 | def lex(<>, binary, opts, line, column, type, data, acc, n) do 323 | lex(rest, binary, opts, line, column+2, :comment, [], insert_node(type, line, column, data, opts, acc), n) 324 | end 325 | def lex(<>, binary, opts, line, column, _type, data, acc, n) do 326 | lex(rest, binary, opts, line, column+2, :comments, data, acc, n) 327 | end 328 | def lex(<>, binary, opts, line, column, :comments, data, acc, n) do 329 | lex(rest, binary, opts, line, column+2, nil, [], insert_node(node(:comments, line, column, data, opts), acc), n) 330 | end 331 | def lex(<>, binary, opts, line, column, :comments, data, acc, n) do 332 | lex(rest, binary, opts, line, column+1, :comments, [data | [b]], acc, n) 333 | end 334 | def lex(<>, binary, opts, line, column, nil, data, acc, n) do 335 | lex(rest, binary, opts, line, column+2, :var, data, acc, n) 336 | end 337 | def lex(<>, binary, [_, _, _, {:format, true}] = opts, line, column, _type, data, acc, 0 = n), do: lex(rest, binary, opts, line, column+2, nil, [], insert_node(node(:binding, line, column, data, opts), acc), n) 338 | def lex(<>, binary, opts, line, column, type, data, acc, 0 = n) when type in ~w[code var]a do 339 | opts = opts 340 | |> Keyword.update!(:binding, &(&1 ++ [{type, IO.iodata_to_binary(data)}])) 341 | |> Keyword.update!(:params, &(&1+1)) 342 | lex(rest, binary, opts, line, column+2, nil, [], insert_node(node(:binding, line, column, Keyword.get(opts, :params), opts), acc), n) 343 | end 344 | def lex(<>, binary, opts, line, column, :code = type, data, acc, n) do 345 | lex(rest, binary, opts, line, column+1, type, [data | [?}]], acc, n-1) 346 | end 347 | def lex(<>, binary, opts, line, column, type, data, acc, n) when type in ~w[var code]a and b in [?{] do 348 | lex(rest, binary, opts, line, column+1, :code, [data | [b]], acc, n+1) 349 | end 350 | def lex(<>, binary, opts, line, column, :var = type, data, acc, n) when b in ?0..?9 and data != [] do 351 | lex(rest, binary, opts, line, column+1, type, [data | [b]], acc, n) 352 | end 353 | def lex(<>, binary, opts, line, column, :var = type, data, acc, n) when b in ?a..?z or b in ?A..?Z or (b == ?_ and data != []) do 354 | lex(rest, binary, opts, line, column+1, type, [data | [b]], acc, n) 355 | end 356 | def lex(<>, binary, opts, line, column, type, data, acc, n) when type in ~w[var code]a do 357 | lex(rest, binary, opts, line, column+1, :code, [data | [b]], acc, n) 358 | end 359 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b in [?(, ?[] do 360 | acc = case ident(type, data) do 361 | nil -> acc 362 | :ident -> insert_node(node(type, line, column, data, opts), acc) 363 | tag -> insert_node(node(tag, line, column, [], opts), acc) 364 | end 365 | case lex(rest, binary, opts, line, column+1, nil, [], [], n) do 366 | {rest, opts, line, column, value} -> 367 | lex(rest, binary, opts, line, column, nil, [], insert_node(node(ident(type, [b]), line, column, value, opts), acc), n) 368 | {rest, binary, o, l, c, t, d, a, _n} -> 369 | value = if t, do: insert_node(node(t, l, c, d, o), a), else: a 370 | lex(rest, binary, opts, l, c, (if b == ?(, do: :parens, else: :bracket), [], insert_node(node(ident(type, [b]), line, column, value, opts), acc), n) 371 | end 372 | end 373 | def lex(<>, _binary, opts, line, column, type, data, acc, _n) when b in [?), ?]] do 374 | acc = if type, do: insert_node(node(type, line, column, data, opts), acc), else: acc 375 | {rest, opts, line, column+1, acc} 376 | end 377 | def lex(<>, binary, opts, line, column, :double_quote = type, data, acc, n) do 378 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node(type, line, column, data, opts), acc), n) 379 | end 380 | def lex(<>, binary, opts, line, column, :backtick = type, data, acc, n) do 381 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node(type, line, column, data, opts), acc), n) 382 | end 383 | def lex(<>, binary, opts, line, column, :quote = type, data, acc, n) do 384 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node(type, line, column, data, opts), acc), n) 385 | end 386 | def lex(<>, binary, opts, line, column, type, data, acc, n) when type in ~w[double_quote quote backtick]a do 387 | lex(rest, binary, opts, line, column+1, type, [data | [b]], acc, n) 388 | end 389 | def lex(<>, binary, opts, line, column, type, data, acc, n) when is_newline(b) do 390 | if data == [] do 391 | lex(rest, binary, opts, line+1, column, type, data, acc, n) 392 | else 393 | tag = ident(type, data) 394 | lex(rest, binary, opts, line+1, column, nil, [], insert_node(node(tag, line, column, data, opts), acc), n) 395 | end 396 | end 397 | def lex(<>, binary, opts, line, column, type, data, acc, n) when is_space(b) do 398 | if data == [] do 399 | lex(rest, binary, opts, line, column+1, type, data, acc, n) 400 | else 401 | tag = ident(type, data) 402 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node(tag, line, column, data, opts), acc), n) 403 | end 404 | end 405 | def lex(<>, binary, opts, line, column, type, data, acc, n) when is_whitespace(b) do 406 | if data == [] do 407 | lex(rest, binary, opts, line, column+1, type, data, acc, n) 408 | else 409 | tag = ident(type, data) 410 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node(tag, line, column, data, opts), acc), n) 411 | end 412 | end 413 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b in [?,, ?;] do 414 | acc = if type, do: insert_node(node(ident(type, data), line, column, data, opts), acc), else: acc 415 | lex(rest, binary, opts, line, column, nil, [], insert_node(node(type(b), line, column+1, [], opts), acc), n) 416 | end 417 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b in ~w[^-= |*= <=>] do 418 | node = node(String.to_atom(b), line, column+3, [], opts) 419 | if data == [] do 420 | lex(rest, binary, opts, line, column+3, type, data, insert_node(node, acc), n) 421 | else 422 | lex(rest, binary, opts, line, column+3, nil, [], insert_node(node, insert_node(node(ident(type, data), line, column, data, opts), acc)), n) 423 | end 424 | end 425 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b in ~w[:: <> != !< !> <= >= += -= *= /= %= &= ||] do 426 | node = node(String.to_atom(b), line, column+2, [], opts) 427 | if data == [] do 428 | lex(rest, binary, opts, line, column+2, type, data, insert_node(node, acc), n) 429 | else 430 | lex(rest, binary, opts, line, column+2, nil, [], insert_node(node, insert_node(node(ident(type, data), line, column, data, opts), acc)), n) 431 | end 432 | end 433 | def lex(<>, binary, opts, line, column, type, data, acc, n) when type in ~w[integer float]a and b in [?E, ?e] and (e in [?-, ?+] or e in ?0..?9) do 434 | type = :float 435 | lex(rest, binary, opts, line, column+2, type, merge(merge(data, b, type), e, type), acc, n) 436 | end 437 | def lex(<>, binary, opts, line, column, nil, [], acc, n) when b == ?. and e in ?0..?9 do 438 | lex(rest, binary, opts, line, column+2, :float, [b, e], acc, n) 439 | end 440 | def lex(<>, binary, opts, line, column, nil, [], acc, n) when b in [?-, ?+] and e == ?. do 441 | lex(rest, binary, opts, line, column+2, :float, [b,e], acc, n) 442 | end 443 | def lex(<>, binary, opts, line, column, :integer, data, acc, n) when b == ?. do 444 | type = :float 445 | lex(rest, binary, opts, line, column+1, type, merge(data, b, type), acc, n) 446 | end 447 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b == ?. do 448 | node = node(List.to_atom([b]), line, column+1, [], opts) 449 | if data == [] do 450 | lex(rest, binary, opts, line, column+1, type, data, insert_node(node, acc), n) 451 | else 452 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node, insert_node(node(ident(type, data), line, column, data, opts), acc)), n) 453 | end 454 | end 455 | def lex(<>, binary, opts, line, column, _type, [] = data, [node | _] = acc, n) when b in [?+, ?-, ?^, ?*, ?/, ?%, ?&, ?<, ?>, ?=] and is_data_type(node) do 456 | node = node(List.to_atom([b]), line, column+1, data, opts) 457 | lex(rest, binary, opts, line, column+1, nil, data, insert_node(node, acc), n) 458 | end 459 | def lex(<>, binary, opts, line, column, nil, [], acc, n) when b in [?+, ?-] and c in ?0..?9 do 460 | lex(rest, binary, opts, line, column+2, :integer, [b, c], acc, n) 461 | end 462 | def lex(<>, binary, opts, line, column, nil = type, data, acc, n) when b in [?+, ?-, ?^, ?*, ?/, ?%, ?&, ?<, ?>, ?=] do 463 | node = node(List.to_atom([b]), line, column+1, data, opts) 464 | lex(rest, binary, opts, line, column+1, type, data, insert_node(node, acc), n) 465 | end 466 | def lex(<>, binary, opts, line, column, type, data, acc, n) when b in [?+, ?-, ?^, ?*, ?/, ?%, ?&, ?<, ?>, ?=] and type in ~w[integer float ident quote double_quote backtick bracket parens nil]a do 467 | node = node(List.to_atom([b]), line, column+1, [], opts) 468 | lex(rest, binary, opts, line, column+1, nil, [], insert_node(node, insert_node(node(ident(type, data), line, column, data, opts), acc)), n) 469 | end 470 | def lex(<>, binary, opts, line, column, type, data, acc, n) do 471 | type = type(b, type) 472 | lex(rest, binary, opts, line, column+1, type, merge(data, b, type), acc, n) 473 | end 474 | 475 | def insert_node(nil, _line, _column, _data, _opts, acc) do 476 | acc 477 | end 478 | def insert_node(type, line, column, data, opts, acc) do 479 | insert_node(node(type, line, column, data, opts), acc) 480 | end 481 | def insert_node(right, [{:. = tag, m, a}, {:., _, [_, _]} = left | acc]) do 482 | [{tag, m, [left, right | a]} | acc] 483 | end 484 | def insert_node(right, [{:. = tag, meta, [left]} | acc]) do 485 | [{tag, meta, [left, right]} | acc] 486 | end 487 | def insert_node({:., _, _} = node, [right, {:. = tag, m, []}, left | acc]) do 488 | [node, {tag, m, [left, right]} | acc] 489 | end 490 | def insert_node({:. = t, m, a}, [left | acc]) do 491 | [{t, m, [left|a]} | acc] 492 | end 493 | def insert_node({:join = t, m, a} = node, acc) do 494 | case join(acc) do 495 | {qualified, rest} -> [{t, m, [qualified|a]} | rest] 496 | rest -> [node | rest] 497 | end 498 | end 499 | def insert_node(node, acc) do 500 | [node | acc] 501 | end 502 | 503 | def join([{:outer, _} = r, {tag, _} = l, {:natural, _} = n | rest]) when tag in ~w[left right full]a do 504 | {[n, l, r], rest} 505 | end 506 | def join([{:outer, _} = r, {tag, _} = l | rest]) when tag in ~w[left right full]a do 507 | {[l, r], rest} 508 | end 509 | def join([{:inner, _} = r, {:natural, _} = l| rest]) do 510 | {[l, r], rest} 511 | end 512 | def join([{tag, _} = l | rest]) when tag in ~w[inner left right full natural cross]a do 513 | {[l], rest} 514 | end 515 | def join(acc) do 516 | acc 517 | end 518 | 519 | def merge([] = data, _b, type) when type in ~w[double_quote quote backtick]a, do: data 520 | def merge(data, b, _type), do: [data | [b]] 521 | 522 | def type(?;), do: :colon 523 | def type(?,), do: :comma 524 | def type(?"), do: :double_quote 525 | def type(?'), do: :quote 526 | def type(?`), do: :backtick 527 | def type(?(), do: :left_paren 528 | def type(?)), do: :right_paren 529 | def type(?[), do: :left_bracket 530 | def type(?]), do: :right_bracket 531 | 532 | def type(%param{}), do: param 533 | def type(param) when is_float(param), do: :float 534 | def type(param) when is_integer(param), do: :integer 535 | def type(param) when is_map(param), do: :map 536 | def type(param) when is_list(param), do: {:list, Enum.uniq(Enum.map(param, &type/1))} 537 | def type(param) when is_binary(param), do: :string 538 | def type(_param), do: nil 539 | 540 | def type(_, type) when type in ~w[double_quote quote backtick comment comments]a, do: type 541 | def type(?", _type), do: :double_quote 542 | def type(?', _type), do: :quote 543 | def type(?`, _type), do: :backtick 544 | def type(b, type) when b in ?0..?9 and type in ~w[nil integer float]a, do: type || :integer 545 | def type(?., :integer), do: :float 546 | def type(_b, _type), do: :ident 547 | 548 | def meta(_line, _column, [_,_,_,{_,false}|_]), do: [] 549 | def meta(line, column, [_, _, {_, file} |_]), do: [line: line, column: column, file: file] 550 | 551 | def node(:binding = tag, line, column, [idx], [{:binding, false}, {:params, params}|_] = opts) do 552 | {tag, meta(line, column, opts), Enum.at(params, idx)} 553 | end 554 | def node(:binding = tag, line, column, data, opts) when is_integer(data), do: {tag, meta(line, column, opts), [data]} 555 | def node(tag, line, column, data, opts) when tag in ~w[ident float integer double_quote quote backtick binding parens bracket . comment comments]a do 556 | data = case data do 557 | [] -> data 558 | [{_, _, _} | _] -> data 559 | _ -> [IO.iodata_to_binary(data)] 560 | end 561 | {tag, meta(line, column, opts), data} 562 | end 563 | def node(tag, line, column, _data, opts) when tag in ~w[asterisk inner left right full natural cross outer]a do 564 | {tag, meta(line, column, opts)} 565 | end 566 | def node(tag, line, column, _data, opts) do 567 | {tag, meta(line, column, opts), []} 568 | end 569 | 570 | def ident(_type, [?*]), do: :asterisk 571 | def ident(_type, [?(]), do: :parens 572 | def ident(_type, [?[]), do: :bracket 573 | <%= for keyword <- @keywords do %> 574 | def ident(:ident, <%= Enum.reduce(1..byte_size(keyword), "[]", fn n, acc -> "[\#{acc}, b\#{n}]" end) %>) when <%= guard(keyword) %>, do: <%= inspect(String.to_atom(String.downcase(keyword))) %> 575 | <% end %> 576 | def ident(type, _), do: type 577 | end 578 | """) 579 | end 580 | -------------------------------------------------------------------------------- /lib/mix/tasks/sql.gen.test.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule Mix.Tasks.Sql.Gen.Test do 5 | use Mix.Task 6 | import Mix.Generator 7 | @moduledoc since: "0.2.0" 8 | 9 | @shortdoc "Generates test from the BNF rules" 10 | def run([base]) do 11 | create_file("test/conformance/e_test.exs", test_template([mod: SQL.Conformance.ETest, dir: Path.join(base, "E")])) 12 | create_file("test/conformance/f_test.exs", test_template([mod: SQL.Conformance.FTest, dir: Path.join(base, "F")])) 13 | create_file("test/conformance/s_test.exs", test_template([mod: SQL.Conformance.STest, dir: Path.join(base, "S")])) 14 | create_file("test/conformance/t_test.exs", test_template([mod: SQL.Conformance.TTest, dir: Path.join(base, "T")])) 15 | end 16 | 17 | def generate_test(dir) do 18 | for path <- File.ls!(dir), path =~ ".tests.yml", [{~c"feature", feature}, {~c"id", id}, {~c"sql", sql}] <- :yamerl.decode_file(to_charlist(Path.join(dir, path))) do 19 | statements = if is_list(hd(sql)), do: sql, else: [sql] 20 | statements = Enum.map(statements, &String.replace(to_string(&1), ~r{(VARING)}, "VARYING")) 21 | {"#{feature} #{id}", Enum.map(statements, &{trim(&1), &1})} 22 | end 23 | end 24 | 25 | def trim(value) do 26 | value 27 | |> String.replace(~r{\(\s+\b}, &String.replace(&1, " ", "")) 28 | |> String.replace(~r{\(\s+'}, &String.replace(&1, " ", "")) 29 | |> String.replace(~r{\(\s+"}, &String.replace(&1, " ", "")) 30 | |> String.replace(~r{\(\s+\*}, &String.replace(&1, " ", "")) 31 | |> String.replace(~r{[[:alpha:]]+\s+\(}, &String.replace(&1, " ", "")) 32 | |> String.replace(~r{\b\s+\,}, &String.replace(&1, " ", "")) 33 | |> String.replace(~r{\)\s+\,}, &String.replace(&1, " ", "")) 34 | |> String.replace(~r{\'\s+\,}, &String.replace(&1, " ", "")) 35 | |> String.replace(~r{\b\s+\)}, &String.replace(&1, " ", "")) 36 | |> String.replace(~r{'\s+\)}, &String.replace(&1, " ", "")) 37 | |> String.replace(~r{\*\s+\)}, &String.replace(&1, " ", "")) 38 | |> String.replace(~r{\)\s+\)}, &String.replace(&1, " ", "")) 39 | |> String.replace(~r{\W(SELECT|REFERENCES|INSERT|UPDATE|IN|MYTEMP)\(}, &Enum.join(Regex.split(~r{\(}, &1, include_captures: true, trim: true), " ")) 40 | |> String.replace(~r{^(SELECT)\(}, &Enum.join(Regex.split(~r{\(}, &1, include_captures: true, trim: true), " ")) 41 | |> String.replace(~r{\s+\.\s+}, &String.replace(&1, " ", "")) 42 | |> String.replace(~r{\d\s(\+|\-)\d}, &Enum.join(Enum.map(Regex.split(~r{\+|\-}, &1, include_captures: true, trim: true), fn x -> String.trim(x) end), " ")) 43 | |> String.trim() 44 | end 45 | 46 | embed_template(:test, """ 47 | # SPDX-License-Identifier: Apache-2.0 48 | # SPDX-FileCopyrightText: 2025 DBVisor 49 | 50 | defmodule <%= inspect @mod %>.Adapter do 51 | use SQL.Token 52 | 53 | def token_to_string(value, mod \\\\ __MODULE__) 54 | def token_to_string(value, _mod) when is_atom(value), do: String.upcase(Atom.to_string(value)) 55 | def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod) 56 | end 57 | defmodule <%= inspect @mod %> do 58 | use ExUnit.Case, async: true 59 | use SQL, adapter: <%= inspect @mod %>.Adapter 60 | 61 | <%= for {name, statements} <- generate_test(@dir) do %> 62 | test <%= inspect name %> do 63 | <%= for {left, right} <- statements do %> 64 | assert ~s{<%= left %>} == to_string(~SQL[<%= right %>]) 65 | <% end %> 66 | end 67 | <% end %> 68 | end 69 | """) 70 | end 71 | -------------------------------------------------------------------------------- /lib/parser.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Parser do 5 | @moduledoc false 6 | @compile {:inline, parse: 1, parse: 5, predicate: 1, insert_node: 5} 7 | 8 | import Kernel, except: [is_boolean: 1] 9 | 10 | defguard is_and(node) when elem(node, 0) == :and 11 | defguard is_between(node) when elem(node, 0) == :between 12 | defguard is_boolean(node) when elem(node, 0) in ~w[and or <> <= >= != < > !< !> = true false unknown like ilike in all any is isnull notnull between]a 13 | defguard is_combinator(node) when elem(node, 0) in ~w[except intersect union]a and elem(node, 2) == [] 14 | defguard is_comma(node) when elem(node, 0) == :comma 15 | defguard is_comment(node) when elem(node, 0) in ~w[comment comments]a 16 | defguard is_conditional(node) when elem(node, 0) in ~w[and or]a and elem(node, 2) == [] 17 | defguard is_colon(node) when elem(node, 0) == :colon 18 | defguard is_distinct(node) when elem(node, 0) == :distinct 19 | defguard is_declare(node) when elem(node, 0) == :declare 20 | defguard is_data_type(node) when elem(node, 0) in ~w[integer float ident quote double_quote backtick bracket parens . binding]a 21 | defguard is_fetch(node) when elem(node, 0) == :fetch 22 | defguard is_fetch_dir(node) when elem(node, 0) in ~w[absolute backward forward relative]a 23 | defguard is_from(node) when elem(node, 0) == :from 24 | defguard is_for(node) when elem(node, 0) == :for 25 | defguard is_grant(node) when elem(node, 0) == :grant 26 | defguard is_revoke(node) when elem(node, 0) == :revoke 27 | defguard is_keyword(node) when elem(node, 0) in [:abs, :absent, :acos, :all, :allocate, :alter, :and, :any, :any_value, :are, :array, :array_agg, :array_max_cardinality, :as, :asensitive, :asin, :asymmetric, :at, :atan, :atomic, :authorization, :avg, :begin, :begin_frame, :begin_partition, :between, :bigint, :binary, :blob, :boolean, :both, :btrim, :by, :call, :called, :cardinality, :cascaded, :case, :cast, :ceil, :ceiling, :char, :char_length, :character, :character_length, :check, :classifier, :clob, :close, :coalesce, :collate, :collect, :column, :commit, :condition, :connect, :constraint, :contains, :convert, :copy, :corr, :corresponding, :cos, :cosh, :count, :covar_pop, :covar_samp, :create, :cross, :cube, :cume_dist, :current, :current_catalog, :current_date, :current_default_transform_group, :current_path, :current_role, :current_row, :current_schema, :current_time, :current_timestamp, :current_transform_group_for_type, :current_user, :cursor, :cycle, :date, :day, :deallocate, :dec, :decfloat, :decimal, :declare, :default, :define, :delete, :dense_rank, :deref, :describe, :deterministic, :disconnect, :distinct, :double, :drop, :dynamic, :each, :element, :else, :empty, :end, :end_frame, :end_partition, :"end-exec", :equals, :escape, :every, :except, :exec, :execute, :exists, :exp, :external, :extract, false, :fetch, :filter, :first_value, :float, :floor, :for, :foreign, :frame_row, :free, :from, :full, :function, :fusion, :get, :global, :grant, :greatest, :group, :grouping, :groups, :having, :hold, :hour, :identity, :in, :indicator, :initial, :inner, :inout, :insensitive, :insert, :int, :integer, :intersect, :intersection, :interval, :into, :is, :join, :json, :json_array, :json_arrayagg, :json_exists, :json_object, :json_objectagg, :json_query, :json_scalar, :json_serialize, :json_table, :json_table_primitive, :json_value, :lag, :language, :large, :last_value, :lateral, :lead, :leading, :least, :left, :like, :like_regex, :listagg, :ln, :local, :localtime, :localtimestamp, :log, :log10, :lower, :lpad, :ltrim, :match, :match_number, :match_recognize, :matches, :max, :member, :merge, :method, :min, :minute, :mod, :modifies, :module, :month, :multiset, :national, :natural, :nchar, :nclob, :new, :no, :none, :normalize, :not, :nth_value, :ntile, :null, :nullif, :numeric, :occurrences_regex, :octet_length, :of, :offset, :old, :omit, :on, :one, :only, :open, :or, :order, :out, :outer, :over, :overlaps, :overlay, :parameter, :partition, :pattern, :per, :percent, :percent_rank, :percentile_cont, :percentile_disc, :period, :portion, :position, :position_regex, :power, :precedes, :precision, :prepare, :primary, :procedure, :ptf, :range, :rank, :reads, :real, :recursive, :ref, :references, :referencing, :regr_avgx, :regr_avgy, :regr_count, :regr_intercept, :regr_r2, :regr_slope, :regr_sxx, :regr_sxy, :regr_syy, :release, :result, :return, :returns, :revoke, :right, :rollback, :rollup, :row, :row_number, :rows, :rpad, :rtrim, :running, :savepoint, :scope, :scroll, :search, :second, :seek, :select, :sensitive, :session_user, :set, :show, :similar, :sin, :sinh, :skip, :smallint, :some, :specific, :specifictype, :sql, :sqlexception, :sqlstate, :sqlwarning, :sqrt, :start, :static, :stddev_pop, :stddev_samp, :submultiset, :subset, :substring, :substring_regex, :succeeds, :sum, :symmetric, :system, :system_time, :system_user, :table, :tablesample, :tan, :tanh, :then, :time, :timestamp, :timezone_hour, :timezone_minute, :to, :trailing, :translate, :translate_regex, :translation, :treat, :trigger, :trim, :trim_array, true, :truncate, :uescape, :union, :unique, :unknown, :unnest, :update, :upper, :user, :using, :value, :values, :value_of, :var_pop, :var_samp, :varbinary, :varchar, :varying, :versioning, :when, :whenever, :where, :width_bucket, :window, :with, :within, :without, :year, :a, :absolute, :action, :ada, :add, :admin, :after, :always, :asc, :assertion, :assignment, :attribute, :attributes, :before, :bernoulli, :breadth, :c, :cascade, :catalog, :catalog_name, :chain, :chaining, :character_set_catalog, :character_set_name, :character_set_schema, :characteristics, :characters, :class_origin, :cobol, :collation, :collation_catalog, :collation_name, :collation_schema, :columns, :column_name, :command_function, :command_function_code, :committed, :conditional, :condition_number, :connection, :connection_name, :constraint_catalog, :constraint_name, :constraint_schema, :constraints, :constructor, :continue, :copartition, :cursor_name, :data, :datetime_interval_code, :datetime_interval_precision, :defaults, :deferrable, :deferred, :defined, :definer, :degree, :depth, :derived, :desc, :descriptor, :diagnostics, :dispatch, :domain, :dynamic_function, :dynamic_function_code, :encoding, :enforced, :error, :exclude, :excluding, :expression, :final, :finish, :first, :flag, :following, :format, :fortran, :found, :fulfill, :g, :general, :generated, :go, :goto, :granted, :hierarchy, :ignore, :immediate, :immediately, :implementation, :including, :increment, :initially, :input, :instance, :instantiable, :instead, :invoker, :isolation, :k, :keep, :key, :keys, :key_member, :key_type, :last, :length, :level, :locator, :m, :map, :matched, :maxvalue, :measures, :message_length, :message_octet_length, :message_text, :minvalue, :more, :mumps, :name, :names, :nested, :nesting, :next, :nfc, :nfd, :nfkc, :nfkd, :normalized, :null_ordering, :nullable, :nulls, :number, :object, :occurrence, :octets, :option, :options, :ordering, :ordinality, :others, :output, :overflow, :overriding, :p, :pad, :parameter_mode, :parameter_name, :parameter_ordinal_position, :parameter_specific_catalog, :parameter_specific_name, :parameter_specific_schema, :partial, :pascal, :pass, :passing, :past, :path, :permute, :pipe, :placing, :plan, :pli, :preceding, :preserve, :prev, :prior, :private, :privileges, :prune, :public, :quotes, :read, :relative, :repeatable, :respect, :restart, :restrict, :returned_cardinality, :returned_length, :returned_octet_length, :returned_sqlstate, :returning, :role, :routine, :routine_catalog, :routine_name, :routine_schema, :row_count, :scalar, :scale, :schema, :schema_name, :scope_catalog, :scope_name, :scope_schema, :section, :security, :self, :semantics, :sequence, :serializable, :server_name, :session, :sets, :simple, :size, :sort_direction, :source, :space, :specific_name, :state, :statement, :string, :structure, :style, :subclass_origin, :t, :table_name, :temporary, :through, :ties, :top_level_count, :transaction, :transaction_active, :transactions_committed, :transactions_rolled_back, :transform, :transforms, :trigger_catalog, :trigger_name, :trigger_schema, :type, :unbounded, :uncommitted, :unconditional, :under, :unmatched, :unnamed, :usage, :user_defined_type_catalog, :user_defined_type_code, :user_defined_type_name, :user_defined_type_schema, :utf16, :utf32, :utf8, :view, :work, :wrapper, :write, :zone, :limit, :ilike, :backward, :forward, :isnull, :notnull] 28 | defguard is_not(node) when elem(node, 0) == :not and elem(node, 2) == [] 29 | defguard is_join(node) when elem(node, 0) == :join 30 | defguard is_parens(node) when elem(node, 0) == :parens 31 | defguard is_operator(node) when elem(node, 0) in ~w[operator :: + - * / ^ % & += -= *= /= %= &= ^-= |*= <=> || as <> <= >= != < > !< !> = like ilike in all any is isnull notnull between]a 32 | defguard is_of(node) when elem(node, 0) == :of 33 | defguard is_is(node) when elem(node, 0) == :is 34 | defguard is_on(node) when elem(node, 0) == :on 35 | defguard is_select(node) when elem(node, 0) == :select 36 | 37 | def predicate([l, c, r]) when is_boolean(l) and is_conditional(c) and is_boolean(r) do 38 | {elem(c, 0), elem(c, 1), [l, r]} 39 | end 40 | def predicate([l, b]) when is_boolean(b) do 41 | [{elem(b, 0), elem(b, 1), [l | elem(b, 2)]}] 42 | end 43 | def predicate([l, b, r | rest]) when is_boolean(b) or is_operator(b) do 44 | predicate([{elem(b, 0), elem(b, 1), [l, r]} | rest]) 45 | end 46 | def predicate([{_, _, _}, node | _] = unit) when is_comma(node) do 47 | unit 48 | end 49 | def predicate([l, b, r, c | rest]) when is_comma(c) and (is_boolean(b) or is_operator(b)) do 50 | [{elem(b, 0), elem(b, 1), [l, r]}, c | rest] 51 | end 52 | def predicate([l, c, r, c2 | rest]) when is_boolean(l) and is_conditional(c) and is_boolean(r) and is_conditional(c2) do 53 | predicate([{elem(c, 0), elem(c, 1), [l, r]}, c2 | rest]) 54 | end 55 | def predicate([f, c, l, b, r, c2 | rest]) when is_boolean(b) and is_conditional(c) and is_conditional(c2) do 56 | predicate([f, c, {elem(b, 0), elem(b, 1), [l, r]}, c2 | rest]) 57 | end 58 | def predicate([f, c, l, b, r]) when is_boolean(b) and is_conditional(c) do 59 | predicate([f, c, {elem(b, 0), elem(b, 1), [l, r]}]) 60 | end 61 | def predicate([l, b, r, c | rest]) when is_boolean(b) and is_conditional(c) do 62 | predicate([{elem(b, 0), elem(b, 1), [l, r]}, c | rest]) 63 | end 64 | def predicate(unit) do 65 | unit 66 | end 67 | 68 | 69 | def insert_node(node, unit, acc, context, root) when is_parens(node) do 70 | {[{elem(node, 0), elem(node, 1), parse(elem(node, 2))} | unit], acc, context, root} 71 | end 72 | def insert_node(node, [{:in = tag, meta, []}, right, {:using, _, _} = using | unit], acc, context, root) do 73 | {[{tag, meta, [node, [right, using | unit]]}], acc, context, root} 74 | end 75 | def insert_node({:in, _, _} = node, [_, {:using, _, _}|_] = unit, acc, context, root) do 76 | {[node | unit], acc, context, root} 77 | end 78 | def insert_node({:into = tag, meta, _}, [_] = unit, acc, context, root) do 79 | {[{tag, meta, unit}], acc, context, root} 80 | end 81 | def insert_node(node, [n, b, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_not(n) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 82 | {[{elem(b, 0), elem(b, 1), [{elem(n, 0), elem(n, 1), [node]}, {elem(c, 0), elem(c, 1), [r, l]}]} | unit], acc, context, root} 83 | end 84 | def insert_node(node, [n, b, s, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_not(n) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 85 | {[{elem(b, 0), elem(b, 1), [{elem(n, 0), elem(n, 1), [node]}, {elem(s, 0), elem(s, 1), [{elem(c, 0), elem(c, 1), [r, l]}]}]} | unit], acc, context, root} 86 | end 87 | def insert_node(node, [b, s, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 88 | {[{elem(b, 0), elem(b, 1), [node, {elem(s, 0), elem(s, 1), [{elem(c, 0), elem(c, 1), [r, l]}]}]} | unit], acc, context, root} 89 | end 90 | def insert_node(node, [b, r, c, l | unit], acc, context, root) when is_between(b) and is_and(c) and is_data_type(r) and is_data_type(l) and is_data_type(node) do 91 | {[{elem(b, 0), elem(b, 1), [node, {elem(c, 0), elem(c, 1), [r, l]}]} | unit], acc, context, root} 92 | end 93 | def insert_node(node, [b, l, c | unit], acc, context, root) when is_data_type(node) and is_operator(b) and is_data_type(l) and is_conditional(c) do 94 | {[{elem(b, 0), elem(b, 1), [node, l]}, c | unit], acc, context, root} 95 | end 96 | def insert_node(node, [r, b, l | unit], acc, context, root) when is_conditional(node) and is_data_type(r) and is_operator(b) and is_data_type(l) do 97 | {[node, {elem(b, 0), elem(b, 1), [r, l]} | unit], acc, context, root} 98 | end 99 | def insert_node(node, [o, l], acc, context, root) when is_data_type(node) and is_operator(o) and is_data_type(l) do 100 | {[{elem(o, 0), elem(o, 1), [node, l]}], acc, context, root} 101 | end 102 | def insert_node(node, [u | unit], acc, context, root) when is_not(node) and elem(u, 0) in ~w[false true unknown null]a do 103 | {[{elem(node, 0), elem(node, 1), [u]} | unit], acc, context, root} 104 | end 105 | def insert_node(node, [u | unit], acc, context, root) when is_not(u) and is_data_type(node) do 106 | {[{elem(u, 0), elem(u, 1), [node | unit]}], acc, context, root} 107 | end 108 | def insert_node({:into = tag, meta, []}, [ident, parens, values], acc, context, root) do 109 | {[], [{tag, meta, [ident, parens, values]} | acc], context, root} 110 | end 111 | def insert_node({tag, meta, []}, [ident, parens], acc, context, root) when tag in ~w[into table]a do 112 | {[], [{tag, meta, [ident, parens]} | acc], context, root} 113 | end 114 | def insert_node({:add = tag, meta, []}, [ident, type], acc, context, root) do 115 | {[], [{tag, meta, [ident, type]} | acc], context, root} 116 | end 117 | def insert_node({:type = tag, meta, []}, [ident, as, type], acc, context, root) do 118 | {[], [{tag, meta, [{elem(as, 0), elem(as, 1), [ident, type]}]} | acc], context, root} 119 | end 120 | def insert_node({tag, meta, []}, [ident], acc, context, root) when tag in ~w[type table]a do 121 | {[], [{tag, meta, [ident]} | acc], context, root} 122 | end 123 | def insert_node({:with = tag, meta, []}, [{:recursive = t, m, []}, {:ident, _, _} = l, {:parens, _, _} = r, {:as = t2, m2, a}], [], context, root) do 124 | {[], [], context, root ++ [{tag, meta, [{t2, m2, [{t, m, [l, r]} | a]}]}]} 125 | end 126 | def insert_node({:with = tag, meta, []}, [{:ident, _, _} = l, {:parens, _, _} = r, {:as = t2, m2, a}], [], context, root) do 127 | {[], [], context, root ++ [{tag, meta, [{t2, m2, [[l, r] | a]}]}]} 128 | end 129 | def insert_node({:with = tag, meta, []}, [{:ident, _, _}, {:as, _, _}] = unit, acc, context, root) do 130 | {[], [], context, root ++ [{tag, meta, unit ++ acc}]} 131 | end 132 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[by in references]a do 133 | {[{tag, meta, predicate(unit ++ acc)}], [], context, root} 134 | end 135 | def insert_node(node, [n|_] = unit, acc, context, root) when (is_on(n) or is_of(n)) and elem(node, 0) in ~w[select insert update delete truncate references trigger create connect temporary execute usage set alter system maintain]a do 136 | {[node|unit], acc, context, root} 137 | end 138 | def insert_node(node, [_, n|_] = unit, acc, context, root) when is_for(n) and is_from(node) do 139 | {[node|unit], acc, context, root} 140 | end 141 | def insert_node(node, [_, _, _, n|_] = unit, acc, context, root) when is_for(n) and is_select(node) do 142 | {[node|unit], acc, context, root} 143 | end 144 | def insert_node(node, [] = unit, [] = acc, [] = context, root) when elem(node, 0) in ~w[create drop insert alter update delete start set open close commit rollback]a do 145 | {[node | unit], acc, context, root} 146 | end 147 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[create drop insert alter update delete start set open close commit rollback]a do 148 | {[], [], context, [{tag, meta, List.wrap(predicate(unit ++ acc))} | root]} 149 | end 150 | def insert_node(node, [n |_] = unit, acc, context, root) when is_grant(node) and elem(n, 0) == :option do 151 | {[node | unit], acc, context, root} 152 | end 153 | def insert_node(node, unit, acc, context, root) when is_grant(node) or is_revoke(node) or is_declare(node) do 154 | {[], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]} 155 | end 156 | def insert_node({:distinct = tag, meta, []}, [{:on, _, _} = on | unit], acc, context, root) do 157 | {[{tag, meta, [on]} | unit], acc, context, root} 158 | end 159 | def insert_node(node, [u | unit], acc, context, root) when is_fetch_dir(node) and elem(u, 0) != :in do 160 | {[{elem(node, 0), elem(node, 1), [u]}], unit++acc, context, root} 161 | end 162 | def insert_node(node, [u | unit], acc, context, root) when is_fetch(node) do 163 | {[], [], context, [{elem(node, 0), elem(node, 1), [u]} | unit ++ acc ++ root]} 164 | end 165 | def insert_node(node, [on], [], context, root) when is_join(node) and is_on(on) do 166 | {[], [], context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [on]} | root]} 167 | end 168 | def insert_node(node, [ident, on], [] = acc, context, root) when is_join(node) and is_on(on) do 169 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [ident | elem(on, 2)]}]} | root]} 170 | end 171 | def insert_node(node, [ident, as, on | unit], [] = acc, context, root) when is_join(node) and is_on(on) do 172 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [[ident, as]] ++ elem(on, 2) ++ unit}]} | root]} 173 | end 174 | def insert_node(node, [ident, on | unit], [] = acc, context, root) when is_join(node) and is_on(on) do 175 | {[], acc, context, [{elem(node, 0), elem(node, 1), elem(node, 2) ++ [{elem(on, 0), elem(on, 1), [ident] ++ elem(on, 2) ++ unit}]} | root]} 176 | end 177 | def insert_node(node, unit, acc, context, root) when is_join(node) do 178 | a = elem(node, 2) 179 | acc = unit ++ acc 180 | acc = if a == [], do: acc, else: a ++ [acc] 181 | {[], [], context, [{elem(node, 0), elem(node, 1), acc} | root]} 182 | end 183 | def insert_node({tag, meta, []}, unit, acc, context, root) when tag in ~w[select from where group having order limit offset]a do 184 | {[], [], context, [{tag, meta, List.wrap(predicate(unit ++ acc))} | root]} 185 | end 186 | def insert_node(node, unit, acc, context, {:colon, meta, []}) do 187 | {unit, acc, context, {:colon, meta, [node]}} 188 | end 189 | def insert_node(node, [parens | unit], acc, context, root) when is_parens(parens) and is_keyword(node) do 190 | {[{elem(node, 0), elem(node, 1), [parens]} | unit], acc, context, root} 191 | end 192 | def insert_node(node, unit, acc, context, root) do 193 | {[node | unit], acc, context, root} 194 | end 195 | 196 | def parse(tokens) do 197 | parse(tokens, [], [], [], []) 198 | end 199 | def parse([], [], [], [], root) do 200 | root 201 | end 202 | def parse([], unit, acc, [], []) do 203 | predicate(unit ++ acc) 204 | end 205 | def parse([], unit, acc, [], root) do 206 | predicate(unit ++ acc) ++ root 207 | end 208 | def parse([], unit, acc, context, root) when is_tuple(context) do 209 | [{elem(context, 0), elem(context, 1), [unit ++ acc ++ root, elem(context, 2)]}] 210 | end 211 | def parse([node | rest], unit, acc, context, root) when is_comment(node) do 212 | parse(rest, unit, acc, context, [node | root]) 213 | end 214 | def parse([{:all, m, _}, node | rest], unit, acc, [], root) when is_combinator(node) do 215 | parse(rest, [], [], {elem(node, 0), elem(node, 1), [{:all, m, unit ++ acc ++ root}]}, []) 216 | end 217 | def parse([node | rest], unit, acc, [], root) when is_combinator(node) do 218 | parse(rest, [], [], {elem(node, 0), elem(node, 1), unit ++ acc ++ root}, []) 219 | end 220 | def parse([node | rest], unit, acc, context, root) when is_colon(node) do 221 | parse(rest, [], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]) 222 | end 223 | def parse([ident, from, distinct, n, is, left | rest], unit, acc, context, root) when is_is(is) and is_from(from) and is_distinct(distinct) do 224 | node = {elem(is, 0), elem(is, 1), [left, {elem(n, 0), elem(n, 1), [{elem(distinct, 0), elem(distinct, 1), [{elem(from, 0), elem(from, 1), [ident]}]}]}]} 225 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 226 | parse(rest, unit, acc, context, root) 227 | end 228 | def parse([ident, from, distinct, is, left | rest], unit, acc, context, root) when is_is(is) and is_from(from) and is_distinct(distinct) do 229 | node = {elem(is, 0), elem(is, 1), [left, {elem(distinct, 0), elem(distinct, 1), [{elem(from, 0), elem(from, 1), [ident]}]}]} 230 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 231 | parse(rest, unit, acc, context, root) 232 | end 233 | def parse([node | rest], unit, acc, context, root) when is_colon(node) do 234 | parse(rest, [], [], context, [{elem(node, 0), elem(node, 1), unit ++ acc ++ root}]) 235 | end 236 | def parse([parens, node | rest], unit, acc, [], root) when is_parens(parens) and is_combinator(node) do 237 | parse(rest, unit, acc, {elem(node, 0), elem(node, 1), [{elem(parens, 0), elem(parens, 1), parse(elem(parens, 2))}]}, root) 238 | end 239 | def parse([node | rest], unit, acc, context, root) when is_comma(node) do 240 | parse(rest, [], [{elem(node, 0), elem(node, 1), predicate(unit)} | acc], context, root) 241 | end 242 | def parse([node | rest], unit, acc, context, root) do 243 | {unit, acc, context, root} = insert_node(node, unit, acc, context, root) 244 | parse(rest, unit, acc, context, root) 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /lib/sql.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL do 5 | @moduledoc "README.md" 6 | |> File.read!() 7 | |> String.split("") 8 | |> Enum.fetch!(1) 9 | @moduledoc since: "0.1.0" 10 | 11 | @adapters [SQL.Adapters.ANSI, SQL.Adapters.MySQL, SQL.Adapters.Postgres, SQL.Adapters.TDS] 12 | 13 | defmacro __using__(opts) do 14 | quote bind_quoted: [opts: opts] do 15 | @doc false 16 | @behaviour SQL 17 | import SQL 18 | @sql_adapter opts[:adapter] 19 | def sql_config, do: unquote(opts) 20 | def token_to_sql(token), do: token_to_sql(token) 21 | defoverridable token_to_sql: 1 22 | end 23 | end 24 | @optional_callbacks token_to_sql: 1 25 | 26 | @doc """ 27 | Returns a SQL string for a given token. 28 | """ 29 | @doc since: "0.1.0" 30 | @doc deprecated: "Use SQL.Token.token_to_string/1 instead" 31 | @callback token_to_sql(token :: {atom, keyword, list}) :: String.t() 32 | 33 | defstruct [:tokens, :params, :module, :id, :string, :inspect] 34 | 35 | defimpl Inspect, for: SQL do 36 | def inspect(sql, _opts), do: Inspect.Algebra.concat(["~SQL\"\"\"\n", sql.inspect, "\n\"\"\""]) 37 | end 38 | 39 | defimpl String.Chars, for: SQL do 40 | def to_string(sql), do: sql.string 41 | end 42 | 43 | @doc """ 44 | Returns a parameterized SQL. 45 | 46 | ## Examples 47 | iex(1)> email = "john@example.com" 48 | iex(2)> SQL.to_sql(~SQL"select id, email from users where email = {{email}}") 49 | {"select id, email from users where email = ?", ["john@example.com"]} 50 | """ 51 | @doc since: "0.1.0" 52 | def to_sql(sql), do: {sql.string, sql.params} 53 | 54 | @doc """ 55 | Handles the sigil `~SQL` for SQL. 56 | 57 | It returns a `%SQL{}` struct that can be transformed to a parameterized query. 58 | 59 | ## Examples 60 | iex(1)> ~SQL"from users select id, email" 61 | ~SQL"\"\" 62 | from users select id, email 63 | "\"\" 64 | """ 65 | @doc since: "0.1.0" 66 | defmacro sigil_SQL(left \\ [], right, modifiers) do 67 | SQL.build(left, right, modifiers, __CALLER__) 68 | end 69 | 70 | @doc false 71 | @doc since: "0.1.0" 72 | def parse(binary) do 73 | {:ok, _opts, _, _, _, _, tokens} = SQL.Lexer.lex(binary, __ENV__.file, 0, [format: true]) 74 | tokens 75 | |> SQL.Parser.parse() 76 | |> to_query() 77 | |> to_string(SQL.Adapters.ANSI) 78 | end 79 | 80 | @doc false 81 | @doc since: "0.1.0" 82 | @acc ~w[for create drop insert alter with update delete select set fetch from join where group having window except intersect union order limit offset lock colon in declare start grant revoke commit rollback open close comment comments into]a 83 | def to_query([value | _] = tokens) when is_tuple(value) and elem(value, 0) in @acc do 84 | Enum.reduce(@acc, [], fn key, acc -> acc ++ for {k, meta, v} <- Enum.filter(tokens, &(elem(&1, 0) == key)), do: {k, meta, Enum.map(v, &to_query/1)} end) 85 | end 86 | def to_query({:parens = tag, meta, values}) do 87 | {tag, meta, to_query(values)} 88 | end 89 | def to_query({tag, meta, values}) do 90 | {tag, meta, Enum.map(values, &to_query/1)} 91 | end 92 | def to_query(tokens) when is_list(tokens) do 93 | Enum.map(tokens, &to_query/1) 94 | end 95 | def to_query(token) do 96 | token 97 | end 98 | 99 | @doc false 100 | def to_string(tokens, module) when module in @adapters do 101 | tokens 102 | |> Enum.reduce([], fn 103 | token, [] = acc -> [acc | module.token_to_string(token)] 104 | token, acc -> 105 | case module.token_to_string(token) do 106 | <<";", _::binary>> = v -> [acc | v] 107 | v -> [acc, " " | v] 108 | end 109 | end) 110 | |> IO.iodata_to_binary() 111 | end 112 | def to_string(tokens, module) do 113 | fun = cond do 114 | Kernel.function_exported?(module, :sql_config, 0) -> &module.sql_config()[:adapter].token_to_string(&1) 115 | Kernel.function_exported?(module, :token_to_string, 2) -> &module.token_to_string(&1) 116 | true -> &SQL.String.token_to_sql(&1) 117 | end 118 | tokens 119 | |> Enum.reduce([], fn 120 | token, [] = acc -> [acc | fun.(token)] 121 | token, acc -> 122 | case fun.(token) do 123 | <<";", _::binary>> = v -> [acc | v] 124 | v -> [acc, " " | v] 125 | end 126 | end) 127 | |> IO.iodata_to_binary() 128 | end 129 | 130 | @doc false 131 | def build(left, {:<<>>, _, _} = right, _modifiers, env) do 132 | case build(left, right) do 133 | {:static, data} -> 134 | {:ok, opts, _, _, _, _, tokens} = SQL.Lexer.lex(data, env.file) 135 | tokens = SQL.to_query(SQL.Parser.parse(tokens)) 136 | string = if mod = env.module do 137 | SQL.to_string(tokens, Module.get_attribute(mod, :sql_adapter)) 138 | else 139 | SQL.to_string(tokens, SQL.Adapters.ANSI) 140 | end 141 | sql = struct(SQL, tokens: tokens, string: string, module: env.module, inspect: data, id: id(data)) 142 | quote bind_quoted: [params: opts[:binding], sql: Macro.escape(sql)] do 143 | %{sql | params: cast_params(params, [], binding())} 144 | end 145 | 146 | {:dynamic, data} -> 147 | sql = struct(SQL, id: id(data), module: env.module) 148 | quote bind_quoted: [left: Macro.unpipe(left), right: right, file: env.file, data: data, sql: Macro.escape(sql)] do 149 | {t, p} = Enum.reduce(left, {[], []}, fn 150 | {[], 0}, acc -> acc 151 | {v, 0}, {t, p} -> {t ++ v.tokens, p ++ v.params} 152 | end) 153 | {tokens, params} = tokens(right, file, length(p), sql.id) 154 | tokens = t ++ tokens 155 | %{sql | params: cast_params(params, p, binding()), tokens: tokens, string: plan(tokens, sql.id, sql.module), inspect: plan_inspect(data, sql.id)} 156 | end 157 | end 158 | end 159 | 160 | @doc false 161 | def build(left, {:<<>>, _, right}) do 162 | left 163 | |> Macro.unpipe() 164 | |> Enum.reduce({:static, right}, fn 165 | {[], 0}, acc -> acc 166 | {{:sigil_SQL, _meta, [{:<<>>, _, value}, []]}, 0}, {type, acc} -> {type, [value, ?\s, acc]} 167 | {{_, _, _} = var, 0}, {_, acc} -> {:dynamic, [var, ?\s, acc]} 168 | end) 169 | |> case do 170 | {:static, data} -> {:static, IO.iodata_to_binary(data)} 171 | {:dynamic, data} -> {:dynamic, data} 172 | end 173 | end 174 | 175 | @doc false 176 | def id(data) do 177 | if id = :persistent_term.get(data, nil) do 178 | id 179 | else 180 | id = System.unique_integer([:positive]) 181 | :persistent_term.put(data, id) 182 | id 183 | end 184 | end 185 | 186 | @doc false 187 | def cast_params(bindings, params, binding) do 188 | Enum.reduce(bindings, params, fn 189 | {:var, var}, acc -> if v = binding[String.to_atom(var)], do: acc ++ [v], else: acc 190 | {:code, code}, acc -> acc ++ [elem(Code.eval_string(code, binding), 0)] 191 | end) 192 | end 193 | 194 | @doc false 195 | def tokens(binary, file, count, id) do 196 | key = {id, :lex} 197 | if result = :persistent_term.get(key, nil) do 198 | result 199 | else 200 | {:ok, opts, _, _, _, _, tokens} = SQL.Lexer.lex(binary, file, count) 201 | result = {tokens, opts[:binding]} 202 | :persistent_term.put(key, result) 203 | result 204 | end 205 | end 206 | 207 | @doc false 208 | def plan(tokens, id, module) do 209 | key = {module, id, :plan} 210 | if string = :persistent_term.get(key, nil) do 211 | string 212 | else 213 | string = to_string(SQL.to_query(SQL.Parser.parse(tokens)), module) 214 | :persistent_term.put(key, string) 215 | string 216 | end 217 | end 218 | 219 | @doc false 220 | def plan_inspect(data, id) do 221 | key = {id, :inspect} 222 | if inspect = :persistent_term.get(key, nil) do 223 | inspect 224 | else 225 | inspect = data 226 | |> Enum.map(fn 227 | ast when is_struct(ast) -> ast.inspect 228 | x -> x 229 | end) 230 | |> IO.iodata_to_binary() 231 | 232 | 233 | :persistent_term.put(key, inspect) 234 | inspect 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /lib/string.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.String do 5 | @moduledoc false 6 | 7 | @doc false 8 | def token_to_sql(value, mod \\ __MODULE__) 9 | def token_to_sql(value, _mod) when is_struct(value) do 10 | to_string(value) 11 | end 12 | def token_to_sql({tag, _, [{:parens, _, _} = value]}, mod) when tag in ~w[integer float update]a do 13 | "#{mod.token_to_sql(tag)}#{mod.token_to_sql(value)}" 14 | end 15 | def token_to_sql({tag, _, value}, _mod) when tag in ~w[ident integer float]a do 16 | "#{value}" 17 | end 18 | def token_to_sql({tag, _}, mod) do 19 | mod.token_to_sql(tag) 20 | end 21 | def token_to_sql({:comment, _, value}, _mod) do 22 | "-- #{value}" 23 | end 24 | def token_to_sql({:comments, _, value}, _mod) do 25 | "\\* #{value} *\\" 26 | end 27 | def token_to_sql({:double_quote, _, value}, _mod) do 28 | "\"#{value}\"" 29 | end 30 | def token_to_sql({:quote, _, value}, _mod) do 31 | "'#{value}'" 32 | end 33 | def token_to_sql({:parens, _, value}, mod) do 34 | "(#{mod.token_to_sql(value)})" 35 | end 36 | def token_to_sql({:bracket, _, value}, mod) do 37 | "[#{mod.token_to_sql(value)}]" 38 | end 39 | def token_to_sql({:colon, _, value}, mod) do 40 | "; #{mod.token_to_sql(value)}" 41 | end 42 | def token_to_sql({:comma, _, value}, mod) do 43 | ", #{mod.token_to_sql(value)}" 44 | end 45 | def token_to_sql({tag, _, []}, mod) do 46 | mod.token_to_sql(tag) 47 | end 48 | def token_to_sql({tag, _, [[_ | _] = left, right]}, mod) when tag in ~w[join]a do 49 | "#{mod.token_to_sql(left)} #{mod.token_to_sql(tag)} #{mod.token_to_sql(right)}" 50 | end 51 | def token_to_sql({tag, _, [{:with = t, _, [left, right]}]}, mod) when tag in ~w[to]a do 52 | "#{mod.token_to_sql(tag)} #{mod.token_to_sql(left)} #{mod.token_to_sql(t)} #{mod.token_to_sql(right)}" 53 | end 54 | def token_to_sql({tag, _, value}, mod) when tag in ~w[select from fetch limit where order offset group having with join by distinct create type drop insert alter table add into delete update start grant revoke set declare open close commit rollback references recursive]a do 55 | "#{mod.token_to_sql(tag)} #{mod.token_to_sql(value)}" 56 | end 57 | def token_to_sql({:on = tag, _, [source, as, value]}, mod) do 58 | "#{mod.token_to_sql(source)} #{mod.token_to_sql(as)} #{mod.token_to_sql(tag)} #{mod.token_to_sql(value)}" 59 | end 60 | def token_to_sql({tag, _, [left, [{:all = t, _, right}]]}, mod) when tag in ~w[union except intersect]a do 61 | "#{mod.token_to_sql(left)} #{mod.token_to_sql(tag)} #{mod.token_to_sql(t)} #{mod.token_to_sql(right)}" 62 | end 63 | def token_to_sql({:between = tag, _, [{:not = t, _, right}, left]}, mod) do 64 | "#{mod.token_to_sql(right)} #{mod.token_to_sql(t)} #{mod.token_to_sql(tag)} #{mod.token_to_sql(left)}" 65 | end 66 | def token_to_sql({tag, _, [left, right]}, mod) when tag in ~w[:: [\] <> <= >= != || + - ^ * / % < > = like ilike as union except intersect between and or on is not in cursor for to]a do 67 | "#{mod.token_to_sql(left)} #{mod.token_to_sql(tag)} #{mod.token_to_sql(right)}" 68 | end 69 | def token_to_sql({tag, _, [{:parens, _, _} = value]}, mod) when tag not in ~w[in on]a do 70 | "#{mod.token_to_sql(tag)}#{mod.token_to_sql(value)}" 71 | end 72 | def token_to_sql({tag, _, values}, mod) when tag in ~w[not all between symmetric absolute relative forward backward on in for without]a do 73 | "#{mod.token_to_sql(tag)} #{mod.token_to_sql(values)}" 74 | end 75 | def token_to_sql({tag, _, [left, right]}, mod) when tag in ~w[.]a do 76 | "#{mod.token_to_sql(left)}.#{mod.token_to_sql(right)}" 77 | end 78 | def token_to_sql({tag, _, [left]}, mod) when tag in ~w[not]a do 79 | "#{mod.token_to_sql(left)} #{mod.token_to_sql(tag)}" 80 | end 81 | def token_to_sql({tag, _, [left]}, mod) when tag in ~w[asc desc isnull notnull]a do 82 | "#{mod.token_to_sql(left)} #{mod.token_to_sql(tag)}" 83 | end 84 | def token_to_sql({:binding, _, [idx]}, _mod) when is_integer(idx) do 85 | "?" 86 | end 87 | def token_to_sql({:binding, _, value}, _mod) do 88 | "{{#{value}}}" 89 | end 90 | def token_to_sql(:asterisk, _mod) do 91 | "*" 92 | end 93 | def token_to_sql(value, _mod) when is_atom(value) do 94 | "#{value}" 95 | end 96 | def token_to_sql(value, _mod) when is_binary(value) do 97 | "'#{value}'" 98 | end 99 | def token_to_sql(values, mod) when is_list(values) do 100 | values 101 | |> Enum.reduce([], fn 102 | token, [] = acc -> [acc | mod.token_to_sql(token, mod)] 103 | {:comma, _, _} = token, acc -> [acc | mod.token_to_sql(token, mod)] 104 | token, acc -> [acc, " " | mod.token_to_sql(token, mod)] 105 | end) 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/token.ex: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Token do 5 | @moduledoc false 6 | 7 | @doc """ 8 | Returns a SQL string for a given token. 9 | """ 10 | @doc since: "0.2.0" 11 | @callback token_to_string(token :: {atom, keyword, list} | {atom, keyword}) :: String.t() 12 | 13 | defmacro __using__(opts) do 14 | quote bind_quoted: [opts: opts] do 15 | @doc false 16 | @behaviour SQL.Token 17 | def token_to_string(token), do: SQL.Adapters.ANSI.token_to_string(token, __MODULE__) 18 | defoverridable token_to_string: 1 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.MixProject do 5 | use Mix.Project 6 | 7 | @version "0.2.0" 8 | 9 | def project do 10 | [ 11 | app: :sql, 12 | version: @version, 13 | elixir: "~> 1.18", 14 | deps: deps(), 15 | description: "Brings an extensible SQL parser and sigil to Elixir, confidently write SQL with automatic parameterized queries.", 16 | name: "SQL", 17 | docs: docs(), 18 | package: package(), 19 | aliases: ["sql.bench": "run bench.exs"] 20 | ] 21 | end 22 | 23 | defp package do 24 | %{ 25 | licenses: ["Apache-2.0"], 26 | maintainers: ["Benjamin Schultzer"], 27 | links: %{"GitHub" => "https://github.com/elixir-dbvisor/sql"} 28 | } 29 | end 30 | 31 | defp docs do 32 | [ 33 | main: "readme", 34 | api_reference: false, 35 | source_ref: "v#{@version}", 36 | canonical: "https://hexdocs.pm/sql", 37 | extras: ["CHANGELOG.md", "README.md", "LICENSE"] 38 | ] 39 | end 40 | 41 | defp deps do 42 | [ 43 | {:benchee, "~> 1.3", only: :dev}, 44 | {:ecto_sql, "~> 3.12", only: [:dev, :test]}, 45 | {:ex_doc, "~> 0.37", only: :dev}, 46 | {:postgrex, ">= 0.0.0", only: [:dev, :test]}, 47 | {:yamerl, ">= 0.0.0", only: [:dev, :test]}, 48 | ] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, 3 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 4 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 5 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 7 | "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, 9 | "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 | "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, 15 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 16 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 17 | "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, 18 | } 19 | -------------------------------------------------------------------------------- /test/adapters/ansi_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.ANSITest do 5 | use ExUnit.Case, async: true 6 | use SQL, adapter: SQL.Adapters.ANSI 7 | 8 | describe "with" do 9 | test "recursive" do 10 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 11 | end 12 | 13 | test "regular" do 14 | assert "with temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 15 | end 16 | end 17 | 18 | describe "combinations" do 19 | test "except" do 20 | assert "(select id from users) except (select id from users)" == to_string(~SQL[(select id from users) except (select id from users)]) 21 | assert "(select id from users) except select id from users" == to_string(~SQL[(select id from users) except select id from users]) 22 | assert "select id from users except (select id from users)" == to_string(~SQL[select id from users except (select id from users)]) 23 | assert "select id from users except select id from users" == to_string(~SQL[select id from users except select id from users]) 24 | 25 | assert "(select id from users) except all (select id from users)" == to_string(~SQL[(select id from users) except all (select id from users)]) 26 | assert "(select id from users) except all select id from users" == to_string(~SQL[(select id from users) except all select id from users]) 27 | assert "select id from users except all (select id from users)" == to_string(~SQL[select id from users except all (select id from users)]) 28 | assert "select id from users except all select id from users" == to_string(~SQL[select id from users except all select id from users]) 29 | end 30 | 31 | test "intersect" do 32 | assert "(select id from users) intersect (select id from users)" == to_string(~SQL[(select id from users) intersect (select id from users)]) 33 | assert "(select id from users) intersect select id from users" == to_string(~SQL[(select id from users) intersect select id from users]) 34 | assert "select id from users intersect (select id from users)" == to_string(~SQL[select id from users intersect (select id from users)]) 35 | assert "select id from users intersect select id from users" == to_string(~SQL[select id from users intersect select id from users]) 36 | 37 | assert "(select id from users) intersect all (select id from users)" == to_string(~SQL[(select id from users) intersect all (select id from users)]) 38 | assert "(select id from users) intersect all select id from users" == to_string(~SQL[(select id from users) intersect all select id from users]) 39 | assert "select id from users intersect all (select id from users)" == to_string(~SQL[select id from users intersect all (select id from users)]) 40 | assert "select id from users intersect all select id from users" == to_string(~SQL[select id from users intersect all select id from users]) 41 | end 42 | 43 | test "union" do 44 | assert "(select id from users) union (select id from users)" == to_string(~SQL[(select id from users) union (select id from users)]) 45 | assert "(select id from users) union select id from users" == to_string(~SQL[(select id from users) union select id from users]) 46 | assert "select id from users union (select id from users)" == to_string(~SQL[select id from users union (select id from users)]) 47 | assert "select id from users union select id from users" == to_string(~SQL[select id from users union select id from users]) 48 | 49 | assert "(select id from users) union all (select id from users)" == to_string(~SQL[(select id from users) union all (select id from users)]) 50 | assert "(select id from users) union all select id from users" == to_string(~SQL[(select id from users) union all select id from users]) 51 | assert "select id from users union all (select id from users)" == to_string(~SQL[select id from users union all (select id from users)]) 52 | assert "select id from users union all select id from users" == to_string(~SQL[select id from users union all select id from users]) 53 | end 54 | end 55 | 56 | describe "query" do 57 | test "select" do 58 | assert "select id" == to_string(~SQL[select id]) 59 | assert "select id, id as di" == to_string(~SQL[select id, id as di]) 60 | assert "select id, (select id from users) as di" == to_string(~SQL[select id, (select id from users) as di]) 61 | assert "select unknownn" == to_string(~SQL[select unknownn]) 62 | assert "select truee" == to_string(~SQL[select truee]) 63 | assert "select falsee" == to_string(~SQL[select falsee]) 64 | assert "select nulll" == to_string(~SQL[select nulll]) 65 | assert "select isnulll" == to_string(~SQL[select isnulll]) 66 | assert "select notnulll" == to_string(~SQL[select notnulll]) 67 | assert "select ascc" == to_string(~SQL[select ascc]) 68 | assert "select descc" == to_string(~SQL[select descc]) 69 | assert "select distinct id" == to_string(~SQL[select distinct id]) 70 | assert "select distinct on (id, users) id" == to_string(~SQL[select distinct on (id, users) id]) 71 | end 72 | 73 | test "from" do 74 | assert "from users" == to_string(~SQL[from users]) 75 | assert "from users u, persons p" == to_string(~SQL[from users u, persons p]) 76 | assert "from users u" == to_string(~SQL[from users u]) 77 | assert "from users as u" == to_string(~SQL[from users as u]) 78 | assert "from users u" == to_string(~SQL[from users u]) 79 | end 80 | 81 | test "join" do 82 | assert "inner join users" == to_string(~SQL[inner join users]) 83 | assert "join users" == to_string(~SQL[join users]) 84 | assert "left outer join users" == to_string(~SQL[left outer join users]) 85 | assert "left join users" == to_string(~SQL[left join users]) 86 | assert "natural join users" == to_string(~SQL[natural join users]) 87 | assert "full join users" == to_string(~SQL[full join users]) 88 | assert "cross join users" == to_string(~SQL[cross join users]) 89 | assert "join users u" == to_string(~SQL[join users u]) 90 | assert "join users on id = id" == to_string(~SQL[join users on id = id]) 91 | assert "join users u on id = id" == to_string(~SQL[join users u on id = id]) 92 | assert "join users on (id = id)" == to_string(~SQL[join users on (id = id)]) 93 | assert "join (select * from users) on (id = id)" == to_string(~SQL[join (select * from users) on (id = id)]) 94 | assert "join (select * from users) u on (id = id)" == to_string(~SQL[join (select * from users) u on (id = id)]) 95 | end 96 | 97 | test "where" do 98 | assert "where 1 = 2" == to_string(~SQL[where 1 = 2]) 99 | assert "where 1 = 2" == to_string(~SQL[where 1=2]) 100 | assert "where 1 != 2" == to_string(~SQL[where 1 != 2]) 101 | assert "where 1 <> 2" == to_string(~SQL[where 1 <> 2]) 102 | assert "where 1 = 2 and id = users.id and id > 3 or true" == to_string(~SQL[where 1 = 2 and id = users.id and id > 3 or true]) 103 | end 104 | 105 | test "group by" do 106 | assert "group by id" == to_string(~SQL[group by id]) 107 | assert "group by users.id" == to_string(~SQL[group by users.id]) 108 | assert "group by id, users.id" == to_string(~SQL[group by id, users.id]) 109 | end 110 | 111 | test "having" do 112 | assert "having 1 = 2" == to_string(~SQL[having 1 = 2]) 113 | assert "having 1 != 2" == to_string(~SQL[having 1 != 2]) 114 | assert "having 1 <> 2" == to_string(~SQL[having 1 <> 2]) 115 | end 116 | 117 | test "order by" do 118 | assert "order by id" == to_string(~SQL[order by id]) 119 | assert "order by users.id" == to_string(~SQL[order by users.id]) 120 | assert "order by id, users.id, users.id asc, id desc" == to_string(~SQL[order by id, users.id, users.id asc, id desc]) 121 | end 122 | 123 | test "offset" do 124 | assert "offset 1" == to_string(~SQL[offset 1]) 125 | end 126 | 127 | test "limit" do 128 | assert "limit 1" == to_string(~SQL[limit 1]) 129 | end 130 | 131 | test "fetch" do 132 | assert "fetch next from users" == to_string(~SQL[fetch next from users]) 133 | assert "fetch prior from users" == to_string(~SQL[fetch prior from users]) 134 | assert "fetch first from users" == to_string(~SQL[fetch first from users]) 135 | assert "fetch last from users" == to_string(~SQL[fetch last from users]) 136 | assert "fetch absolute 1 from users" == to_string(~SQL[fetch absolute 1 from users]) 137 | assert "fetch relative 1 from users" == to_string(~SQL[fetch relative 1 from users]) 138 | assert "fetch 1 from users" == to_string(~SQL[fetch 1 from users]) 139 | assert "fetch all from users" == to_string(~SQL[fetch all from users]) 140 | assert "fetch forward from users" == to_string(~SQL[fetch forward from users]) 141 | assert "fetch forward 1 from users" == to_string(~SQL[fetch forward 1 from users]) 142 | assert "fetch forward all from users" == to_string(~SQL[fetch forward all from users]) 143 | assert "fetch backward from users" == to_string(~SQL[fetch backward from users]) 144 | assert "fetch backward 1 from users" == to_string(~SQL[fetch backward 1 from users]) 145 | assert "fetch backward all from users" == to_string(~SQL[fetch backward all from users]) 146 | 147 | assert "fetch next in users" == to_string(~SQL[fetch next in users]) 148 | assert "fetch prior in users" == to_string(~SQL[fetch prior in users]) 149 | assert "fetch first in users" == to_string(~SQL[fetch first in users]) 150 | assert "fetch last in users" == to_string(~SQL[fetch last in users]) 151 | assert "fetch absolute 1 in users" == to_string(~SQL[fetch absolute 1 in users]) 152 | assert "fetch relative 1 in users" == to_string(~SQL[fetch relative 1 in users]) 153 | assert "fetch 1 in users" == to_string(~SQL[fetch 1 in users]) 154 | assert "fetch all in users" == to_string(~SQL[fetch all in users]) 155 | assert "fetch forward in users" == to_string(~SQL[fetch forward in users]) 156 | assert "fetch forward 1 in users" == to_string(~SQL[fetch forward 1 in users]) 157 | assert "fetch forward all in users" == to_string(~SQL[fetch forward all in users]) 158 | assert "fetch backward in users" == to_string(~SQL[fetch backward in users]) 159 | assert "fetch backward 1 in users" == to_string(~SQL[fetch backward 1 in users]) 160 | assert "fetch backward all in users" == to_string(~SQL[fetch backward all in users]) 161 | end 162 | end 163 | 164 | describe "datatypes" do 165 | test "integer" do 166 | assert "select 1" == to_string(~SQL[select 1]) 167 | assert "select 1000" == to_string(~SQL[select 1000]) 168 | assert "select -1000" == to_string(~SQL[select -1000]) 169 | assert "select +1000" == to_string(~SQL[select +1000]) 170 | end 171 | 172 | test "float" do 173 | assert "select +10.00" == to_string(~SQL[select +10.00]) 174 | assert "select -10.00" == to_string(~SQL[select -10.00]) 175 | end 176 | 177 | test "identifier" do 178 | assert "select db.users.id" == to_string(~SQL[select db.users.id]) 179 | assert "select db.users" == to_string(~SQL[select db.users]) 180 | assert "select db" == to_string(~SQL[select db]) 181 | end 182 | 183 | test "qouted" do 184 | assert "select \"db.users.id\"" == to_string(~SQL[select "db.users.id"]) 185 | assert "select 'db.users'" == to_string(~SQL[select 'db.users']) 186 | assert "select \"db.users.id\", 'db.users'" == to_string(~SQL[select "db.users.id", 'db.users']) 187 | end 188 | end 189 | 190 | describe "interpolation" do 191 | test "binding" do 192 | var1 = 1 193 | var0 = "id" 194 | var2 = ~SQL[select {{var0}}] 195 | assert ["id"] == var2.params 196 | sql = ~SQL[select {{var2}}, {{var1}}] 197 | assert [var2, 1] == sql.params 198 | assert "select ?, ?" == to_string(sql) 199 | end 200 | 201 | test ". syntax" do 202 | map = %{k: "v"} 203 | sql = ~SQL[select {{map.k <> "v"}}] 204 | assert ["vv"] == sql.params 205 | assert "select ?" == to_string(sql) 206 | end 207 | 208 | test "code" do 209 | sql = ~SQL[select {{0}}, {{%{k: 1}}}] 210 | assert [0, %{k: 1}] == sql.params 211 | assert "select ?, ?" == to_string(sql) 212 | end 213 | 214 | test "in" do 215 | sql = ~SQL"select {{1}} in {{[1, 2]}}" 216 | assert [1, [1, 2]] == sql.params 217 | assert "select ? in ?" == to_string(sql) 218 | 219 | sql = ~SQL"select {{1}} not in {{[1, 2]}}" 220 | assert [1, [1, 2]] == sql.params 221 | assert "select ? not in ?" == to_string(sql) 222 | end 223 | end 224 | 225 | describe "operators" do 226 | test "=" do 227 | assert "where id = 1" == to_string(~SQL[where id = 1]) 228 | assert "where id = 1" == to_string(~SQL[where id=1]) 229 | end 230 | test "-" do 231 | assert "where id - 1" == to_string(~SQL[where id - 1]) 232 | assert "where id - 1" == to_string(~SQL[where id-1]) 233 | end 234 | test "+" do 235 | assert "where id + 1" == to_string(~SQL[where id + 1]) 236 | assert "where id + 1" == to_string(~SQL[where id+1]) 237 | end 238 | test "*" do 239 | assert "where id * 1" == to_string(~SQL[where id * 1]) 240 | assert "where id * 1" == to_string(~SQL[where id*1]) 241 | end 242 | test "/" do 243 | assert "where id / 1" == to_string(~SQL[where id / 1]) 244 | assert "where id / 1" == to_string(~SQL[where id/1]) 245 | end 246 | test "<>" do 247 | assert "where id <> 1" == to_string(~SQL[where id <> 1]) 248 | assert "where id <> 1" == to_string(~SQL[where id<>1]) 249 | end 250 | test ">" do 251 | assert "where id > 1" == to_string(~SQL[where id > 1]) 252 | assert "where id > 1" == to_string(~SQL[where id>1]) 253 | end 254 | test "<" do 255 | assert "where id < 1" == to_string(~SQL[where id < 1]) 256 | assert "where id < 1" == to_string(~SQL[where id<1]) 257 | end 258 | test ">=" do 259 | assert "where id >= 1" == to_string(~SQL[where id >= 1]) 260 | assert "where id >= 1" == to_string(~SQL[where id>=1]) 261 | end 262 | test "<=" do 263 | assert "where id <= 1" == to_string(~SQL[where id <= 1]) 264 | assert "where id <= 1" == to_string(~SQL[where id<=1]) 265 | end 266 | test "between" do 267 | assert "where id between 1 and 2" == to_string(~SQL[where id between 1 and 2]) 268 | assert "where id not between 1 and 2" == to_string(~SQL[where id not between 1 and 2]) 269 | assert "where id between symmetric 1 and 2" == to_string(~SQL[where id between symmetric 1 and 2]) 270 | assert "where id not between symmetric 1 and 2" == to_string(~SQL[where id not between symmetric 1 and 2]) 271 | end 272 | test "like" do 273 | assert "where id like 1" == to_string(~SQL[where id like 1]) 274 | end 275 | test "ilike" do 276 | assert "where id ilike 1" == to_string(~SQL[where id ilike 1]) 277 | end 278 | test "in" do 279 | assert "where id in (1, 2)" == to_string(~SQL[where id in (1, 2)]) 280 | end 281 | test "is" do 282 | assert "where id is null" == to_string(~SQL[where id is null]) 283 | assert "where id is false" == to_string(~SQL[where id is false]) 284 | assert "where id is true" == to_string(~SQL[where id is true]) 285 | assert "where id is unknown" == to_string(~SQL[where id is unknown]) 286 | 287 | assert "where id is not null" == to_string(~SQL[where id is not null]) 288 | assert "where id is not false" == to_string(~SQL[where id is not false]) 289 | assert "where id is not true" == to_string(~SQL[where id is not true]) 290 | assert "where id is not unknown" == to_string(~SQL[where id is not unknown]) 291 | 292 | assert "where id is distinct from 1" == to_string(~SQL[where id is distinct from 1]) 293 | assert "where id is not distinct from 1" == to_string(~SQL[where id is not distinct from 1]) 294 | 295 | assert "where id isnull" == to_string(~SQL[where id isnull]) 296 | assert "where id notnull" == to_string(~SQL[where id notnull]) 297 | end 298 | test "as" do 299 | assert "select id as dd" == to_string(~SQL[select id as dd]) 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /test/adapters/mysql_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.MySQLTest do 5 | use ExUnit.Case, async: true 6 | use SQL, adapter: SQL.Adapters.MySQL 7 | 8 | describe "with" do 9 | test "recursive" do 10 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 11 | end 12 | 13 | test "regular" do 14 | assert "with temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 15 | end 16 | end 17 | 18 | describe "combinations" do 19 | test "except" do 20 | assert "(select id from users) except (select id from users)" == to_string(~SQL[(select id from users) except (select id from users)]) 21 | assert "(select id from users) except select id from users" == to_string(~SQL[(select id from users) except select id from users]) 22 | assert "select id from users except (select id from users)" == to_string(~SQL[select id from users except (select id from users)]) 23 | assert "select id from users except select id from users" == to_string(~SQL[select id from users except select id from users]) 24 | 25 | assert "(select id from users) except all (select id from users)" == to_string(~SQL[(select id from users) except all (select id from users)]) 26 | assert "(select id from users) except all select id from users" == to_string(~SQL[(select id from users) except all select id from users]) 27 | assert "select id from users except all (select id from users)" == to_string(~SQL[select id from users except all (select id from users)]) 28 | assert "select id from users except all select id from users" == to_string(~SQL[select id from users except all select id from users]) 29 | end 30 | 31 | test "intersect" do 32 | assert "(select id from users) intersect (select id from users)" == to_string(~SQL[(select id from users) intersect (select id from users)]) 33 | assert "(select id from users) intersect select id from users" == to_string(~SQL[(select id from users) intersect select id from users]) 34 | assert "select id from users intersect (select id from users)" == to_string(~SQL[select id from users intersect (select id from users)]) 35 | assert "select id from users intersect select id from users" == to_string(~SQL[select id from users intersect select id from users]) 36 | 37 | assert "(select id from users) intersect all (select id from users)" == to_string(~SQL[(select id from users) intersect all (select id from users)]) 38 | assert "(select id from users) intersect all select id from users" == to_string(~SQL[(select id from users) intersect all select id from users]) 39 | assert "select id from users intersect all (select id from users)" == to_string(~SQL[select id from users intersect all (select id from users)]) 40 | assert "select id from users intersect all select id from users" == to_string(~SQL[select id from users intersect all select id from users]) 41 | end 42 | 43 | test "union" do 44 | assert "(select id from users) union (select id from users)" == to_string(~SQL[(select id from users) union (select id from users)]) 45 | assert "(select id from users) union select id from users" == to_string(~SQL[(select id from users) union select id from users]) 46 | assert "select id from users union (select id from users)" == to_string(~SQL[select id from users union (select id from users)]) 47 | assert "select id from users union select id from users" == to_string(~SQL[select id from users union select id from users]) 48 | 49 | assert "(select id from users) union all (select id from users)" == to_string(~SQL[(select id from users) union all (select id from users)]) 50 | assert "(select id from users) union all select id from users" == to_string(~SQL[(select id from users) union all select id from users]) 51 | assert "select id from users union all (select id from users)" == to_string(~SQL[select id from users union all (select id from users)]) 52 | assert "select id from users union all select id from users" == to_string(~SQL[select id from users union all select id from users]) 53 | end 54 | end 55 | 56 | describe "query" do 57 | test "select" do 58 | assert "select id" == to_string(~SQL[select id]) 59 | assert "select id, id as di" == to_string(~SQL[select id, id as di]) 60 | assert "select id, (select id from users) as di" == to_string(~SQL[select id, (select id from users) as di]) 61 | assert "select unknownn" == to_string(~SQL[select unknownn]) 62 | assert "select truee" == to_string(~SQL[select truee]) 63 | assert "select falsee" == to_string(~SQL[select falsee]) 64 | assert "select nulll" == to_string(~SQL[select nulll]) 65 | assert "select isnulll" == to_string(~SQL[select isnulll]) 66 | assert "select notnulll" == to_string(~SQL[select notnulll]) 67 | assert "select ascc" == to_string(~SQL[select ascc]) 68 | assert "select descc" == to_string(~SQL[select descc]) 69 | assert "select distinct id" == to_string(~SQL[select distinct id]) 70 | assert "select distinct on (id, users) id" == to_string(~SQL[select distinct on (id, users) id]) 71 | end 72 | 73 | test "from" do 74 | assert "from users" == to_string(~SQL[from users]) 75 | assert "from users u, persons p" == to_string(~SQL[from users u, persons p]) 76 | assert "from users u" == to_string(~SQL[from users u]) 77 | assert "from users as u" == to_string(~SQL[from users as u]) 78 | assert "from users u" == to_string(~SQL[from users u]) 79 | end 80 | 81 | test "join" do 82 | assert "inner join users" == to_string(~SQL[inner join users]) 83 | assert "join users" == to_string(~SQL[join users]) 84 | assert "left outer join users" == to_string(~SQL[left outer join users]) 85 | assert "left join users" == to_string(~SQL[left join users]) 86 | assert "natural join users" == to_string(~SQL[natural join users]) 87 | assert "full join users" == to_string(~SQL[full join users]) 88 | assert "cross join users" == to_string(~SQL[cross join users]) 89 | assert "join users u" == to_string(~SQL[join users u]) 90 | assert "join users on id = id" == to_string(~SQL[join users on id = id]) 91 | assert "join users u on id = id" == to_string(~SQL[join users u on id = id]) 92 | assert "join users on (id = id)" == to_string(~SQL[join users on (id = id)]) 93 | assert "join (select * from users) on (id = id)" == to_string(~SQL[join (select * from users) on (id = id)]) 94 | assert "join (select * from users) u on (id = id)" == to_string(~SQL[join (select * from users) u on (id = id)]) 95 | end 96 | 97 | test "where" do 98 | assert "where 1 = 2" == to_string(~SQL[where 1 = 2]) 99 | assert "where 1 = 2" == to_string(~SQL[where 1=2]) 100 | assert "where 1 != 2" == to_string(~SQL[where 1 != 2]) 101 | assert "where 1 <> 2" == to_string(~SQL[where 1 <> 2]) 102 | assert "where 1 = 2 and id = users.id and id > 3 or true" == to_string(~SQL[where 1 = 2 and id = users.id and id > 3 or true]) 103 | end 104 | 105 | test "group by" do 106 | assert "group by id" == to_string(~SQL[group by id]) 107 | assert "group by users.id" == to_string(~SQL[group by users.id]) 108 | assert "group by id, users.id" == to_string(~SQL[group by id, users.id]) 109 | end 110 | 111 | test "having" do 112 | assert "having 1 = 2" == to_string(~SQL[having 1 = 2]) 113 | assert "having 1 != 2" == to_string(~SQL[having 1 != 2]) 114 | assert "having 1 <> 2" == to_string(~SQL[having 1 <> 2]) 115 | end 116 | 117 | test "order by" do 118 | assert "order by id" == to_string(~SQL[order by id]) 119 | assert "order by users.id" == to_string(~SQL[order by users.id]) 120 | assert "order by id, users.id, users.id asc, id desc" == to_string(~SQL[order by id, users.id, users.id asc, id desc]) 121 | end 122 | 123 | test "offset" do 124 | assert "offset 1" == to_string(~SQL[offset 1]) 125 | end 126 | 127 | test "limit" do 128 | assert "limit 1" == to_string(~SQL[limit 1]) 129 | end 130 | 131 | test "fetch" do 132 | assert "fetch next from users" == to_string(~SQL[fetch next from users]) 133 | assert "fetch prior from users" == to_string(~SQL[fetch prior from users]) 134 | assert "fetch first from users" == to_string(~SQL[fetch first from users]) 135 | assert "fetch last from users" == to_string(~SQL[fetch last from users]) 136 | assert "fetch absolute 1 from users" == to_string(~SQL[fetch absolute 1 from users]) 137 | assert "fetch relative 1 from users" == to_string(~SQL[fetch relative 1 from users]) 138 | assert "fetch 1 from users" == to_string(~SQL[fetch 1 from users]) 139 | assert "fetch all from users" == to_string(~SQL[fetch all from users]) 140 | assert "fetch forward from users" == to_string(~SQL[fetch forward from users]) 141 | assert "fetch forward 1 from users" == to_string(~SQL[fetch forward 1 from users]) 142 | assert "fetch forward all from users" == to_string(~SQL[fetch forward all from users]) 143 | assert "fetch backward from users" == to_string(~SQL[fetch backward from users]) 144 | assert "fetch backward 1 from users" == to_string(~SQL[fetch backward 1 from users]) 145 | assert "fetch backward all from users" == to_string(~SQL[fetch backward all from users]) 146 | 147 | assert "fetch next in users" == to_string(~SQL[fetch next in users]) 148 | assert "fetch prior in users" == to_string(~SQL[fetch prior in users]) 149 | assert "fetch first in users" == to_string(~SQL[fetch first in users]) 150 | assert "fetch last in users" == to_string(~SQL[fetch last in users]) 151 | assert "fetch absolute 1 in users" == to_string(~SQL[fetch absolute 1 in users]) 152 | assert "fetch relative 1 in users" == to_string(~SQL[fetch relative 1 in users]) 153 | assert "fetch 1 in users" == to_string(~SQL[fetch 1 in users]) 154 | assert "fetch all in users" == to_string(~SQL[fetch all in users]) 155 | assert "fetch forward in users" == to_string(~SQL[fetch forward in users]) 156 | assert "fetch forward 1 in users" == to_string(~SQL[fetch forward 1 in users]) 157 | assert "fetch forward all in users" == to_string(~SQL[fetch forward all in users]) 158 | assert "fetch backward in users" == to_string(~SQL[fetch backward in users]) 159 | assert "fetch backward 1 in users" == to_string(~SQL[fetch backward 1 in users]) 160 | assert "fetch backward all in users" == to_string(~SQL[fetch backward all in users]) 161 | end 162 | end 163 | 164 | describe "datatypes" do 165 | test "integer" do 166 | assert "select 1" == to_string(~SQL[select 1]) 167 | assert "select 1000" == to_string(~SQL[select 1000]) 168 | assert "select -1000" == to_string(~SQL[select -1000]) 169 | assert "select +1000" == to_string(~SQL[select +1000]) 170 | end 171 | 172 | test "float" do 173 | assert "select +10.00" == to_string(~SQL[select +10.00]) 174 | assert "select -10.00" == to_string(~SQL[select -10.00]) 175 | end 176 | 177 | test "identifier" do 178 | assert "select db.users.id" == to_string(~SQL[select db.users.id]) 179 | assert "select db.users" == to_string(~SQL[select db.users]) 180 | assert "select db" == to_string(~SQL[select db]) 181 | end 182 | 183 | test "qouted" do 184 | assert "select \"db.users.id\"" == to_string(~SQL[select "db.users.id"]) 185 | assert "select 'db.users'" == to_string(~SQL[select 'db.users']) 186 | assert "select \"db.users.id\", 'db.users'" == to_string(~SQL[select "db.users.id", 'db.users']) 187 | end 188 | end 189 | 190 | describe "interpolation" do 191 | test "binding" do 192 | var1 = 1 193 | var0 = "id" 194 | var2 = ~SQL[select {{var0}}] 195 | assert ["id"] == var2.params 196 | sql = ~SQL[select {{var2}}, {{var1}}] 197 | assert [var2, 1] == sql.params 198 | assert "select ?, ?" == to_string(sql) 199 | end 200 | 201 | test ". syntax" do 202 | map = %{k: "v"} 203 | sql = ~SQL[select {{map.k <> "v"}}] 204 | assert ["vv"] == sql.params 205 | assert "select ?" == to_string(sql) 206 | end 207 | 208 | test "code" do 209 | sql = ~SQL[select {{0}}, {{%{k: 1}}}] 210 | assert [0, %{k: 1}] == sql.params 211 | assert "select ?, ?" == to_string(sql) 212 | end 213 | 214 | test "in" do 215 | sql = ~SQL"select {{1}} in {{[1, 2]}}" 216 | assert [1, [1, 2]] == sql.params 217 | assert "select ? in ?" == to_string(sql) 218 | 219 | sql = ~SQL"select {{1}} not in {{[1, 2]}}" 220 | assert [1, [1, 2]] == sql.params 221 | assert "select ? not in ?" == to_string(sql) 222 | end 223 | end 224 | 225 | describe "operators" do 226 | test "=" do 227 | assert "where id = 1" == to_string(~SQL[where id = 1]) 228 | assert "where id = 1" == to_string(~SQL[where id=1]) 229 | end 230 | test "-" do 231 | assert "where id - 1" == to_string(~SQL[where id - 1]) 232 | assert "where id - 1" == to_string(~SQL[where id-1]) 233 | end 234 | test "+" do 235 | assert "where id + 1" == to_string(~SQL[where id + 1]) 236 | assert "where id + 1" == to_string(~SQL[where id+1]) 237 | end 238 | test "*" do 239 | assert "where id * 1" == to_string(~SQL[where id * 1]) 240 | assert "where id * 1" == to_string(~SQL[where id*1]) 241 | end 242 | test "/" do 243 | assert "where id / 1" == to_string(~SQL[where id / 1]) 244 | assert "where id / 1" == to_string(~SQL[where id/1]) 245 | end 246 | test "<>" do 247 | assert "where id <> 1" == to_string(~SQL[where id <> 1]) 248 | assert "where id <> 1" == to_string(~SQL[where id<>1]) 249 | end 250 | test ">" do 251 | assert "where id > 1" == to_string(~SQL[where id > 1]) 252 | assert "where id > 1" == to_string(~SQL[where id>1]) 253 | end 254 | test "<" do 255 | assert "where id < 1" == to_string(~SQL[where id < 1]) 256 | assert "where id < 1" == to_string(~SQL[where id<1]) 257 | end 258 | test ">=" do 259 | assert "where id >= 1" == to_string(~SQL[where id >= 1]) 260 | assert "where id >= 1" == to_string(~SQL[where id>=1]) 261 | end 262 | test "<=" do 263 | assert "where id <= 1" == to_string(~SQL[where id <= 1]) 264 | assert "where id <= 1" == to_string(~SQL[where id<=1]) 265 | end 266 | test "between" do 267 | assert "where id between 1 and 2" == to_string(~SQL[where id between 1 and 2]) 268 | assert "where id not between 1 and 2" == to_string(~SQL[where id not between 1 and 2]) 269 | assert "where id between symmetric 1 and 2" == to_string(~SQL[where id between symmetric 1 and 2]) 270 | assert "where id not between symmetric 1 and 2" == to_string(~SQL[where id not between symmetric 1 and 2]) 271 | end 272 | test "like" do 273 | assert "where id like 1" == to_string(~SQL[where id like 1]) 274 | end 275 | test "ilike" do 276 | assert "where id ilike 1" == to_string(~SQL[where id ilike 1]) 277 | end 278 | test "in" do 279 | assert "where id in (1, 2)" == to_string(~SQL[where id in (1, 2)]) 280 | end 281 | test "is" do 282 | assert "where id is null" == to_string(~SQL[where id is null]) 283 | assert "where id is false" == to_string(~SQL[where id is false]) 284 | assert "where id is true" == to_string(~SQL[where id is true]) 285 | assert "where id is unknown" == to_string(~SQL[where id is unknown]) 286 | 287 | assert "where id is not null" == to_string(~SQL[where id is not null]) 288 | assert "where id is not false" == to_string(~SQL[where id is not false]) 289 | assert "where id is not true" == to_string(~SQL[where id is not true]) 290 | assert "where id is not unknown" == to_string(~SQL[where id is not unknown]) 291 | 292 | assert "where id is distinct from 1" == to_string(~SQL[where id is distinct from 1]) 293 | assert "where id is not distinct from 1" == to_string(~SQL[where id is not distinct from 1]) 294 | 295 | assert "where id isnull" == to_string(~SQL[where id isnull]) 296 | assert "where id notnull" == to_string(~SQL[where id notnull]) 297 | end 298 | test "as" do 299 | assert "select id as dd" == to_string(~SQL[select id as dd]) 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /test/adapters/postgres_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.PostgresTest do 5 | use ExUnit.Case, async: true 6 | use SQL, adapter: SQL.Adapters.Postgres 7 | 8 | describe "with" do 9 | test "recursive" do 10 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 11 | end 12 | 13 | test "regular" do 14 | assert "with temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 15 | end 16 | end 17 | 18 | describe "combinations" do 19 | test "except" do 20 | assert "(select id from users) except (select id from users)" == to_string(~SQL[(select id from users) except (select id from users)]) 21 | assert "(select id from users) except select id from users" == to_string(~SQL[(select id from users) except select id from users]) 22 | assert "select id from users except (select id from users)" == to_string(~SQL[select id from users except (select id from users)]) 23 | assert "select id from users except select id from users" == to_string(~SQL[select id from users except select id from users]) 24 | 25 | assert "(select id from users) except all (select id from users)" == to_string(~SQL[(select id from users) except all (select id from users)]) 26 | assert "(select id from users) except all select id from users" == to_string(~SQL[(select id from users) except all select id from users]) 27 | assert "select id from users except all (select id from users)" == to_string(~SQL[select id from users except all (select id from users)]) 28 | assert "select id from users except all select id from users" == to_string(~SQL[select id from users except all select id from users]) 29 | end 30 | 31 | test "intersect" do 32 | assert "(select id from users) intersect (select id from users)" == to_string(~SQL[(select id from users) intersect (select id from users)]) 33 | assert "(select id from users) intersect select id from users" == to_string(~SQL[(select id from users) intersect select id from users]) 34 | assert "select id from users intersect (select id from users)" == to_string(~SQL[select id from users intersect (select id from users)]) 35 | assert "select id from users intersect select id from users" == to_string(~SQL[select id from users intersect select id from users]) 36 | 37 | assert "(select id from users) intersect all (select id from users)" == to_string(~SQL[(select id from users) intersect all (select id from users)]) 38 | assert "(select id from users) intersect all select id from users" == to_string(~SQL[(select id from users) intersect all select id from users]) 39 | assert "select id from users intersect all (select id from users)" == to_string(~SQL[select id from users intersect all (select id from users)]) 40 | assert "select id from users intersect all select id from users" == to_string(~SQL[select id from users intersect all select id from users]) 41 | end 42 | 43 | test "union" do 44 | assert "(select id from users) union (select id from users)" == to_string(~SQL[(select id from users) union (select id from users)]) 45 | assert "(select id from users) union select id from users" == to_string(~SQL[(select id from users) union select id from users]) 46 | assert "select id from users union (select id from users)" == to_string(~SQL[select id from users union (select id from users)]) 47 | assert "select id from users union select id from users" == to_string(~SQL[select id from users union select id from users]) 48 | 49 | assert "(select id from users) union all (select id from users)" == to_string(~SQL[(select id from users) union all (select id from users)]) 50 | assert "(select id from users) union all select id from users" == to_string(~SQL[(select id from users) union all select id from users]) 51 | assert "select id from users union all (select id from users)" == to_string(~SQL[select id from users union all (select id from users)]) 52 | assert "select id from users union all select id from users" == to_string(~SQL[select id from users union all select id from users]) 53 | end 54 | end 55 | 56 | describe "query" do 57 | test "select" do 58 | assert "select id" == to_string(~SQL[select id]) 59 | assert "select id, id as di" == to_string(~SQL[select id, id as di]) 60 | assert "select id, (select id from users) as di" == to_string(~SQL[select id, (select id from users) as di]) 61 | assert "select unknownn" == to_string(~SQL[select unknownn]) 62 | assert "select truee" == to_string(~SQL[select truee]) 63 | assert "select falsee" == to_string(~SQL[select falsee]) 64 | assert "select nulll" == to_string(~SQL[select nulll]) 65 | assert "select isnulll" == to_string(~SQL[select isnulll]) 66 | assert "select notnulll" == to_string(~SQL[select notnulll]) 67 | assert "select ascc" == to_string(~SQL[select ascc]) 68 | assert "select descc" == to_string(~SQL[select descc]) 69 | assert "select distinct id" == to_string(~SQL[select distinct id]) 70 | assert "select distinct on (id, users) id" == to_string(~SQL[select distinct on (id, users) id]) 71 | end 72 | 73 | test "from" do 74 | assert "from users" == to_string(~SQL[from users]) 75 | assert "from users u, persons p" == to_string(~SQL[from users u, persons p]) 76 | assert "from users u" == to_string(~SQL[from users u]) 77 | assert "from users as u" == to_string(~SQL[from users as u]) 78 | assert "from users u" == to_string(~SQL[from users u]) 79 | end 80 | 81 | test "join" do 82 | assert "inner join users" == to_string(~SQL[inner join users]) 83 | assert "join users" == to_string(~SQL[join users]) 84 | assert "left outer join users" == to_string(~SQL[left outer join users]) 85 | assert "left join users" == to_string(~SQL[left join users]) 86 | assert "natural join users" == to_string(~SQL[natural join users]) 87 | assert "full join users" == to_string(~SQL[full join users]) 88 | assert "cross join users" == to_string(~SQL[cross join users]) 89 | assert "join users u" == to_string(~SQL[join users u]) 90 | assert "join users on id = id" == to_string(~SQL[join users on id = id]) 91 | assert "join users u on id = id" == to_string(~SQL[join users u on id = id]) 92 | assert "join users on (id = id)" == to_string(~SQL[join users on (id = id)]) 93 | assert "join (select * from users) on (id = id)" == to_string(~SQL[join (select * from users) on (id = id)]) 94 | assert "join (select * from users) u on (id = id)" == to_string(~SQL[join (select * from users) u on (id = id)]) 95 | end 96 | 97 | test "where" do 98 | assert "where 1 = 2" == to_string(~SQL[where 1 = 2]) 99 | assert "where 1 = 2" == to_string(~SQL[where 1=2]) 100 | assert "where 1 != 2" == to_string(~SQL[where 1 != 2]) 101 | assert "where 1 <> 2" == to_string(~SQL[where 1 <> 2]) 102 | assert "where 1 = 2 and id = users.id and id > 3 or true" == to_string(~SQL[where 1 = 2 and id = users.id and id > 3 or true]) 103 | end 104 | 105 | test "group by" do 106 | assert "group by id" == to_string(~SQL[group by id]) 107 | assert "group by users.id" == to_string(~SQL[group by users.id]) 108 | assert "group by id, users.id" == to_string(~SQL[group by id, users.id]) 109 | end 110 | 111 | test "having" do 112 | assert "having 1 = 2" == to_string(~SQL[having 1 = 2]) 113 | assert "having 1 != 2" == to_string(~SQL[having 1 != 2]) 114 | assert "having 1 <> 2" == to_string(~SQL[having 1 <> 2]) 115 | end 116 | 117 | test "order by" do 118 | assert "order by id" == to_string(~SQL[order by id]) 119 | assert "order by users.id" == to_string(~SQL[order by users.id]) 120 | assert "order by id, users.id, users.id asc, id desc" == to_string(~SQL[order by id, users.id, users.id asc, id desc]) 121 | end 122 | 123 | test "offset" do 124 | assert "offset 1" == to_string(~SQL[offset 1]) 125 | end 126 | 127 | test "limit" do 128 | assert "limit 1" == to_string(~SQL[limit 1]) 129 | end 130 | 131 | test "fetch" do 132 | assert "fetch next from users" == to_string(~SQL[fetch next from users]) 133 | assert "fetch prior from users" == to_string(~SQL[fetch prior from users]) 134 | assert "fetch first from users" == to_string(~SQL[fetch first from users]) 135 | assert "fetch last from users" == to_string(~SQL[fetch last from users]) 136 | assert "fetch absolute 1 from users" == to_string(~SQL[fetch absolute 1 from users]) 137 | assert "fetch relative 1 from users" == to_string(~SQL[fetch relative 1 from users]) 138 | assert "fetch 1 from users" == to_string(~SQL[fetch 1 from users]) 139 | assert "fetch all from users" == to_string(~SQL[fetch all from users]) 140 | assert "fetch forward from users" == to_string(~SQL[fetch forward from users]) 141 | assert "fetch forward 1 from users" == to_string(~SQL[fetch forward 1 from users]) 142 | assert "fetch forward all from users" == to_string(~SQL[fetch forward all from users]) 143 | assert "fetch backward from users" == to_string(~SQL[fetch backward from users]) 144 | assert "fetch backward 1 from users" == to_string(~SQL[fetch backward 1 from users]) 145 | assert "fetch backward all from users" == to_string(~SQL[fetch backward all from users]) 146 | 147 | assert "fetch next in users" == to_string(~SQL[fetch next in users]) 148 | assert "fetch prior in users" == to_string(~SQL[fetch prior in users]) 149 | assert "fetch first in users" == to_string(~SQL[fetch first in users]) 150 | assert "fetch last in users" == to_string(~SQL[fetch last in users]) 151 | assert "fetch absolute 1 in users" == to_string(~SQL[fetch absolute 1 in users]) 152 | assert "fetch relative 1 in users" == to_string(~SQL[fetch relative 1 in users]) 153 | assert "fetch 1 in users" == to_string(~SQL[fetch 1 in users]) 154 | assert "fetch all in users" == to_string(~SQL[fetch all in users]) 155 | assert "fetch forward in users" == to_string(~SQL[fetch forward in users]) 156 | assert "fetch forward 1 in users" == to_string(~SQL[fetch forward 1 in users]) 157 | assert "fetch forward all in users" == to_string(~SQL[fetch forward all in users]) 158 | assert "fetch backward in users" == to_string(~SQL[fetch backward in users]) 159 | assert "fetch backward 1 in users" == to_string(~SQL[fetch backward 1 in users]) 160 | assert "fetch backward all in users" == to_string(~SQL[fetch backward all in users]) 161 | end 162 | end 163 | 164 | describe "datatypes" do 165 | test "integer" do 166 | assert "select 1" == to_string(~SQL[select 1]) 167 | assert "select 1000" == to_string(~SQL[select 1000]) 168 | assert "select -1000" == to_string(~SQL[select -1000]) 169 | assert "select +1000" == to_string(~SQL[select +1000]) 170 | end 171 | 172 | test "float" do 173 | assert "select +10.00" == to_string(~SQL[select +10.00]) 174 | assert "select -10.00" == to_string(~SQL[select -10.00]) 175 | end 176 | 177 | test "identifier" do 178 | assert "select db.users.id" == to_string(~SQL[select db.users.id]) 179 | assert "select db.users" == to_string(~SQL[select db.users]) 180 | assert "select db" == to_string(~SQL[select db]) 181 | end 182 | 183 | test "qouted" do 184 | assert "select \"db.users.id\"" == to_string(~SQL[select "db.users.id"]) 185 | assert "select 'db.users'" == to_string(~SQL[select 'db.users']) 186 | assert "select \"db.users.id\", 'db.users'" == to_string(~SQL[select "db.users.id", 'db.users']) 187 | end 188 | end 189 | 190 | describe "interpolation" do 191 | test "binding" do 192 | var1 = 1 193 | var0 = "id" 194 | var2 = ~SQL[select {{var0}}] 195 | assert ["id"] == var2.params 196 | sql = ~SQL[select {{var2}}, {{var1}}] 197 | assert [var2, 1] == sql.params 198 | assert "select $1, $2" == to_string(sql) 199 | end 200 | 201 | test ". syntax" do 202 | map = %{k: "v"} 203 | sql = ~SQL[select {{map.k <> "v"}}] 204 | assert ["vv"] == sql.params 205 | assert "select $1" == to_string(sql) 206 | end 207 | 208 | test "code" do 209 | sql = ~SQL[select {{0}}, {{%{k: 1}}}] 210 | assert [0, %{k: 1}] == sql.params 211 | assert "select $1, $2" == to_string(sql) 212 | end 213 | 214 | test "in" do 215 | sql = ~SQL"select {{1}} in {{[1, 2]}}" 216 | assert [1, [1, 2]] == sql.params 217 | assert "select $1 = ANY($2)" == to_string(sql) 218 | 219 | sql = ~SQL"select {{1}} not in {{[1, 2]}}" 220 | assert [1, [1, 2]] == sql.params 221 | assert "select $1 != ANY($2)" == to_string(sql) 222 | end 223 | end 224 | 225 | describe "operators" do 226 | test "=" do 227 | assert "where id = 1" == to_string(~SQL[where id = 1]) 228 | assert "where id = 1" == to_string(~SQL[where id=1]) 229 | end 230 | test "-" do 231 | assert "where id - 1" == to_string(~SQL[where id - 1]) 232 | assert "where id - 1" == to_string(~SQL[where id-1]) 233 | end 234 | test "+" do 235 | assert "where id + 1" == to_string(~SQL[where id + 1]) 236 | assert "where id + 1" == to_string(~SQL[where id+1]) 237 | end 238 | test "*" do 239 | assert "where id * 1" == to_string(~SQL[where id * 1]) 240 | assert "where id * 1" == to_string(~SQL[where id*1]) 241 | end 242 | test "/" do 243 | assert "where id / 1" == to_string(~SQL[where id / 1]) 244 | assert "where id / 1" == to_string(~SQL[where id/1]) 245 | end 246 | test "<>" do 247 | assert "where id <> 1" == to_string(~SQL[where id <> 1]) 248 | assert "where id <> 1" == to_string(~SQL[where id<>1]) 249 | end 250 | test ">" do 251 | assert "where id > 1" == to_string(~SQL[where id > 1]) 252 | assert "where id > 1" == to_string(~SQL[where id>1]) 253 | end 254 | test "<" do 255 | assert "where id < 1" == to_string(~SQL[where id < 1]) 256 | assert "where id < 1" == to_string(~SQL[where id<1]) 257 | end 258 | test ">=" do 259 | assert "where id >= 1" == to_string(~SQL[where id >= 1]) 260 | assert "where id >= 1" == to_string(~SQL[where id>=1]) 261 | end 262 | test "<=" do 263 | assert "where id <= 1" == to_string(~SQL[where id <= 1]) 264 | assert "where id <= 1" == to_string(~SQL[where id<=1]) 265 | end 266 | test "between" do 267 | assert "where id between 1 and 2" == to_string(~SQL[where id between 1 and 2]) 268 | assert "where id not between 1 and 2" == to_string(~SQL[where id not between 1 and 2]) 269 | assert "where id between symmetric 1 and 2" == to_string(~SQL[where id between symmetric 1 and 2]) 270 | assert "where id not between symmetric 1 and 2" == to_string(~SQL[where id not between symmetric 1 and 2]) 271 | end 272 | test "like" do 273 | assert "where id like 1" == to_string(~SQL[where id like 1]) 274 | end 275 | test "ilike" do 276 | assert "where id ilike 1" == to_string(~SQL[where id ilike 1]) 277 | end 278 | test "in" do 279 | assert "where id in (1, 2)" == to_string(~SQL[where id in (1, 2)]) 280 | end 281 | test "is" do 282 | assert "where id is null" == to_string(~SQL[where id is null]) 283 | assert "where id is false" == to_string(~SQL[where id is false]) 284 | assert "where id is true" == to_string(~SQL[where id is true]) 285 | assert "where id is unknown" == to_string(~SQL[where id is unknown]) 286 | 287 | assert "where id is not null" == to_string(~SQL[where id is not null]) 288 | assert "where id is not false" == to_string(~SQL[where id is not false]) 289 | assert "where id is not true" == to_string(~SQL[where id is not true]) 290 | assert "where id is not unknown" == to_string(~SQL[where id is not unknown]) 291 | 292 | assert "where id is distinct from 1" == to_string(~SQL[where id is distinct from 1]) 293 | assert "where id is not distinct from 1" == to_string(~SQL[where id is not distinct from 1]) 294 | 295 | assert "where id isnull" == to_string(~SQL[where id isnull]) 296 | assert "where id notnull" == to_string(~SQL[where id notnull]) 297 | end 298 | test "as" do 299 | assert "select id as dd" == to_string(~SQL[select id as dd]) 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /test/adapters/tds_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Adapters.TDSTest do 5 | use ExUnit.Case, async: true 6 | use SQL, adapter: SQL.Adapters.TDS 7 | 8 | describe "with" do 9 | test "recursive" do 10 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 11 | end 12 | 13 | test "regular" do 14 | assert "with temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 15 | end 16 | end 17 | 18 | describe "combinations" do 19 | test "except" do 20 | assert "(select id from users) except (select id from users)" == to_string(~SQL[(select id from users) except (select id from users)]) 21 | assert "(select id from users) except select id from users" == to_string(~SQL[(select id from users) except select id from users]) 22 | assert "select id from users except (select id from users)" == to_string(~SQL[select id from users except (select id from users)]) 23 | assert "select id from users except select id from users" == to_string(~SQL[select id from users except select id from users]) 24 | 25 | assert "(select id from users) except all (select id from users)" == to_string(~SQL[(select id from users) except all (select id from users)]) 26 | assert "(select id from users) except all select id from users" == to_string(~SQL[(select id from users) except all select id from users]) 27 | assert "select id from users except all (select id from users)" == to_string(~SQL[select id from users except all (select id from users)]) 28 | assert "select id from users except all select id from users" == to_string(~SQL[select id from users except all select id from users]) 29 | end 30 | 31 | test "intersect" do 32 | assert "(select id from users) intersect (select id from users)" == to_string(~SQL[(select id from users) intersect (select id from users)]) 33 | assert "(select id from users) intersect select id from users" == to_string(~SQL[(select id from users) intersect select id from users]) 34 | assert "select id from users intersect (select id from users)" == to_string(~SQL[select id from users intersect (select id from users)]) 35 | assert "select id from users intersect select id from users" == to_string(~SQL[select id from users intersect select id from users]) 36 | 37 | assert "(select id from users) intersect all (select id from users)" == to_string(~SQL[(select id from users) intersect all (select id from users)]) 38 | assert "(select id from users) intersect all select id from users" == to_string(~SQL[(select id from users) intersect all select id from users]) 39 | assert "select id from users intersect all (select id from users)" == to_string(~SQL[select id from users intersect all (select id from users)]) 40 | assert "select id from users intersect all select id from users" == to_string(~SQL[select id from users intersect all select id from users]) 41 | end 42 | 43 | test "union" do 44 | assert "(select id from users) union (select id from users)" == to_string(~SQL[(select id from users) union (select id from users)]) 45 | assert "(select id from users) union select id from users" == to_string(~SQL[(select id from users) union select id from users]) 46 | assert "select id from users union (select id from users)" == to_string(~SQL[select id from users union (select id from users)]) 47 | assert "select id from users union select id from users" == to_string(~SQL[select id from users union select id from users]) 48 | 49 | assert "(select id from users) union all (select id from users)" == to_string(~SQL[(select id from users) union all (select id from users)]) 50 | assert "(select id from users) union all select id from users" == to_string(~SQL[(select id from users) union all select id from users]) 51 | assert "select id from users union all (select id from users)" == to_string(~SQL[select id from users union all (select id from users)]) 52 | assert "select id from users union all select id from users" == to_string(~SQL[select id from users union all select id from users]) 53 | end 54 | end 55 | 56 | describe "query" do 57 | test "select" do 58 | assert "select id" == to_string(~SQL[select id]) 59 | assert "select id, id as di" == to_string(~SQL[select id, id as di]) 60 | assert "select id, (select id from users) as di" == to_string(~SQL[select id, (select id from users) as di]) 61 | assert "select unknownn" == to_string(~SQL[select unknownn]) 62 | assert "select truee" == to_string(~SQL[select truee]) 63 | assert "select falsee" == to_string(~SQL[select falsee]) 64 | assert "select nulll" == to_string(~SQL[select nulll]) 65 | assert "select isnulll" == to_string(~SQL[select isnulll]) 66 | assert "select notnulll" == to_string(~SQL[select notnulll]) 67 | assert "select ascc" == to_string(~SQL[select ascc]) 68 | assert "select descc" == to_string(~SQL[select descc]) 69 | assert "select distinct id" == to_string(~SQL[select distinct id]) 70 | assert "select distinct on (id, users) id" == to_string(~SQL[select distinct on (id, users) id]) 71 | end 72 | 73 | test "from" do 74 | assert "from users" == to_string(~SQL[from users]) 75 | assert "from users u, persons p" == to_string(~SQL[from users u, persons p]) 76 | assert "from users u" == to_string(~SQL[from users u]) 77 | assert "from users as u" == to_string(~SQL[from users as u]) 78 | assert "from users u" == to_string(~SQL[from users u]) 79 | end 80 | 81 | test "join" do 82 | assert "inner join users" == to_string(~SQL[inner join users]) 83 | assert "join users" == to_string(~SQL[join users]) 84 | assert "left outer join users" == to_string(~SQL[left outer join users]) 85 | assert "left join users" == to_string(~SQL[left join users]) 86 | assert "natural join users" == to_string(~SQL[natural join users]) 87 | assert "full join users" == to_string(~SQL[full join users]) 88 | assert "cross join users" == to_string(~SQL[cross join users]) 89 | assert "join users u" == to_string(~SQL[join users u]) 90 | assert "join users on id = id" == to_string(~SQL[join users on id = id]) 91 | assert "join users u on id = id" == to_string(~SQL[join users u on id = id]) 92 | assert "join users on (id = id)" == to_string(~SQL[join users on (id = id)]) 93 | assert "join (select * from users) on (id = id)" == to_string(~SQL[join (select * from users) on (id = id)]) 94 | assert "join (select * from users) u on (id = id)" == to_string(~SQL[join (select * from users) u on (id = id)]) 95 | end 96 | 97 | test "where" do 98 | assert "where 1 = 2" == to_string(~SQL[where 1 = 2]) 99 | assert "where 1 = 2" == to_string(~SQL[where 1=2]) 100 | assert "where 1 != 2" == to_string(~SQL[where 1 != 2]) 101 | assert "where 1 <> 2" == to_string(~SQL[where 1 <> 2]) 102 | assert "where 1 = 2 and id = users.id and id > 3 or true" == to_string(~SQL[where 1 = 2 and id = users.id and id > 3 or true]) 103 | end 104 | 105 | test "group by" do 106 | assert "group by id" == to_string(~SQL[group by id]) 107 | assert "group by users.id" == to_string(~SQL[group by users.id]) 108 | assert "group by id, users.id" == to_string(~SQL[group by id, users.id]) 109 | end 110 | 111 | test "having" do 112 | assert "having 1 = 2" == to_string(~SQL[having 1 = 2]) 113 | assert "having 1 != 2" == to_string(~SQL[having 1 != 2]) 114 | assert "having 1 <> 2" == to_string(~SQL[having 1 <> 2]) 115 | end 116 | 117 | test "order by" do 118 | assert "order by id" == to_string(~SQL[order by id]) 119 | assert "order by users.id" == to_string(~SQL[order by users.id]) 120 | assert "order by id, users.id, users.id asc, id desc" == to_string(~SQL[order by id, users.id, users.id asc, id desc]) 121 | end 122 | 123 | test "offset" do 124 | assert "offset 1" == to_string(~SQL[offset 1]) 125 | end 126 | 127 | test "limit" do 128 | assert "limit 1" == to_string(~SQL[limit 1]) 129 | end 130 | 131 | test "fetch" do 132 | assert "fetch next from users" == to_string(~SQL[fetch next from users]) 133 | assert "fetch prior from users" == to_string(~SQL[fetch prior from users]) 134 | assert "fetch first from users" == to_string(~SQL[fetch first from users]) 135 | assert "fetch last from users" == to_string(~SQL[fetch last from users]) 136 | assert "fetch absolute 1 from users" == to_string(~SQL[fetch absolute 1 from users]) 137 | assert "fetch relative 1 from users" == to_string(~SQL[fetch relative 1 from users]) 138 | assert "fetch 1 from users" == to_string(~SQL[fetch 1 from users]) 139 | assert "fetch all from users" == to_string(~SQL[fetch all from users]) 140 | assert "fetch forward from users" == to_string(~SQL[fetch forward from users]) 141 | assert "fetch forward 1 from users" == to_string(~SQL[fetch forward 1 from users]) 142 | assert "fetch forward all from users" == to_string(~SQL[fetch forward all from users]) 143 | assert "fetch backward from users" == to_string(~SQL[fetch backward from users]) 144 | assert "fetch backward 1 from users" == to_string(~SQL[fetch backward 1 from users]) 145 | assert "fetch backward all from users" == to_string(~SQL[fetch backward all from users]) 146 | 147 | assert "fetch next in users" == to_string(~SQL[fetch next in users]) 148 | assert "fetch prior in users" == to_string(~SQL[fetch prior in users]) 149 | assert "fetch first in users" == to_string(~SQL[fetch first in users]) 150 | assert "fetch last in users" == to_string(~SQL[fetch last in users]) 151 | assert "fetch absolute 1 in users" == to_string(~SQL[fetch absolute 1 in users]) 152 | assert "fetch relative 1 in users" == to_string(~SQL[fetch relative 1 in users]) 153 | assert "fetch 1 in users" == to_string(~SQL[fetch 1 in users]) 154 | assert "fetch all in users" == to_string(~SQL[fetch all in users]) 155 | assert "fetch forward in users" == to_string(~SQL[fetch forward in users]) 156 | assert "fetch forward 1 in users" == to_string(~SQL[fetch forward 1 in users]) 157 | assert "fetch forward all in users" == to_string(~SQL[fetch forward all in users]) 158 | assert "fetch backward in users" == to_string(~SQL[fetch backward in users]) 159 | assert "fetch backward 1 in users" == to_string(~SQL[fetch backward 1 in users]) 160 | assert "fetch backward all in users" == to_string(~SQL[fetch backward all in users]) 161 | end 162 | end 163 | 164 | describe "datatypes" do 165 | test "integer" do 166 | assert "select 1" == to_string(~SQL[select 1]) 167 | assert "select 1000" == to_string(~SQL[select 1000]) 168 | assert "select -1000" == to_string(~SQL[select -1000]) 169 | assert "select +1000" == to_string(~SQL[select +1000]) 170 | end 171 | 172 | test "float" do 173 | assert "select +10.00" == to_string(~SQL[select +10.00]) 174 | assert "select -10.00" == to_string(~SQL[select -10.00]) 175 | end 176 | 177 | test "identifier" do 178 | assert "select db.users.id" == to_string(~SQL[select db.users.id]) 179 | assert "select db.users" == to_string(~SQL[select db.users]) 180 | assert "select db" == to_string(~SQL[select db]) 181 | end 182 | 183 | test "qouted" do 184 | assert "select \"db.users.id\"" == to_string(~SQL[select "db.users.id"]) 185 | assert "select 'db.users'" == to_string(~SQL[select 'db.users']) 186 | assert "select \"db.users.id\", 'db.users'" == to_string(~SQL[select "db.users.id", 'db.users']) 187 | end 188 | end 189 | 190 | describe "interpolation" do 191 | test "binding" do 192 | var1 = 1 193 | var0 = "id" 194 | var2 = ~SQL[select {{var0}}] 195 | assert ["id"] == var2.params 196 | sql = ~SQL[select {{var2}}, {{var1}}] 197 | assert [var2, 1] == sql.params 198 | assert "select @1, @2" == to_string(sql) 199 | end 200 | 201 | test ". syntax" do 202 | map = %{k: "v"} 203 | sql = ~SQL[select {{map.k <> "v"}}] 204 | assert ["vv"] == sql.params 205 | assert "select @1" == to_string(sql) 206 | end 207 | 208 | test "code" do 209 | sql = ~SQL[select {{0}}, {{%{k: 1}}}] 210 | assert [0, %{k: 1}] == sql.params 211 | assert "select @1, @2" == to_string(sql) 212 | end 213 | 214 | test "in" do 215 | sql = ~SQL"select {{1}} in {{[1, 2]}}" 216 | assert [1, [1, 2]] == sql.params 217 | assert "select @1 in @2" == to_string(sql) 218 | 219 | sql = ~SQL"select {{1}} not in {{[1, 2]}}" 220 | assert [1, [1, 2]] == sql.params 221 | assert "select @1 not in @2" == to_string(sql) 222 | end 223 | end 224 | 225 | describe "operators" do 226 | test "=" do 227 | assert "where id = 1" == to_string(~SQL[where id = 1]) 228 | assert "where id = 1" == to_string(~SQL[where id=1]) 229 | end 230 | test "-" do 231 | assert "where id - 1" == to_string(~SQL[where id - 1]) 232 | assert "where id - 1" == to_string(~SQL[where id-1]) 233 | end 234 | test "+" do 235 | assert "where id + 1" == to_string(~SQL[where id + 1]) 236 | assert "where id + 1" == to_string(~SQL[where id+1]) 237 | end 238 | test "*" do 239 | assert "where id * 1" == to_string(~SQL[where id * 1]) 240 | assert "where id * 1" == to_string(~SQL[where id*1]) 241 | end 242 | test "/" do 243 | assert "where id / 1" == to_string(~SQL[where id / 1]) 244 | assert "where id / 1" == to_string(~SQL[where id/1]) 245 | end 246 | test "<>" do 247 | assert "where id <> 1" == to_string(~SQL[where id <> 1]) 248 | assert "where id <> 1" == to_string(~SQL[where id<>1]) 249 | end 250 | test ">" do 251 | assert "where id > 1" == to_string(~SQL[where id > 1]) 252 | assert "where id > 1" == to_string(~SQL[where id>1]) 253 | end 254 | test "<" do 255 | assert "where id < 1" == to_string(~SQL[where id < 1]) 256 | assert "where id < 1" == to_string(~SQL[where id<1]) 257 | end 258 | test ">=" do 259 | assert "where id >= 1" == to_string(~SQL[where id >= 1]) 260 | assert "where id >= 1" == to_string(~SQL[where id>=1]) 261 | end 262 | test "<=" do 263 | assert "where id <= 1" == to_string(~SQL[where id <= 1]) 264 | assert "where id <= 1" == to_string(~SQL[where id<=1]) 265 | end 266 | test "between" do 267 | assert "where id between 1 and 2" == to_string(~SQL[where id between 1 and 2]) 268 | assert "where id not between 1 and 2" == to_string(~SQL[where id not between 1 and 2]) 269 | assert "where id between symmetric 1 and 2" == to_string(~SQL[where id between symmetric 1 and 2]) 270 | assert "where id not between symmetric 1 and 2" == to_string(~SQL[where id not between symmetric 1 and 2]) 271 | end 272 | test "like" do 273 | assert "where id like 1" == to_string(~SQL[where id like 1]) 274 | end 275 | test "ilike" do 276 | assert "where id ilike 1" == to_string(~SQL[where id ilike 1]) 277 | end 278 | test "in" do 279 | assert "where id in (1, 2)" == to_string(~SQL[where id in (1, 2)]) 280 | end 281 | test "is" do 282 | assert "where id is null" == to_string(~SQL[where id is null]) 283 | assert "where id is false" == to_string(~SQL[where id is false]) 284 | assert "where id is true" == to_string(~SQL[where id is true]) 285 | assert "where id is unknown" == to_string(~SQL[where id is unknown]) 286 | 287 | assert "where id is not null" == to_string(~SQL[where id is not null]) 288 | assert "where id is not false" == to_string(~SQL[where id is not false]) 289 | assert "where id is not true" == to_string(~SQL[where id is not true]) 290 | assert "where id is not unknown" == to_string(~SQL[where id is not unknown]) 291 | 292 | assert "where id is distinct from 1" == to_string(~SQL[where id is distinct from 1]) 293 | assert "where id is not distinct from 1" == to_string(~SQL[where id is not distinct from 1]) 294 | 295 | assert "where id isnull" == to_string(~SQL[where id isnull]) 296 | assert "where id notnull" == to_string(~SQL[where id notnull]) 297 | end 298 | test "as" do 299 | assert "select id as dd" == to_string(~SQL[select id as dd]) 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /test/bnf_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.BNFTest do 5 | use ExUnit.Case, async: true 6 | 7 | test "parse/1" do 8 | assert %{"" => ""} == SQL.BNF.parse(""" 9 | ::= 10 | 11 | """) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/formatter_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.FormatterTest do 5 | use ExUnit.Case, async: true 6 | 7 | test "features/1" do 8 | assert [{:sigils, [:SQL]}, {:extensions, nil}] == SQL.MixFormatter.features([]) 9 | end 10 | 11 | test "format/2 preserve interpolation" do 12 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + {{one}}, (n + {{one}}) * fact from temp where n < 9)" == SQL.MixFormatter.format("with recursive temp(n, fact) as (select 0, 1 union all select n + {{one}}, (n + {{one}}) * fact from temp where n < 9)", []) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/sql_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQLTest do 5 | use ExUnit.Case, async: true 6 | import SQL 7 | 8 | def from(var \\ "users") do 9 | ~SQL[from {{var}} u] 10 | end 11 | 12 | def where(sql, var \\ "john@example.com") do 13 | sql |> ~SQL[where u.email = {{var}}] 14 | end 15 | 16 | describe "composable" do 17 | test "pipedream" do 18 | sql = ~SQL[from users u] 19 | |> ~SQL[where u.email = "john@example.com"] 20 | |> ~SQL[select id, email, inserted_at, updated_at] 21 | 22 | assert ~s(select id, email, inserted_at, updated_at from users u where u.email = "john@example.com") == to_string(sql) 23 | end 24 | 25 | test "functional" do 26 | sql = from() 27 | |> ~SQL[select id, email, inserted_at, updated_at] 28 | |> where() 29 | 30 | assert ["users", "john@example.com"] = sql.params 31 | assert "select id, email, inserted_at, updated_at from ? u where u.email = ?" == to_string(sql) 32 | end 33 | end 34 | 35 | test "inspect/1" do 36 | assert ~s(~SQL"""\nselect +1000\n""") == inspect(~SQL[select +1000]) 37 | end 38 | 39 | test "to_sql/1" do 40 | email = "john@example.com" 41 | assert {"select id, email from users where email = ?", ["john@example.com"]} == to_sql(~SQL""" 42 | select id, email 43 | where email = {{email}} 44 | from users 45 | """) 46 | end 47 | 48 | test "can parse multiple queries" do 49 | email = "john@example.com" 50 | assert {"select id, email from users where email = ?; select id from users", [email]} == to_sql(~SQL""" 51 | select id, email 52 | where email = {{email}} 53 | from users; 54 | select id from users 55 | """) 56 | end 57 | 58 | 59 | describe "error" do 60 | test "missing )" do 61 | assert_raise TokenMissingError, ~r"token missing on", fn -> 62 | SQL.parse("select id in (1, 2") 63 | end 64 | assert_raise TokenMissingError, ~r"token missing on", fn -> 65 | SQL.parse("select id from users join orgs on (id = id") 66 | end 67 | end 68 | 69 | test "missing ]" do 70 | assert_raise TokenMissingError, ~r"token missing on", fn -> 71 | SQL.parse("select id in ([1)") 72 | end 73 | assert_raise TokenMissingError, ~r"token missing on", fn -> 74 | SQL.parse("select id from users join orgs on ([1)") 75 | end 76 | end 77 | 78 | test "missing }" do 79 | assert_raise TokenMissingError, ~r"token missing on", fn -> 80 | SQL.parse("select id in {{1") 81 | end 82 | assert_raise TokenMissingError, ~r"token missing on", fn -> 83 | SQL.parse("select id from users join orgs on {{id") 84 | end 85 | end 86 | 87 | test "missing \"" do 88 | assert_raise TokenMissingError, ~r"token missing on", fn -> 89 | SQL.parse("select id in \"1") 90 | end 91 | assert_raise TokenMissingError, ~r"token missing on", fn -> 92 | SQL.parse("select id from users join orgs on \"id") 93 | end 94 | end 95 | 96 | test "missing \'" do 97 | assert_raise TokenMissingError, ~r"token missing on", fn -> 98 | SQL.parse("select id in '1") 99 | end 100 | assert_raise TokenMissingError, ~r"token missing on", fn -> 101 | SQL.parse("select id from users join orgs on 'id") 102 | end 103 | end 104 | end 105 | 106 | describe "functions" do 107 | test "avg" do 108 | assert "select avg(id)" == to_string(~SQL[select avg(id)]) 109 | end 110 | test "any" do 111 | assert "select any(select *)" == to_string(~SQL[select any(select *)]) 112 | end 113 | test "all" do 114 | assert "select all(select *)" == to_string(~SQL[select all(select *)]) 115 | end 116 | test "count" do 117 | assert "select count(*)" == to_string(~SQL[select count(*)]) 118 | assert "select count(id)" == to_string(~SQL[select count(id)]) 119 | end 120 | test "coalesce" do 121 | assert "select coalesce(a, b)" == to_string(~SQL[select coalesce(a, b)]) 122 | end 123 | test "exists" do 124 | assert "select exists(select *)" == to_string(~SQL[select exists(select *)]) 125 | end 126 | test "min" do 127 | assert "select min(a, b)" == to_string(~SQL[select min(a, b)]) 128 | end 129 | test "max" do 130 | assert "select max(a, b)" == to_string(~SQL[select max(a, b)]) 131 | end 132 | test "sum" do 133 | assert "select sum(id)" == to_string(~SQL[select sum(id)]) 134 | end 135 | end 136 | 137 | describe "with" do 138 | test "recursive" do 139 | assert "with recursive temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 140 | end 141 | 142 | test "regular" do 143 | assert "with temp (n, fact) as (select 0, 1 union all select n + 1, (n + 1) * fact from temp where n < 9)" == to_string(~SQL[with temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)]) 144 | end 145 | 146 | test "complex with" do 147 | sql = ~SQL[ 148 | with customer_rankings as( 149 | select customer_id, 150 | sum(amount) as total_spent, 151 | rank() over(order by sum(amount) desc) as spending_rank 152 | from transactions 153 | group by customer_id 154 | ), 155 | top_customers as( 156 | select c.customer_id, 157 | c.name, 158 | cr.total_spent, 159 | cr.spending_rank 160 | from customer_rankings cr 161 | join customers c on c.customer_id = cr.customer_id 162 | where cr.spending_rank <= 10 163 | ) 164 | select tc.name, 165 | tc.total_spent, 166 | tc.spending_rank 167 | from top_customers tc 168 | order by tc.spending_rank 169 | ] 170 | 171 | output = to_string(sql) 172 | assert output == "with customer_rankings as(select customer_id, sum(amount) as total_spent, rank() over(order by sum(amount) desc) as spending_rank from transactions group by customer_id), top_customers as(select c.customer_id, c.name, cr.total_spent, cr.spending_rank from customer_rankings cr join customers c on c.customer_id = cr.customer_id where cr.spending_rank <= 10) select tc.name, tc.total_spent, tc.spending_rank from top_customers tc order by tc.spending_rank" 173 | end 174 | 175 | test "complex with multiple ctes" do 176 | sql = ~SQL[ 177 | with customer_rankings as ( 178 | select 179 | customer_id, 180 | sum(amount) as total_spent, 181 | rank() over (order by sum(amount) desc) as spending_rank 182 | from transactions 183 | group by customer_id 184 | ), 185 | top_customers as ( 186 | select 187 | c.customer_id, 188 | c.name, 189 | cr.total_spent, 190 | cr.spending_rank 191 | from customer_rankings cr 192 | join customers c on c.customer_id = cr.customer_id 193 | where cr.spending_rank <= 10 194 | ) 195 | select 196 | tc.name, 197 | tc.total_spent, 198 | tc.spending_rank, 199 | case 200 | when tc.total_spent > tc.avg_amount * 2 then 'High Value' 201 | when tc.total_spent > tc.avg_amount then 'Medium Value' 202 | else 'Low Value' 203 | end as customer_segment 204 | from top_customers tc 205 | order by tc.spending_rank, tc.month 206 | ] 207 | 208 | output = to_string(sql) 209 | assert output == "with customer_rankings as(select customer_id, sum(amount) as total_spent, rank() over(order by sum(amount) desc) as spending_rank from transactions group by customer_id), top_customers as(select c.customer_id, c.name, cr.total_spent, cr.spending_rank from customer_rankings cr join customers c on c.customer_id = cr.customer_id where cr.spending_rank <= 10) select tc.name, tc.total_spent, tc.spending_rank, case when tc.total_spent > tc.avg_amount * 2 then 'High Value' when tc.total_spent > tc.avg_amount then 'Medium Value' else 'Low Value' end as customer_segment from top_customers tc order by tc.spending_rank, tc.month" 210 | end 211 | end 212 | 213 | describe "combinations" do 214 | test "except" do 215 | assert "(select id from users) except (select id from users)" == to_string(~SQL[(select id from users) except (select id from users)]) 216 | assert "(select id from users) except select id from users" == to_string(~SQL[(select id from users) except select id from users]) 217 | assert "select id from users except (select id from users)" == to_string(~SQL[select id from users except (select id from users)]) 218 | assert "select id from users except select id from users" == to_string(~SQL[select id from users except select id from users]) 219 | 220 | assert "(select id from users) except all (select id from users)" == to_string(~SQL[(select id from users) except all (select id from users)]) 221 | assert "(select id from users) except all select id from users" == to_string(~SQL[(select id from users) except all select id from users]) 222 | assert "select id from users except all (select id from users)" == to_string(~SQL[select id from users except all (select id from users)]) 223 | assert "select id from users except all select id from users" == to_string(~SQL[select id from users except all select id from users]) 224 | end 225 | 226 | test "intersect" do 227 | assert "(select id from users) intersect (select id from users)" == to_string(~SQL[(select id from users) intersect (select id from users)]) 228 | assert "(select id from users) intersect select id from users" == to_string(~SQL[(select id from users) intersect select id from users]) 229 | assert "select id from users intersect (select id from users)" == to_string(~SQL[select id from users intersect (select id from users)]) 230 | assert "select id from users intersect select id from users" == to_string(~SQL[select id from users intersect select id from users]) 231 | 232 | assert "(select id from users) intersect all (select id from users)" == to_string(~SQL[(select id from users) intersect all (select id from users)]) 233 | assert "(select id from users) intersect all select id from users" == to_string(~SQL[(select id from users) intersect all select id from users]) 234 | assert "select id from users intersect all (select id from users)" == to_string(~SQL[select id from users intersect all (select id from users)]) 235 | assert "select id from users intersect all select id from users" == to_string(~SQL[select id from users intersect all select id from users]) 236 | end 237 | 238 | test "union" do 239 | assert "(select id from users) union (select id from users)" == to_string(~SQL[(select id from users) union (select id from users)]) 240 | assert "(select id from users) union select id from users" == to_string(~SQL[(select id from users) union select id from users]) 241 | assert "select id from users union (select id from users)" == to_string(~SQL[select id from users union (select id from users)]) 242 | assert "select id from users union select id from users" == to_string(~SQL[select id from users union select id from users]) 243 | 244 | assert "(select id from users) union all (select id from users)" == to_string(~SQL[(select id from users) union all (select id from users)]) 245 | assert "(select id from users) union all select id from users" == to_string(~SQL[(select id from users) union all select id from users]) 246 | assert "select id from users union all (select id from users)" == to_string(~SQL[select id from users union all (select id from users)]) 247 | assert "select id from users union all select id from users" == to_string(~SQL[select id from users union all select id from users]) 248 | end 249 | end 250 | 251 | describe "query" do 252 | test "select" do 253 | assert "select id" == to_string(~SQL[select id]) 254 | assert "select id, id as di" == to_string(~SQL[select id, id as di]) 255 | assert "select id, (select id from users) as di" == to_string(~SQL[select id, (select id from users) as di]) 256 | assert "select unknownn" == to_string(~SQL[select unknownn]) 257 | assert "select truee" == to_string(~SQL[select truee]) 258 | assert "select falsee" == to_string(~SQL[select falsee]) 259 | assert "select nulll" == to_string(~SQL[select nulll]) 260 | assert "select isnulll" == to_string(~SQL[select isnulll]) 261 | assert "select notnulll" == to_string(~SQL[select notnulll]) 262 | assert "select ascc" == to_string(~SQL[select ascc]) 263 | assert "select descc" == to_string(~SQL[select descc]) 264 | assert "select distinct id" == to_string(~SQL[select distinct id]) 265 | assert "select distinct on (id, users) id" == to_string(~SQL[select distinct on (id, users) id]) 266 | end 267 | 268 | test "from" do 269 | assert "from users" == to_string(~SQL[from users]) 270 | assert "from users u, persons p" == to_string(~SQL[from users u, persons p]) 271 | assert "from users u" == to_string(~SQL[from users u]) 272 | assert "from users as u" == to_string(~SQL[from users as u]) 273 | assert "from users u" == to_string(~SQL[from users u]) 274 | end 275 | 276 | test "join" do 277 | assert "inner join users" == to_string(~SQL[inner join users]) 278 | assert "join users" == to_string(~SQL[join users]) 279 | assert "left outer join users" == to_string(~SQL[left outer join users]) 280 | assert "left join users" == to_string(~SQL[left join users]) 281 | assert "natural join users" == to_string(~SQL[natural join users]) 282 | assert "full join users" == to_string(~SQL[full join users]) 283 | assert "cross join users" == to_string(~SQL[cross join users]) 284 | assert "join users u" == to_string(~SQL[join users u]) 285 | assert "join users on id = id" == to_string(~SQL[join users on id = id]) 286 | assert "join users u on id = id" == to_string(~SQL[join users u on id = id]) 287 | assert "join users on (id = id)" == to_string(~SQL[join users on (id = id)]) 288 | assert "join (select * from users) on (id = id)" == to_string(~SQL[join (select * from users) on (id = id)]) 289 | assert "join (select * from users) u on (id = id)" == to_string(~SQL[join (select * from users) u on (id = id)]) 290 | end 291 | 292 | test "where" do 293 | assert "where 1 = 2" == to_string(~SQL[where 1 = 2]) 294 | assert "where 1 = 2" == to_string(~SQL[where 1=2]) 295 | assert "where 1 != 2" == to_string(~SQL[where 1 != 2]) 296 | assert "where 1 <> 2" == to_string(~SQL[where 1 <> 2]) 297 | assert "where 1 = 2 and id = users.id and id > 3 or true" == to_string(~SQL[where 1 = 2 and id = users.id and id > 3 or true]) 298 | end 299 | 300 | test "group by" do 301 | assert "group by id" == to_string(~SQL[group by id]) 302 | assert "group by users.id" == to_string(~SQL[group by users.id]) 303 | assert "group by id, users.id" == to_string(~SQL[group by id, users.id]) 304 | end 305 | 306 | test "having" do 307 | assert "having 1 = 2" == to_string(~SQL[having 1 = 2]) 308 | assert "having 1 != 2" == to_string(~SQL[having 1 != 2]) 309 | assert "having 1 <> 2" == to_string(~SQL[having 1 <> 2]) 310 | end 311 | 312 | test "order by" do 313 | assert "order by id" == to_string(~SQL[order by id]) 314 | assert "order by users.id" == to_string(~SQL[order by users.id]) 315 | assert "order by id, users.id, users.id asc, id desc" == to_string(~SQL[order by id, users.id, users.id asc, id desc]) 316 | end 317 | 318 | test "offset" do 319 | assert "offset 1" == to_string(~SQL[offset 1]) 320 | end 321 | 322 | test "limit" do 323 | assert "limit 1" == to_string(~SQL[limit 1]) 324 | end 325 | 326 | test "fetch" do 327 | assert "fetch next from users" == to_string(~SQL[fetch next from users]) 328 | assert "fetch prior from users" == to_string(~SQL[fetch prior from users]) 329 | assert "fetch first from users" == to_string(~SQL[fetch first from users]) 330 | assert "fetch last from users" == to_string(~SQL[fetch last from users]) 331 | assert "fetch absolute 1 from users" == to_string(~SQL[fetch absolute 1 from users]) 332 | assert "fetch relative 1 from users" == to_string(~SQL[fetch relative 1 from users]) 333 | assert "fetch 1 from users" == to_string(~SQL[fetch 1 from users]) 334 | assert "fetch all from users" == to_string(~SQL[fetch all from users]) 335 | assert "fetch forward from users" == to_string(~SQL[fetch forward from users]) 336 | assert "fetch forward 1 from users" == to_string(~SQL[fetch forward 1 from users]) 337 | assert "fetch forward all from users" == to_string(~SQL[fetch forward all from users]) 338 | assert "fetch backward from users" == to_string(~SQL[fetch backward from users]) 339 | assert "fetch backward 1 from users" == to_string(~SQL[fetch backward 1 from users]) 340 | assert "fetch backward all from users" == to_string(~SQL[fetch backward all from users]) 341 | 342 | assert "fetch next in users" == to_string(~SQL[fetch next in users]) 343 | assert "fetch prior in users" == to_string(~SQL[fetch prior in users]) 344 | assert "fetch first in users" == to_string(~SQL[fetch first in users]) 345 | assert "fetch last in users" == to_string(~SQL[fetch last in users]) 346 | assert "fetch absolute 1 in users" == to_string(~SQL[fetch absolute 1 in users]) 347 | assert "fetch relative 1 in users" == to_string(~SQL[fetch relative 1 in users]) 348 | assert "fetch 1 in users" == to_string(~SQL[fetch 1 in users]) 349 | assert "fetch all in users" == to_string(~SQL[fetch all in users]) 350 | assert "fetch forward in users" == to_string(~SQL[fetch forward in users]) 351 | assert "fetch forward 1 in users" == to_string(~SQL[fetch forward 1 in users]) 352 | assert "fetch forward all in users" == to_string(~SQL[fetch forward all in users]) 353 | assert "fetch backward in users" == to_string(~SQL[fetch backward in users]) 354 | assert "fetch backward 1 in users" == to_string(~SQL[fetch backward 1 in users]) 355 | assert "fetch backward all in users" == to_string(~SQL[fetch backward all in users]) 356 | end 357 | end 358 | 359 | describe "datatypes" do 360 | test "integer" do 361 | assert "select 1" == to_string(~SQL[select 1]) 362 | assert "select 1000" == to_string(~SQL[select 1000]) 363 | assert "select -1000" == to_string(~SQL[select -1000]) 364 | assert "select +1000" == to_string(~SQL[select +1000]) 365 | end 366 | 367 | test "float" do 368 | assert "select +10.00" == to_string(~SQL[select +10.00]) 369 | assert "select -10.00" == to_string(~SQL[select -10.00]) 370 | end 371 | 372 | test "identifier" do 373 | assert "select db.users.id" == to_string(~SQL[select db.users.id]) 374 | assert "select db.users" == to_string(~SQL[select db.users]) 375 | assert "select db" == to_string(~SQL[select db]) 376 | end 377 | 378 | test "qouted" do 379 | assert "select \"db.users.id\"" == to_string(~SQL[select "db.users.id"]) 380 | assert "select 'db.users'" == to_string(~SQL[select 'db.users']) 381 | assert "select \"db.users.id\", 'db.users'" == to_string(~SQL[select "db.users.id", 'db.users']) 382 | end 383 | end 384 | 385 | describe "interpolation" do 386 | test "binding" do 387 | var1 = 1 388 | var0 = "id" 389 | var2 = ~SQL[select {{var0}}] 390 | assert ["id"] == var2.params 391 | sql = ~SQL[select {{var2}}, {{var1}}] 392 | assert [var2, 1] == sql.params 393 | assert "select ?, ?" == to_string(sql) 394 | end 395 | 396 | test ". syntax" do 397 | map = %{k: "v"} 398 | sql = ~SQL[select {{map.k <> "v"}}] 399 | assert ["vv"] == sql.params 400 | assert "select ?" == to_string(sql) 401 | end 402 | 403 | test "code" do 404 | sql = ~SQL[select {{0}}, {{%{k: 1}}}] 405 | assert [0, %{k: 1}] == sql.params 406 | assert "select ?, ?" == to_string(sql) 407 | end 408 | 409 | test "in" do 410 | sql = ~SQL"select {{1}} in {{[1, 2]}}" 411 | assert [1, [1, 2]] == sql.params 412 | assert "select ? in ?" == to_string(sql) 413 | 414 | sql = ~SQL"select {{1}} not in {{[1, 2]}}" 415 | assert [1, [1, 2]] == sql.params 416 | assert "select ? not in ?" == to_string(sql) 417 | end 418 | 419 | test "mixin" do 420 | for email <- ["1@example.com", "2@example.com", "3@example.com"] do 421 | sql = from() |> ~SQL[select id, email, inserted_at, updated_at] |> where(email) 422 | assert {"select id, email, inserted_at, updated_at from ? u where u.email = ?", ["users", email]} == SQL.to_sql(sql) 423 | end 424 | end 425 | end 426 | 427 | describe "operators" do 428 | test "=" do 429 | assert "where id = 1" == to_string(~SQL[where id = 1]) 430 | assert "where id = 1" == to_string(~SQL[where id=1]) 431 | end 432 | test "-" do 433 | assert "where id - 1" == to_string(~SQL[where id - 1]) 434 | assert "where id - 1" == to_string(~SQL[where id-1]) 435 | end 436 | test "+" do 437 | assert "where id + 1" == to_string(~SQL[where id + 1]) 438 | assert "where id + 1" == to_string(~SQL[where id+1]) 439 | end 440 | test "*" do 441 | assert "where id * 1" == to_string(~SQL[where id * 1]) 442 | assert "where id * 1" == to_string(~SQL[where id*1]) 443 | end 444 | test "/" do 445 | assert "where id / 1" == to_string(~SQL[where id / 1]) 446 | assert "where id / 1" == to_string(~SQL[where id/1]) 447 | end 448 | test "<>" do 449 | assert "where id <> 1" == to_string(~SQL[where id <> 1]) 450 | assert "where id <> 1" == to_string(~SQL[where id<>1]) 451 | end 452 | test ">" do 453 | assert "where id > 1" == to_string(~SQL[where id > 1]) 454 | assert "where id > 1" == to_string(~SQL[where id>1]) 455 | end 456 | test "<" do 457 | assert "where id < 1" == to_string(~SQL[where id < 1]) 458 | assert "where id < 1" == to_string(~SQL[where id<1]) 459 | end 460 | test ">=" do 461 | assert "where id >= 1" == to_string(~SQL[where id >= 1]) 462 | assert "where id >= 1" == to_string(~SQL[where id>=1]) 463 | end 464 | test "<=" do 465 | assert "where id <= 1" == to_string(~SQL[where id <= 1]) 466 | assert "where id <= 1" == to_string(~SQL[where id<=1]) 467 | end 468 | test "between" do 469 | assert "where id between 1 and 2" == to_string(~SQL[where id between 1 and 2]) 470 | assert "where id not between 1 and 2" == to_string(~SQL[where id not between 1 and 2]) 471 | assert "where id between symmetric 1 and 2" == to_string(~SQL[where id between symmetric 1 and 2]) 472 | assert "where id not between symmetric 1 and 2" == to_string(~SQL[where id not between symmetric 1 and 2]) 473 | end 474 | test "like" do 475 | assert "where id like 1" == to_string(~SQL[where id like 1]) 476 | end 477 | test "ilike" do 478 | assert "where id ilike 1" == to_string(~SQL[where id ilike 1]) 479 | end 480 | test "in" do 481 | assert "where id in (1, 2)" == to_string(~SQL[where id in (1, 2)]) 482 | end 483 | test "is" do 484 | assert "where id is null" == to_string(~SQL[where id is null]) 485 | assert "where id is false" == to_string(~SQL[where id is false]) 486 | assert "where id is true" == to_string(~SQL[where id is true]) 487 | assert "where id is unknown" == to_string(~SQL[where id is unknown]) 488 | 489 | assert "where id is not null" == to_string(~SQL[where id is not null]) 490 | assert "where id is not false" == to_string(~SQL[where id is not false]) 491 | assert "where id is not true" == to_string(~SQL[where id is not true]) 492 | assert "where id is not unknown" == to_string(~SQL[where id is not unknown]) 493 | 494 | assert "where id is distinct from 1" == to_string(~SQL[where id is distinct from 1]) 495 | assert "where id is not distinct from 1" == to_string(~SQL[where id is not distinct from 1]) 496 | 497 | assert "where id isnull" == to_string(~SQL[where id isnull]) 498 | assert "where id notnull" == to_string(~SQL[where id notnull]) 499 | end 500 | test "as" do 501 | assert "select id as dd" == to_string(~SQL[select id as dd]) 502 | end 503 | end 504 | end 505 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 DBVisor 3 | 4 | defmodule SQL.Repo do 5 | use Ecto.Repo, otp_app: :sql, adapter: Ecto.Adapters.Postgres 6 | end 7 | ExUnit.start() 8 | --------------------------------------------------------------------------------