├── .formatter.exs
├── .github
├── FUNDING.yml
└── workflows
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── VERSION
├── coveralls.json
├── guides
└── images
│ ├── logo.png
│ ├── logo.svg
│ ├── logo_name.png
│ └── your_logo_here.png
├── lib
├── sql_fmt.ex
└── sql_fmt
│ ├── format_options.ex
│ ├── formatter.ex
│ ├── helpers.ex
│ └── native.ex
├── mix.exs
├── mix.lock
├── native
└── sql_fmt_nif
│ ├── .cargo
│ └── config.toml
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── Cross.toml
│ └── src
│ └── lib.rs
└── test
├── sql_fmt
├── formatter_test.exs
└── helpers_test.exs
├── sql_fmt_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | line_length: 120,
4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
5 | ]
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [akoutmos]
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: SqlFmt CI
2 |
3 | env:
4 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 | SHELL: sh
6 | RUSTLER_FORCE_BUILD: "true"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | branches: [master]
13 |
14 | jobs:
15 | static_analysis:
16 | name: Static Analysis
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v2
22 | - name: Set up Elixir
23 | uses: erlef/setup-beam@v1
24 | with:
25 | elixir-version: "1.17.3"
26 | otp-version: "27.1"
27 | - name: Restore dependencies cache
28 | uses: actions/cache@v4
29 | with:
30 | path: deps
31 | key: ${{ runner.os }}-mix-v2-${{ hashFiles('**/mix.lock') }}
32 | restore-keys: ${{ runner.os }}-mix-v2-
33 | - name: Install dependencies
34 | run: mix deps.get
35 | - name: Restore PLT cache
36 | uses: actions/cache@v4
37 | with:
38 | path: priv/plts
39 | key: ${{ runner.os }}-mix-v2-${{ hashFiles('**/mix.lock') }}
40 | restore-keys: ${{ runner.os }}-mix-v2-
41 | - name: Mix Formatter
42 | run: mix format --check-formatted
43 | - name: Check for compiler warnings
44 | run: mix compile --warnings-as-errors
45 | - name: Credo strict checks
46 | run: mix credo --strict
47 | - name: Doctor documentation checks
48 | run: mix doctor
49 |
50 | unit_test:
51 | name: Run ExUnit tests
52 | runs-on: ubuntu-latest
53 |
54 | strategy:
55 | matrix:
56 | elixir: ["1.17"]
57 | otp: ["25", "26", "27"]
58 |
59 | steps:
60 | - name: Checkout code
61 | uses: actions/checkout@v2
62 | - name: Set up Elixir
63 | uses: erlef/setup-beam@v1
64 | with:
65 | elixir-version: ${{ matrix.elixir }}
66 | otp-version: ${{ matrix.otp }}
67 | - name: Restore dependencies cache
68 | uses: actions/cache@v4
69 | with:
70 | path: deps
71 | key: ${{ runner.os }}-mix-v2-${{ hashFiles('**/mix.lock') }}
72 | restore-keys: ${{ runner.os }}-mix-v2-
73 | - name: Install dependencies
74 | run: mix deps.get
75 | - name: ExUnit tests
76 | run: mix coveralls.github
77 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build precompiled NIFs
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | build_release:
10 | name: NIF ${{ matrix.nif }} - ${{ matrix.job.target }} (${{ matrix.job.os }})
11 | runs-on: ${{ matrix.job.os }}
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | nif: ["2.16", "2.15"]
16 | job:
17 | - {
18 | target: arm-unknown-linux-gnueabihf,
19 | os: ubuntu-20.04,
20 | use-cross: true,
21 | }
22 | - {
23 | target: aarch64-unknown-linux-gnu,
24 | os: ubuntu-20.04,
25 | use-cross: true,
26 | }
27 | - {
28 | target: aarch64-unknown-linux-musl,
29 | os: ubuntu-20.04,
30 | use-cross: true,
31 | }
32 | - { target: aarch64-apple-darwin, os: macos-13 }
33 | - {
34 | target: riscv64gc-unknown-linux-gnu,
35 | os: ubuntu-20.04,
36 | use-cross: true,
37 | }
38 | - { target: x86_64-apple-darwin, os: macos-13 }
39 | - { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04 }
40 | - {
41 | target: x86_64-unknown-linux-musl,
42 | os: ubuntu-20.04,
43 | use-cross: true,
44 | }
45 | - { target: x86_64-pc-windows-gnu, os: windows-2019 }
46 | - { target: x86_64-pc-windows-msvc, os: windows-2019 }
47 |
48 | steps:
49 | - name: Checkout source code
50 | uses: actions/checkout@v3
51 |
52 | - name: Extract project version
53 | shell: bash
54 | run: |
55 | # Get the project version from mix.exs
56 | echo "PROJECT_VERSION=$(cat VERSION)" >> $GITHUB_ENV
57 |
58 | - name: Install Rust toolchain
59 | uses: dtolnay/rust-toolchain@stable
60 | with:
61 | toolchain: stable
62 | target: ${{ matrix.job.target }}
63 |
64 | - name: Build the project
65 | id: build-crate
66 | uses: philss/rustler-precompiled-action@v1.0.1
67 | with:
68 | project-name: sql_fmt_nif
69 | project-version: ${{ env.PROJECT_VERSION }}
70 | target: ${{ matrix.job.target }}
71 | nif-version: ${{ matrix.nif }}
72 | use-cross: ${{ matrix.job.use-cross }}
73 | project-dir: "native/sql_fmt_nif"
74 |
75 | - name: Artifact upload
76 | uses: actions/upload-artifact@v4
77 | with:
78 | name: ${{ steps.build-crate.outputs.file-name }}
79 | path: ${{ steps.build-crate.outputs.file-path }}
80 |
81 | - name: Publish archives and packages
82 | uses: softprops/action-gh-release@v1
83 | with:
84 | files: |
85 | ${{ steps.build-crate.outputs.file-path }}
86 | if: startsWith(github.ref, 'refs/tags/')
87 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
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 | sql_fmt-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
28 | # Shared objects build by Rust.
29 | *.so
30 |
31 | # Checksum file generated by RustlerPrecompiled
32 | checksum-*
33 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [0.4.0] - 2025-3-24
11 |
12 | ### Fixed
13 |
14 | - Updated to a newer version of `sqlformat-rs`.
15 |
16 | ## [0.3.0] - 2024-11-04
17 |
18 | ### Fixed
19 |
20 | - Updated to a newer version of `sqlformat-rs` to avoid splitting Postgres operators.
21 |
22 | ## [0.2.0] - 2024-10-08
23 |
24 | ### Added
25 |
26 | - The `~SQL` sigil to format SQL strings in Elixir code [#3](https://github.com/akoutmos/sql_fmt/pull/3).
27 | - A Mix Formatter plugin so that SQL files and `~SQL` sigil statements are formatted via Mix [#3](https://github.com/akoutmos/sql_fmt/pull/3).
28 |
29 | ## [0.1.0] - 2024-10-06
30 |
31 | ### Added
32 |
33 | - Initial release for SqlFmt.
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Alexander Koutmos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Format and pretty print SQL queries
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | # Contents
32 |
33 | - [Installation](#installation)
34 | - [Example Output](#example-output)
35 | - [Mix Formatter](#mix-formatter)
36 | - [Supporting SqlFmt](#supporting-sqlfmt)
37 | - [Attribution](#attribution)
38 |
39 | ## Installation
40 |
41 | [Available in Hex](https://hex.pm/packages/sql_fmt), the package can be installed by adding `sql_fmt` to your list of
42 | dependencies in `mix.exs`:
43 |
44 | ```elixir
45 | def deps do
46 | [
47 | {:sql_fmt, "~> 0.4.0"}
48 | ]
49 | end
50 | ```
51 |
52 | Documentation can be found at [https://hexdocs.pm/sql_fmt](https://hexdocs.pm/sql_fmt).
53 |
54 | ## Example Output
55 |
56 | After setting up SqlFmt in your application you can use the SqlFmt functions in order to format queries. Here are a
57 | couple examples of queries with having parameters inline and with passing in the parameters separately:
58 |
59 | ```elixir
60 | iex(1)> {:ok, formatted_sql} = SqlFmt.format_query("select * from businesses where id in ('c6f5c5f1-a1fc-4c9a-91f7-6aa40f1e233d', 'f339d4ce-96b6-4440-a541-28a0fb611139');")
61 | {:ok, "SELECT\n *\nFROM\n businesses\nWHERE\n id IN (\n 'c6f5c5f1-a1fc-4c9a-91f7-6aa40f1e233d',\n 'f339d4ce-96b6-4440-a541-28a0fb611139'\n );"}
62 |
63 | iex(2)> IO.puts(formatted_sql)
64 | SELECT
65 | *
66 | FROM
67 | businesses
68 | WHERE
69 | id IN (
70 | 'c6f5c5f1-a1fc-4c9a-91f7-6aa40f1e233d',
71 | 'f339d4ce-96b6-4440-a541-28a0fb611139'
72 | );
73 | :ok
74 | ```
75 |
76 | ```elixir
77 | iex(1)> {:ok, formatted_sql} = SqlFmt.format_query_with_params("select * from help where help.\"col\" in $1;", ["'asdf'"])
78 | {:ok, "SELECT\n *\nFROM\n help\nWHERE\n help.\"col\" IN 'asdf';"}
79 |
80 | iex(2)> IO.puts(formatted_sql)
81 | SELECT
82 | *
83 | FROM
84 | help
85 | WHERE
86 | help."col" IN 'asdf';
87 | :ok
88 | ```
89 |
90 | Be sure to checkout the HexDocs as you can also provide formatting options to the functions to tailor the output to your
91 | liking.
92 |
93 | ## Mix Formatter
94 |
95 | SqlFmt also provides you with the `~SQL` sigil that can be used to format SQL via Mix Formatter plugin. To set up the
96 | Mix Formatter plugin, simply install this package and add update your `.formatter.exs` file as follows:
97 |
98 | ```elixir
99 | [
100 | plugins: [SqlFmt.MixFormatter],
101 | inputs: ["**/*.sql"],
102 | # ...
103 | ]
104 | ```
105 |
106 | With this configuration, the SqlFmt Mix Format plugin will now format all `~SQL` sigils and all files ending in `.sql`.
107 | This can be particularly useful in Ecto migrations where you have large `execute` statements and you want to make sure
108 | that your code is readable. Check out the `SqlFmt.MixFormatter` module docs for more information.
109 |
110 | ## Supporting SqlFmt
111 |
112 | If you rely on this library help you debug your Ecto/SQL queries, it would much appreciated if you can give back
113 | to the project in order to help ensure its continued development.
114 |
115 | Checkout my [GitHub Sponsorship page](https://github.com/sponsors/akoutmos) if you want to help out!
116 |
117 | ### Gold Sponsors
118 |
119 |
120 |
121 |
122 |
123 | ### Silver Sponsors
124 |
125 |
126 |
127 |
128 |
129 | ### Bronze Sponsors
130 |
131 |
132 |
133 |
134 |
135 | ## Attribution
136 |
137 | - The logo for the project is an edited version of an SVG image from the [unDraw project](https://undraw.co/).
138 | - The SqlFmt library leans on the Rust library [sqlformat-rs](https://github.com/shssoichiro/sqlformat-rs) for SQL
139 | statement formatting.
140 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.4.0
2 |
--------------------------------------------------------------------------------
/coveralls.json:
--------------------------------------------------------------------------------
1 | {
2 | "skip_files": [
3 | "lib/sql_fmt/native.ex"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/guides/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akoutmos/sql_fmt/df67df4acb800c2be53594564d8919c7d05ef880/guides/images/logo.png
--------------------------------------------------------------------------------
/guides/images/logo.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/guides/images/logo_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akoutmos/sql_fmt/df67df4acb800c2be53594564d8919c7d05ef880/guides/images/logo_name.png
--------------------------------------------------------------------------------
/guides/images/your_logo_here.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akoutmos/sql_fmt/df67df4acb800c2be53594564d8919c7d05ef880/guides/images/your_logo_here.png
--------------------------------------------------------------------------------
/lib/sql_fmt.ex:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt do
2 | @moduledoc """
3 | This library provides an Elixir wrapper around the Rust
4 | [sqlformat](https://github.com/shssoichiro/sqlformat-rs) library. This allows you
5 | to efficiently format and pretty print SQL queries.
6 | """
7 |
8 | alias SqlFmt.FormatOptions
9 | alias SqlFmt.Native
10 |
11 | @doc """
12 | This function takes a query as a string along with optional formatting
13 | settings and returns the formatted query.
14 |
15 | For formatting settings look at the `SqlFmt.FormatOptions` module docs.
16 | """
17 | @spec format_query(query :: String.t()) :: {:ok, String.t()}
18 | @spec format_query(query :: String.t(), fmt_opts :: keyword()) :: {:ok, String.t()}
19 | def format_query(query, fmt_opts \\ []) do
20 | format_options = FormatOptions.new(fmt_opts)
21 |
22 | Native.format(query, format_options)
23 | end
24 |
25 | @doc """
26 | This function takes a query as a string along with its parameters as a list of strings
27 | and optional formatting settings and returns the formatted query. As a note, when using this
28 | function all of the element in `query_params` must be strings and they all must be
29 | extrapolated out to their appropriate SQL values. For example query params of `["my_id"]`
30 | should be `["'my_id'"]` so that it is valid SQL when the parameter substitution takes place.
31 |
32 | For formatting settings look at the `SqlFmt.FormatOptions` module docs.
33 | """
34 | @spec format_query_with_params(query :: String.t(), query_params :: list(String.t())) :: {:ok, String.t()}
35 | @spec format_query_with_params(query :: String.t(), query_params :: list(String.t()), fmt_opts :: keyword()) ::
36 | {:ok, String.t()}
37 | def format_query_with_params(query, query_params, fmt_opts \\ []) do
38 | format_options = FormatOptions.new(fmt_opts)
39 |
40 | Native.format(query, query_params, format_options)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/sql_fmt/format_options.ex:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.FormatOptions do
2 | @moduledoc """
3 | These options are used to control how the SQL statement is formatted. The
4 | available formatting options are:
5 |
6 | * `:indent` – Specifies how many spaces are used for indents
7 | Defaults to `2`.
8 |
9 | * `:uppercase` – Configures whether SQL reserved words are capitalized.
10 | Defaults to `true`.
11 |
12 | * `:lines_between_queries` – Specifies how many line breaks should be
13 | present after a query.
14 | Defaults to `1`.
15 |
16 | * `:ignore_case_convert` – Configures whether certain strings should
17 | not be case converted.
18 | Defaults to `[]`.
19 | """
20 |
21 | defstruct indent: 2,
22 | uppercase: true,
23 | lines_between_queries: 1,
24 | ignore_case_convert: []
25 |
26 | @typedoc "The available formatting options."
27 | @type t :: %__MODULE__{
28 | indent: non_neg_integer(),
29 | uppercase: boolean(),
30 | lines_between_queries: non_neg_integer(),
31 | ignore_case_convert: list(String.t())
32 | }
33 |
34 | @doc """
35 | Create an instance of the `FormatOptions` struct.
36 | """
37 | @spec new(keyword) :: t
38 | def new(opts \\ []) do
39 | struct!(__MODULE__, opts)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/sql_fmt/formatter.ex:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.MixFormatter do
2 | @moduledoc """
3 | Format SQL queries from `.sql` files or `~SQL` sigils.
4 |
5 | This is a `mix format` [plugin](https://hexdocs.pm/mix/main/Mix.Tasks.Format.html#module-plugins).
6 |
7 | ## Setup
8 |
9 | Add it as a plugin to your `.formatter.exs` file. If you want it to run automatically for
10 | all your project's `sql` files make sure to put the proper pattern in the `inputs` option.
11 |
12 | ```elixir
13 | [
14 | plugins: [SqlFmt.MixFormatter],
15 | inputs: ["**/*.sql"],
16 | # ...
17 | ]
18 | ```
19 |
20 | ## Options
21 |
22 | The following options are supported. You should specify them under a `:sql_fmt` key in
23 | your `.formatter.exs`.
24 |
25 | * `:indent` - specifies how many spaces are used for indents. Defaults to `2`.
26 |
27 | * `:uppercase` - specifies if the SQL reserved words will be capitalized. Defaults
28 | to `true`.
29 |
30 | * `:lines_between_queries` - specifies how many line breaks should be
31 | present after a query. Applicable only for input containing multiple queries.
32 | Defaults to `1`.
33 |
34 | * `:ignore_case_convert` - comma separated list of strings that should
35 | not be case converted. If not set all reserved keywords are capitalized.
36 |
37 | * `:extensions` - change the default file extensions to be considered. Defaults to
38 | `[".sql"]`
39 |
40 | An example configuration object with all supported options set follows.
41 |
42 | [
43 | # ...omitted
44 | sql_fmt: [
45 | indent: 4,
46 | uppercase: true,
47 | lines_between_queries: 2,
48 | ignore_case_convert: ["where", "into"],
49 | extensions: [".sql", ".query"]
50 | ]
51 | ]
52 |
53 | ## Formatting
54 |
55 | The formatter uses `SqlFmt.format_query/2` in order to format the input `SQL`
56 | queries or inline `~SQL` sigils.
57 |
58 | > #### Single line `~SQL` sigils {: .info}
59 | >
60 | > Notice that if an `~SQL` sigil with a single delimiter is used then this is
61 | > respected and only the reserved keyowrds are capitalized. For example:
62 | >
63 | > ```elixir
64 | > query = ~SQL"select * from users"
65 | > ```
66 | >
67 | > would be formatted as:
68 | >
69 | > ```elixir
70 | > query = ~SQL"SELECT * FROM users"
71 | > ```
72 | """
73 | @behaviour Mix.Tasks.Format
74 |
75 | @impl Mix.Tasks.Format
76 | def features(opts) do
77 | extensions = get_in(opts, [:sql_fmt, :extensions]) || [".sql"]
78 | [sigils: [:SQL], extensions: extensions]
79 | end
80 |
81 | @impl Mix.Tasks.Format
82 | def format(source, opts) do
83 | formatter_opts =
84 | opts
85 | |> Keyword.get(:sql_fmt, [])
86 | |> Keyword.drop([:extensions])
87 | |> Keyword.validate!(indent: 2, uppercase: true, lines_between_queries: 1, ignore_case_convert: [])
88 |
89 | {:ok, formatted} = SqlFmt.format_query(source, formatter_opts)
90 |
91 | # we need some special handling for the sigils:
92 | #
93 | # - If we are handling an SQL sigil and the opening delimiter is a single
94 | # character we should not add a trailing line
95 | # - If the sigil's optening delimiter is a single character we need
96 | # to remove all newlines introduced by the formatter
97 |
98 | formatted
99 | |> maybe_remove_newlines(opts[:sigil], opts[:opening_delimiter])
100 | |> maybe_add_newline(opts[:sigil], opts[:opening_delimiter])
101 | end
102 |
103 | defp maybe_remove_newlines(query, :SQL, <<_>>), do: String.replace(query, ~r/\s+/, " ") |> String.trim()
104 | defp maybe_remove_newlines(query, _, _), do: query
105 |
106 | defp maybe_add_newline(query, :SQL, <<_, _, _>>), do: query <> "\n"
107 | defp maybe_add_newline(query, _, _), do: query
108 | end
109 |
--------------------------------------------------------------------------------
/lib/sql_fmt/helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.Helpers do
2 | @moduledoc ~S"""
3 | This module contains the `~SQL` sigil implementation.
4 |
5 | You can wrap your inline sql queries with the `~SQL` sigil and the formatter
6 | will format them using the `SqlFmt.format_query/2` function.
7 |
8 | > #### Sigil Caveats {: .info}
9 | >
10 | > Sigils with capitalized letters do not interpolate strings. So your `~SQL`
11 | > queries must be complete queries without any `#{...}` entries as those will
12 | > not be expanded. Take a look at the [Elixir Docs](https://hexdocs.pm/elixir/1.12/Macro.html#module-custom-sigils)
13 | > for more information.
14 | """
15 |
16 | @doc """
17 | Indicates that a string is an SQL query.
18 |
19 | This is currently used only by the `SqlFmt.MixFormatter` `mix format` plugin
20 | for formatting inling `SQL` in your elixir code.
21 | """
22 | def sigil_SQL(query, _modifiers) do
23 | query
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/sql_fmt/native.ex:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.Native do
2 | @moduledoc false
3 |
4 | version = Mix.Project.config()[:version]
5 |
6 | use RustlerPrecompiled,
7 | otp_app: :sql_fmt,
8 | crate: :sql_fmt_nif,
9 | base_url: "https://github.com/akoutmos/sql_fmt/releases/download/v#{version}",
10 | force_build: System.get_env("RUSTLER_FORCE_BUILD") in ["1", "true"],
11 | targets: Enum.uniq(["aarch64-unknown-linux-musl" | RustlerPrecompiled.Config.default_targets()]),
12 | version: version
13 |
14 | @doc false
15 | def format(_query, _format_options), do: :erlang.nif_error(:nif_not_loaded)
16 | def format(_query, _query_params, _format_options), do: :erlang.nif_error(:nif_not_loaded)
17 | end
18 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :sql_fmt,
7 | name: "SqlFmt",
8 | version: project_version(),
9 | elixir: "~> 1.16",
10 | start_permanent: Mix.env() == :prod,
11 | source_url: "https://github.com/akoutmos/sql_fmt",
12 | homepage_url: "https://hex.pm/packages/sql_fmt",
13 | description: "Pretty print SQL queries",
14 | elixirc_paths: elixirc_paths(Mix.env()),
15 | test_coverage: [tool: ExCoveralls],
16 | preferred_cli_env: [
17 | coveralls: :test,
18 | "coveralls.detail": :test,
19 | "coveralls.post": :test,
20 | "coveralls.html": :test,
21 | "coveralls.github": :test
22 | ],
23 | dialyzer: [
24 | plt_add_apps: [:mix],
25 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
26 | ],
27 | package: package(),
28 | deps: deps(),
29 | docs: docs(),
30 | aliases: aliases()
31 | ]
32 | end
33 |
34 | defp project_version do
35 | "VERSION"
36 | |> File.read!()
37 | |> String.trim()
38 | end
39 |
40 | # Run "mix help compile.app" to learn about applications.
41 | def application do
42 | [
43 | extra_applications: [:logger]
44 | ]
45 | end
46 |
47 | defp elixirc_paths(:test), do: ["lib", "test/support"]
48 | defp elixirc_paths(_), do: ["lib"]
49 |
50 | # Run "mix help deps" to learn about dependencies.
51 | defp deps do
52 | [
53 | # Production deps
54 | {:rustler_precompiled, "~> 0.4"},
55 | {:rustler, ">= 0.0.0", optional: true},
56 |
57 | # Dev deps
58 | {:doctor, "~> 0.21", only: :dev},
59 | {:ex_doc, "~> 0.34", only: :dev},
60 | {:credo, "~> 1.7", only: :dev},
61 | {:dialyxir, "~> 1.4", only: :dev, runtime: false},
62 |
63 | # Test deps
64 | {:excoveralls, "~> 0.18", only: :test, runtime: false}
65 | ]
66 | end
67 |
68 | defp docs do
69 | [
70 | main: "readme",
71 | source_ref: "master",
72 | logo: "guides/images/logo.png",
73 | extras: [
74 | "README.md"
75 | ]
76 | ]
77 | end
78 |
79 | defp package do
80 | [
81 | name: "sql_fmt",
82 | files:
83 | ~w(lib mix.exs README.md LICENSE CHANGELOG.md native/sql_fmt_nif/.cargo native/sql_fmt_nif/src native/sql_fmt_nif/Cargo.* VERSION checksum-*.exs),
84 | licenses: ["MIT"],
85 | maintainers: ["Alex Koutmos"],
86 | links: %{
87 | "GitHub" => "https://github.com/akoutmos/sql_fmt",
88 | "Sponsor" => "https://github.com/sponsors/akoutmos"
89 | }
90 | ]
91 | end
92 |
93 | defp aliases do
94 | [
95 | docs: ["docs", ©_files/1]
96 | ]
97 | end
98 |
99 | defp copy_files(_) do
100 | # Set up directory structure
101 | File.mkdir_p!("./doc/guides/images")
102 |
103 | # Copy over image files
104 | "./guides/images/"
105 | |> File.ls!()
106 | |> Enum.each(fn image_file ->
107 | File.cp!("./guides/images/#{image_file}", "./doc/guides/images/#{image_file}")
108 | end)
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3 | "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
4 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"},
5 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
6 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
7 | "doctor": {:hex, :doctor, "0.22.0", "223e1cace1f16a38eda4113a5c435fa9b10d804aa72d3d9f9a71c471cc958fe7", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "96e22cf8c0df2e9777dc55ebaa5798329b9028889c4023fed3305688d902cd5b"},
8 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
9 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
10 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [: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", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
11 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
12 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
14 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
15 | "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"},
16 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
17 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
18 | "rustler": {:hex, :rustler, "0.36.1", "2d4b1ff57ea2789a44756a40dbb5fbb73c6ee0a13d031dcba96d0a5542598a6a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f3fba4ad272970e0d1bc62972fc4a99809651e54a125c5242de9bad4574b2d02"},
19 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"},
20 | "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
21 | }
22 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.x86_64-apple-darwin]
2 | rustflags = [
3 | "-C", "link-arg=-undefined",
4 | "-C", "link-arg=dynamic_lookup",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 | # See https://github.com/rust-lang/rust/issues/59302
14 | [target.x86_64-unknown-linux-musl]
15 | rustflags = [
16 | "-C", "target-feature=-crt-static"
17 | ]
18 |
19 | # See https://github.com/rust-lang/rust/issues/59302
20 | [target.aarch64-unknown-linux-musl]
21 | rustflags = [
22 | "-C", "target-feature=-crt-static"
23 | ]
24 |
25 | [profile.release]
26 | lto = true
27 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "cfg-if"
7 | version = "1.0.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
10 |
11 | [[package]]
12 | name = "heck"
13 | version = "0.5.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
16 |
17 | [[package]]
18 | name = "inventory"
19 | version = "0.3.15"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
22 |
23 | [[package]]
24 | name = "libloading"
25 | version = "0.8.5"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
28 | dependencies = [
29 | "cfg-if",
30 | "windows-targets",
31 | ]
32 |
33 | [[package]]
34 | name = "memchr"
35 | version = "2.7.4"
36 | source = "registry+https://github.com/rust-lang/crates.io-index"
37 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
38 |
39 | [[package]]
40 | name = "proc-macro2"
41 | version = "1.0.89"
42 | source = "registry+https://github.com/rust-lang/crates.io-index"
43 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
44 | dependencies = [
45 | "unicode-ident",
46 | ]
47 |
48 | [[package]]
49 | name = "quote"
50 | version = "1.0.37"
51 | source = "registry+https://github.com/rust-lang/crates.io-index"
52 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
53 | dependencies = [
54 | "proc-macro2",
55 | ]
56 |
57 | [[package]]
58 | name = "regex-lite"
59 | version = "0.1.6"
60 | source = "registry+https://github.com/rust-lang/crates.io-index"
61 | checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
62 |
63 | [[package]]
64 | name = "rustler"
65 | version = "0.35.0"
66 | source = "registry+https://github.com/rust-lang/crates.io-index"
67 | checksum = "b705f2c3643cc170d8888cb6bad589155d9c0248f3104ef7a04c2b7ffbaf13fc"
68 | dependencies = [
69 | "inventory",
70 | "libloading",
71 | "regex-lite",
72 | "rustler_codegen",
73 | ]
74 |
75 | [[package]]
76 | name = "rustler_codegen"
77 | version = "0.35.0"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "3ad56caff00562948bd6ac33c18dbc579e5a1bbee2d7f2f54073307e57f6b57a"
80 | dependencies = [
81 | "heck",
82 | "inventory",
83 | "proc-macro2",
84 | "quote",
85 | "syn",
86 | ]
87 |
88 | [[package]]
89 | name = "sql_fmt_nif"
90 | version = "0.4.0"
91 | dependencies = [
92 | "rustler",
93 | "sqlformat",
94 | ]
95 |
96 | [[package]]
97 | name = "sqlformat"
98 | version = "0.3.5"
99 | source = "registry+https://github.com/rust-lang/crates.io-index"
100 | checksum = "a0d7b3e8a3b6f2ee93ac391a0f757c13790caa0147892e3545cd549dd5b54bc0"
101 | dependencies = [
102 | "unicode_categories",
103 | "winnow",
104 | ]
105 |
106 | [[package]]
107 | name = "syn"
108 | version = "2.0.87"
109 | source = "registry+https://github.com/rust-lang/crates.io-index"
110 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
111 | dependencies = [
112 | "proc-macro2",
113 | "quote",
114 | "unicode-ident",
115 | ]
116 |
117 | [[package]]
118 | name = "unicode-ident"
119 | version = "1.0.13"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
122 |
123 | [[package]]
124 | name = "unicode_categories"
125 | version = "0.1.1"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
128 |
129 | [[package]]
130 | name = "windows-targets"
131 | version = "0.52.6"
132 | source = "registry+https://github.com/rust-lang/crates.io-index"
133 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
134 | dependencies = [
135 | "windows_aarch64_gnullvm",
136 | "windows_aarch64_msvc",
137 | "windows_i686_gnu",
138 | "windows_i686_gnullvm",
139 | "windows_i686_msvc",
140 | "windows_x86_64_gnu",
141 | "windows_x86_64_gnullvm",
142 | "windows_x86_64_msvc",
143 | ]
144 |
145 | [[package]]
146 | name = "windows_aarch64_gnullvm"
147 | version = "0.52.6"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
150 |
151 | [[package]]
152 | name = "windows_aarch64_msvc"
153 | version = "0.52.6"
154 | source = "registry+https://github.com/rust-lang/crates.io-index"
155 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
156 |
157 | [[package]]
158 | name = "windows_i686_gnu"
159 | version = "0.52.6"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
162 |
163 | [[package]]
164 | name = "windows_i686_gnullvm"
165 | version = "0.52.6"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
168 |
169 | [[package]]
170 | name = "windows_i686_msvc"
171 | version = "0.52.6"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
174 |
175 | [[package]]
176 | name = "windows_x86_64_gnu"
177 | version = "0.52.6"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
180 |
181 | [[package]]
182 | name = "windows_x86_64_gnullvm"
183 | version = "0.52.6"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
186 |
187 | [[package]]
188 | name = "windows_x86_64_msvc"
189 | version = "0.52.6"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
192 |
193 | [[package]]
194 | name = "winnow"
195 | version = "0.6.26"
196 | source = "registry+https://github.com/rust-lang/crates.io-index"
197 | checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
198 | dependencies = [
199 | "memchr",
200 | ]
201 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sql_fmt_nif"
3 | version = "0.4.0"
4 | authors = ["Alex Koutmos"]
5 | edition = "2021"
6 |
7 | [lib]
8 | name = "sql_fmt_nif"
9 | path = "src/lib.rs"
10 | crate-type = ["cdylib"]
11 |
12 | [dependencies]
13 | rustler = "0.35.0"
14 | sqlformat = "0.3.5"
15 |
16 | [features]
17 | default = ["nif_version_2_15"]
18 | nif_version_2_15 = ["rustler/nif_version_2_15"]
19 | nif_version_2_16 = ["rustler/nif_version_2_16"]
20 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/Cross.toml:
--------------------------------------------------------------------------------
1 | [build.env]
2 | passthrough = [
3 | "RUSTLER_NIF_VERSION"
4 | ]
5 |
--------------------------------------------------------------------------------
/native/sql_fmt_nif/src/lib.rs:
--------------------------------------------------------------------------------
1 | use rustler::Atom;
2 | use rustler::NifStruct;
3 | use rustler::NifTuple;
4 |
5 | use sqlformat::{FormatOptions, Indent, QueryParams};
6 |
7 | mod atoms {
8 | rustler::atoms! {
9 | ok,
10 | error
11 | }
12 | }
13 |
14 | #[derive(NifTuple)]
15 | struct StringResultTuple {
16 | lhs: Atom,
17 | rhs: String,
18 | }
19 |
20 | #[derive(NifStruct)]
21 | #[module = "SqlFmt.FormatOptions"]
22 | pub struct ElixirFormatOptions<'a> {
23 | pub indent: u8,
24 | pub uppercase: bool,
25 | pub lines_between_queries: u8,
26 | pub ignore_case_convert: Vec<&'a str>,
27 | }
28 |
29 | #[rustler::nif(schedule = "DirtyCpu")]
30 | fn format(sql_query: String, format_options: ElixirFormatOptions) -> StringResultTuple {
31 | let options = FormatOptions {
32 | indent: Indent::Spaces(format_options.indent),
33 | uppercase: Some(format_options.uppercase),
34 | lines_between_queries: format_options.lines_between_queries,
35 | ignore_case_convert: Some(format_options.ignore_case_convert),
36 | };
37 |
38 | let formatted_sql = sqlformat::format(sql_query.as_str(), &QueryParams::None, &options);
39 |
40 | return StringResultTuple {
41 | lhs: atoms::ok(),
42 | rhs: formatted_sql.to_string(),
43 | };
44 | }
45 |
46 | #[rustler::nif(schedule = "DirtyCpu")]
47 | fn format(
48 | sql_query: String,
49 | query_params: Vec,
50 | format_options: ElixirFormatOptions,
51 | ) -> StringResultTuple {
52 | let options = FormatOptions {
53 | indent: Indent::Spaces(format_options.indent),
54 | uppercase: Some(format_options.uppercase),
55 | lines_between_queries: format_options.lines_between_queries,
56 | ignore_case_convert: Some(format_options.ignore_case_convert),
57 | };
58 |
59 | let formatted_sql = sqlformat::format(
60 | sql_query.as_str(),
61 | &QueryParams::Indexed(query_params),
62 | &options,
63 | );
64 |
65 | return StringResultTuple {
66 | lhs: atoms::ok(),
67 | rhs: formatted_sql.to_string(),
68 | };
69 | }
70 |
71 | rustler::init!("Elixir.SqlFmt.Native");
72 |
--------------------------------------------------------------------------------
/test/sql_fmt/formatter_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.MixFormatterTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Mix.Tasks.Format
5 |
6 | test "formats matching files", context do
7 | in_tmp(context.test, fn ->
8 | write_formatter_config(plugins: [SqlFmt.MixFormatter])
9 | sql_fixture("query.sql")
10 |
11 | Format.run(["query.sql"])
12 | assert File.read!("query.sql") == "SELECT\n *\nFROM\n users"
13 | end)
14 | end
15 |
16 | test "with :indent option set", context do
17 | in_tmp(context.test, fn ->
18 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [indent: 4])
19 | sql_fixture("query.sql")
20 |
21 | Format.run(["query.sql"])
22 | assert File.read!("query.sql") == "SELECT\n *\nFROM\n users"
23 | end)
24 | end
25 |
26 | test "with :uppercase set to false", context do
27 | in_tmp(context.test, fn ->
28 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [uppercase: false])
29 | sql_fixture("query.sql")
30 |
31 | Format.run(["query.sql"])
32 | assert File.read!("query.sql") == "select\n *\nfrom\n users"
33 | end)
34 | end
35 |
36 | test "with multiple queries", context do
37 | in_tmp(context.test, fn ->
38 | write_formatter_config(plugins: [SqlFmt.MixFormatter])
39 | content = "select * from users; select * from groups;"
40 | sql_fixture("query.sql", content)
41 |
42 | Format.run(["query.sql"])
43 |
44 | assert File.read!("query.sql") ==
45 | """
46 | SELECT
47 | *
48 | FROM
49 | users;
50 | SELECT
51 | *
52 | FROM
53 | groups;\
54 | """
55 |
56 | # with :lines-between-queries set to a different value
57 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [lines_between_queries: 2])
58 | Format.run(["query.sql"])
59 |
60 | assert File.read!("query.sql") ==
61 | """
62 | SELECT
63 | *
64 | FROM
65 | users;
66 |
67 | SELECT
68 | *
69 | FROM
70 | groups;\
71 | """
72 | end)
73 | end
74 |
75 | test "with :ignore-case-convert set", context do
76 | in_tmp(context.test, fn ->
77 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [ignore_case_convert: ["select", "where"]])
78 | sql_fixture("query.sql", "select * from users where age > 18;")
79 |
80 | Format.run(["query.sql"])
81 | assert File.read!("query.sql") == "select\n *\nFROM\n users\nwhere\n age > 18;"
82 |
83 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [ignore_case_convert: ["select"]])
84 | Format.run(["query.sql"])
85 | assert File.read!("query.sql") == "select\n *\nFROM\n users\nWHERE\n age > 18;"
86 | end)
87 | end
88 |
89 | test "with sql path in formatter inputs", context do
90 | in_tmp(context.test, fn ->
91 | write_formatter_config(plugins: [SqlFmt.MixFormatter], inputs: ["sql/**/*.sql"])
92 | sql_fixture("sql/path_a/query.sql")
93 | sql_fixture("sql/path_a/query2.sql")
94 | sql_fixture("sql/path_b/query.sql")
95 | sql_fixture("queries/query.sql")
96 |
97 | Format.run(["sql/**/*.sql", "queries/query.sql"])
98 | assert File.read!("sql/path_a/query.sql") == "SELECT\n *\nFROM\n users"
99 | assert File.read!("sql/path_a/query2.sql") == "SELECT\n *\nFROM\n users"
100 | assert File.read!("sql/path_b/query.sql") == "SELECT\n *\nFROM\n users"
101 | assert File.read!("queries/query.sql") == "SELECT\n *\nFROM\n users"
102 | end)
103 | end
104 |
105 | test "with --check-formatted", context do
106 | in_tmp(context.test, fn ->
107 | write_formatter_config(plugins: [SqlFmt.MixFormatter], inputs: ["*.sql"])
108 | sql_fixture("query.sql")
109 |
110 | assert_raise Mix.Error, ~r"mix format failed due to --check-formatted", fn ->
111 | Format.run(["--check-formatted"])
112 | end
113 |
114 | # the file has not changed
115 | assert File.read!("query.sql") == "select * from users"
116 |
117 | # format it and run with --check-formatted again
118 | assert Format.run([]) == :ok
119 | assert Format.run(["--check-formatted"]) == :ok
120 |
121 | assert File.read!("query.sql") == "SELECT\n *\nFROM\n users"
122 | end)
123 | end
124 |
125 | test "with different extensions", context do
126 | in_tmp(context.test, fn ->
127 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [extensions: [".sqlite"]])
128 | sql_fixture("query.sql")
129 | sql_fixture("query.sqlite")
130 |
131 | assert Format.run(["query.sql", "query.sqlite"]) == :ok
132 | assert File.read!("query.sql") == "select * from users"
133 | assert File.read!("query.sqlite") == "SELECT\n *\nFROM\n users"
134 | end)
135 | end
136 |
137 | test "with single-line ~SQL sigil", context do
138 | in_tmp(context.test, fn ->
139 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [extensions: [".sqlite"]])
140 |
141 | elixir_fixture("query.exs", """
142 | import SqlFmt.Helpers
143 |
144 | query = ~SQL"select * from users"
145 | """)
146 |
147 | assert Format.run(["query.exs"]) == :ok
148 |
149 | assert File.read!("query.exs") == """
150 | import SqlFmt.Helpers
151 |
152 | query = ~SQL"SELECT * FROM users"
153 | """
154 | end)
155 | end
156 |
157 | test "with multi-line ~SQL sigil", context do
158 | in_tmp(context.test, fn ->
159 | write_formatter_config(plugins: [SqlFmt.MixFormatter], sql_fmt: [extensions: [".sqlite"]])
160 |
161 | elixir_fixture("query.exs", ~s'''
162 | import SqlFmt.Helpers
163 |
164 | query = ~SQL"""
165 | select * from users
166 | where age > 23
167 | """
168 | ''')
169 |
170 | assert Format.run(["query.exs"]) == :ok
171 |
172 | assert File.read!("query.exs") == ~s'''
173 | import SqlFmt.Helpers
174 |
175 | query = ~SQL"""
176 | SELECT
177 | *
178 | FROM
179 | users
180 | WHERE
181 | age > 23
182 | """
183 | '''
184 | end)
185 | end
186 |
187 | def in_tmp(which, function) do
188 | path = tmp_path(which)
189 | File.rm_rf!(path)
190 | File.mkdir_p!(path)
191 | File.cd!(path, function)
192 | end
193 |
194 | defp tmp_path(path), do: Path.join("../tmp", remove_colons(path)) |> Path.expand()
195 |
196 | defp remove_colons(term) do
197 | term
198 | |> to_string()
199 | |> String.replace(":", "")
200 | end
201 |
202 | defp write_formatter_config(config) when is_list(config), do: write_formatter_config(inspect(config))
203 | defp write_formatter_config(config) when is_binary(config), do: File.write!(".formatter.exs", config)
204 |
205 | defp sql_fixture(path, query \\ "select * from users") do
206 | File.mkdir_p!(Path.dirname(path))
207 | File.write!(path, query)
208 | end
209 |
210 | defp elixir_fixture(path, content) do
211 | File.write!(path, content)
212 | end
213 | end
214 |
--------------------------------------------------------------------------------
/test/sql_fmt/helpers_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqlFmt.HelpersTest do
2 | use ExUnit.Case
3 | import SqlFmt.Helpers
4 |
5 | test "sigil_SQL" do
6 | assert ~SQL"SELECT *" == "SELECT *"
7 |
8 | assert ~SQL"""
9 | SELECT *
10 | FROM users
11 | """ ==
12 | """
13 | SELECT *
14 | FROM users
15 | """
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/sql_fmt_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqlFmtTest do
2 | use ExUnit.Case
3 |
4 | describe "SqlFmt.format_query/2" do
5 | test "should format the query with default formatting options even if special characters are present" do
6 | query = """
7 | SELECT
8 | left <@ right,
9 | left << right,
10 | left >> right,
11 | left &< right,
12 | left &> right,
13 | left -|- right,
14 | @@ left,
15 | @-@ left,
16 | left <-> right,
17 | left <<| right,
18 | left |>> right,
19 | left &<| right,
20 | left |>& right,
21 | left <^ right,
22 | left >^ right,
23 | ?- left,
24 | left ?-| right,
25 | left ?|| right,
26 | left ~= right
27 | """
28 |
29 | assert {:ok, result} = query |> String.trim() |> SqlFmt.format_query()
30 |
31 | assert String.trim("""
32 | SELECT
33 | left <@ right,
34 | left << right,
35 | left >> right,
36 | left &< right,
37 | left &> right,
38 | left -|- right,
39 | @@ left,
40 | @-@ left,
41 | left <-> right,
42 | left <<| right,
43 | left |>> right,
44 | left &<| right,
45 | left |>& right,
46 | left <^ right,
47 | left >^ right,
48 | ?- left,
49 | left ?-| right,
50 | left ?|| right,
51 | left ~= right
52 | """) == result
53 | end
54 |
55 | test "should format the query with default formatting options" do
56 | assert {:ok, result} = SqlFmt.format_query("select * from hello_world;")
57 |
58 | assert String.trim("""
59 | SELECT
60 | *
61 | FROM
62 | hello_world;
63 | """) == result
64 | end
65 |
66 | test "should format the query with custom formatting options" do
67 | assert {:ok, result} = SqlFmt.format_query("select * from hello_world;", indent: 4, uppercase: false)
68 |
69 | assert String.trim("""
70 | select
71 | *
72 | from
73 | hello_world;
74 | """) == result
75 |
76 | assert {:ok, result} =
77 | SqlFmt.format_query("select * from hello_world;", indent: 4, ignore_case_convert: ["select"])
78 |
79 | assert String.trim("""
80 | select
81 | *
82 | FROM
83 | hello_world;
84 | """) == result
85 | end
86 | end
87 |
88 | describe "SqlFmt.format_query_with_params/2" do
89 | test "should format the query with default formatting options even if special characters are present" do
90 | query = """
91 | SELECT
92 | left <@ right,
93 | left << right,
94 | left >> right,
95 | left &< right,
96 | left &> right,
97 | left -|- right,
98 | @@ left,
99 | @-@ left,
100 | left <-> right,
101 | left <<| right,
102 | left |>> right,
103 | left &<| right,
104 | left |>& right,
105 | left <^ right,
106 | left >^ right,
107 | left ~= ?
108 | """
109 |
110 | assert {:ok, result} = query |> String.trim() |> SqlFmt.format_query_with_params(["right"])
111 |
112 | assert String.trim("""
113 | SELECT
114 | left <@ right,
115 | left << right,
116 | left >> right,
117 | left &< right,
118 | left &> right,
119 | left -|- right,
120 | @@ left,
121 | @-@ left,
122 | left <-> right,
123 | left <<| right,
124 | left |>> right,
125 | left &<| right,
126 | left |>& right,
127 | left <^ right,
128 | left >^ right,
129 | left ~= right
130 | """) == result
131 | end
132 |
133 | test "should format the query with default formatting options" do
134 | assert {:ok, result} = SqlFmt.format_query_with_params("select * from hello_world where thing = ?;", ["1"])
135 |
136 | assert String.trim("""
137 | SELECT
138 | *
139 | FROM
140 | hello_world
141 | WHERE
142 | thing = 1;
143 | """) == result
144 | end
145 |
146 | test "should format the query with custom formatting options" do
147 | assert {:ok, result} =
148 | SqlFmt.format_query_with_params("select * from hello_world where thing = ?;", ["1"],
149 | indent: 4,
150 | uppercase: false
151 | )
152 |
153 | assert String.trim("""
154 | select
155 | *
156 | from
157 | hello_world
158 | where
159 | thing = 1;
160 | """) == result
161 |
162 | assert {:ok, result} =
163 | SqlFmt.format_query_with_params("select * from hello_world where thing = ?;", ["1"],
164 | indent: 4,
165 | ignore_case_convert: ["select"]
166 | )
167 |
168 | assert String.trim("""
169 | select
170 | *
171 | FROM
172 | hello_world
173 | WHERE
174 | thing = 1;
175 | """) == result
176 | end
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------