Skip to content

Commit bef240e

Browse files
authored
Add compile time warnings (#22)
1 parent 627dd02 commit bef240e

25 files changed

+1366
-939
lines changed

.iex.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
defmodule SQL.Repo do
55
use Ecto.Repo, otp_app: :sql, adapter: Ecto.Adapters.Postgres
66
end
7+
Application.put_env(:sql, :driver, Postgrex)
8+
Application.put_env(:sql, :username, "postgres")
9+
Application.put_env(:sql, :password, "postgres")
10+
Application.put_env(:sql, :hostname, "localhost")
11+
Application.put_env(:sql, :database, "sql_test#{System.get_env("MIX_TEST_PARTITION")}")
12+
Application.put_env(:sql, :adapter, SQL.Adapters.Postgres)
713
Application.put_env(:sql, :ecto_repos, [SQL.Repo])
814
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)
915
Mix.Tasks.Ecto.Create.run(["-r", "SQL.Repo"])

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
## v0.3.0 (2025-08-01)
99

1010
### Enhancement
11-
- Improve SQL generation with 57-344x compared to Ecto [#12](https://github.com/elixir-dbvisor/sql/pull/12).
11+
- Improve SQL generation with over 100x compared to Ecto [#12](https://github.com/elixir-dbvisor/sql/pull/12), [#19](https://github.com/elixir-dbvisor/sql/pull/19).
1212
- Fix bug for complex CTE [#15](https://github.com/elixir-dbvisor/sql/pull/15). Thanks to @kafaichoi
1313
- Support for PostgresSQL GiST operators [#18](https://github.com/elixir-dbvisor/sql/pull/18). Thanks to @ibarchenkov
1414
- `float` and `integer` nodes have now become `numeric` with metadata to distinguish `sign`, `whole` and `fractional` [#19](https://github.com/elixir-dbvisor/sql/pull/19).
1515
- `keyword` nodes are now `ident` with metadata distinguish if it's a `keyword` [#19](https://github.com/elixir-dbvisor/sql/pull/19).
1616
- `SQL.Lexer.lex/4` now returns `{:ok, context, tokens}` [#19](https://github.com/elixir-dbvisor/sql/pull/19).
1717
- `SQL.Parser.parse/1` has become `SQL.Parser.parse/2` and takes `tokens` and `context` from `SQL.Lexer.lex/4` and returns `{:ok, context, tokens}` or raises an error [#19](https://github.com/elixir-dbvisor/sql/pull/19).
18+
- Support for compile time warnings on missing relations in a query. [#22](https://github.com/elixir-dbvisor/sql/pull/22)
19+
- `mix sql.get` creates a lock file which are used to generate warnings at compile time. [#22](https://github.com/elixir-dbvisor/sql/pull/22)
20+
- Support SQL formatting. [#22](https://github.com/elixir-dbvisor/sql/pull/22)
21+
22+
### Deprecation
23+
- token_to_string/2 is deprecated in favor of to_iodata/3 [#22](https://github.com/elixir-dbvisor/sql/pull/22).
1824

1925

2026
## v0.2.0 (2025-05-04)

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,30 @@ iex(1)> email = "john@example.com"
2020
"john@example.com"
2121
iex(2)> ~SQL[from users] |> ~SQL[where email = {{email}}] |> ~SQL"select id, email"
2222
~SQL"""
23-
where email = {{email}} from users select id, email
23+
select
24+
id,
25+
email
26+
from
27+
users
28+
where
29+
email = {{email}}
2430
"""
2531
iex(3)> sql = ~SQL[from users where email = {{email}} select id, email]
2632
~SQL"""
27-
from users where email = {{email}} select id, email
33+
select
34+
id,
35+
email
36+
from
37+
users
38+
where
39+
email = {{email}}
2840
"""
2941
iex(4)> to_sql(sql)
3042
{"select id, email from users where email = ?", ["john@example.com"]}
3143
iex(5)> to_string(sql)
3244
"select id, email from users where email = ?"
3345
iex(6)> inspect(sql)
34-
"~SQL\"\"\"\nfrom users where email = {{email}} select id, email\n\"\"\""
46+
"~SQL\"\"\"\nselect\n id, \n email\nfrom\n users\nwhere\n email = {{email}}\n\"\"\""
3547
```
3648

3749
### Leverage the Enumerable protocol in your repository

bench.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sql = ~SQL[with recursive temp (n, fact) as (select 0, 1 union all select n+1, (
1111
query = "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])
1212
result = Tuple.to_list(SQL.Lexer.lex("with recursive temp (n, fact) as (select 0, 1 union all select n+1, (n+1)*fact from temp where n < 9)"))
1313
tokens = Enum.at(result, -1)
14-
context = Enum.at(result, 1)
14+
context = Map.put(Enum.at(result, 1), :sql_lock, nil)
1515
Benchee.run(
1616
%{
1717
"comptime to_string" => fn _ -> to_string(sql) end,

lib/adapters/ansi.ex

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ defmodule SQL.Adapters.ANSI do
7575
"#{mod.token_to_string(value)};"
7676
end
7777
def token_to_string({:comma, _, value}, mod) do
78-
", #{mod.token_to_string(value)}"
78+
"#{mod.token_to_string(value)},"
7979
end
8080
def token_to_string({:dot, _, [left, right]}, mod) do
8181
"#{mod.token_to_string(left)}.#{mod.token_to_string(right)}"
@@ -108,9 +108,140 @@ defmodule SQL.Adapters.ANSI do
108108
values
109109
|> Enum.reduce([], fn
110110
[], acc -> acc
111-
token, [] -> [mod.token_to_string(token)]
112-
{:comma, _, _} = token, acc -> [acc,mod.token_to_string(token)]
111+
{:comma, _, _} = token, acc -> [acc, mod.token_to_string(token), " "]
112+
token, [] -> mod.token_to_string(token)
113+
token, [_, _, " "] = acc -> [acc, mod.token_to_string(token)]
114+
token, [_, " "] = acc -> [acc, mod.token_to_string(token)]
113115
token, acc -> [acc, " ", mod.token_to_string(token)]
114116
end)
115117
end
118+
119+
@doc false
120+
def to_iodata({tag, _, values}, context, indent) when tag in ~w[inner outer left right full natural cross]a do
121+
[context.module.to_iodata(tag, context, indent),context.module.to_iodata(values, context, indent, [?\s])]
122+
end
123+
def to_iodata({tag, _, values}, context, indent) when tag in ~w[join]a do
124+
v = Enum.reduce(values, [], fn token, acc -> [acc, ?\s, context.module.to_iodata(token, context, indent)] end)
125+
126+
[indention(indent), context.module.to_iodata(tag, context, indent) | [v, ?\n]]
127+
end
128+
def to_iodata({tag, _, values}, context, indent) when tag in ~w[select from join where group having window order limit offset fetch]a do
129+
v = Enum.reduce(values, [?\n], fn token, acc -> [acc, indention(indent+1), context.module.to_iodata(token, context, indent+1), ?\n] end)
130+
131+
[indention(indent), context.module.to_iodata(tag, context, indent) | v]
132+
end
133+
def to_iodata({:as, [], [left, right]}, context, indent) do
134+
[context.module.to_iodata(left, context, indent),?\s|context.module.to_iodata(right, context, indent)]
135+
end
136+
def to_iodata({tag, _, [left]}, context, indent) when tag in ~w[asc desc isnull notnull]a do
137+
[context.module.to_iodata(left, context, indent),?\s|context.module.to_iodata(tag, context, indent)]
138+
end
139+
def to_iodata({:fun, _, [left, right]}, context, indent) do
140+
[context.module.to_iodata(left, context, indent)|context.module.to_iodata(right, context, indent)]
141+
end
142+
def to_iodata({tag, [{:type, :operator}|_], [left, {:paren, _, _} = right]}, context, indent) do
143+
[context.module.to_iodata(left, context, indent),?\s,context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(right, context, indent)]
144+
end
145+
def to_iodata({tag, [{:type, :operator}|_], [left, right]}, context, indent) do
146+
[context.module.to_iodata(left, context, indent),?\s,context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(right, context, indent)]
147+
end
148+
def to_iodata({:ident, [{:type, :non_reserved},{:tag, tag}|_], [{:paren, _, _} = value]}, context, indent) do
149+
[context.module.to_iodata(tag, context, indent)|context.module.to_iodata(value, context, indent)]
150+
end
151+
def to_iodata({:ident, [{:type, :non_reserved}, {:tag, tag}|_], [{:numeric, _, _} = value]}, context, indent) do
152+
[context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(value, context, indent)]
153+
end
154+
def to_iodata({:ident, [{:type, :non_reserved}, {:tag, tag}|_], _}, context, indent) do
155+
context.module.to_iodata(tag, context, indent)
156+
end
157+
def to_iodata({tag, [{:type, :reserved}|_], [{:paren, _, _} = value]}, context, indent) when tag not in ~w[on in select]a do
158+
[context.module.to_iodata(tag, context, indent)|context.module.to_iodata(value, context, indent)]
159+
end
160+
def to_iodata({tag, [{:type, :reserved}|_], []}, context, indent) do
161+
[?\s, context.module.to_iodata(tag, context, indent)]
162+
end
163+
def to_iodata({tag, _, [left, {:all = t, _, right}]}, context, indent) when tag in ~w[union except intersect]a do
164+
[context.module.to_iodata(left, context, indent), indention(indent), context.module.to_iodata(tag, context, indent),?\s,context.module.to_iodata(t, context, indent),?\n|context.module.to_iodata(right, context, indent)]
165+
end
166+
def to_iodata({:between = tag, _, [{:not = t, _, right}, left]}, context, indent) do
167+
[context.module.to_iodata(right, context, indent),?\s,context.module.to_iodata(t, context, indent),?\s,context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(left, context, indent)]
168+
end
169+
def to_iodata({:binding, _, [idx]}, %{format: true, binding: binding}, _indent) do
170+
[?{,?{,Macro.to_string(Enum.at(binding, idx-1))|[?},?}]]
171+
end
172+
def to_iodata({:binding, _, _}, _context, _indent) do
173+
[??]
174+
end
175+
def to_iodata({:comment, _, value}, _context, _indent) do
176+
[?-,?-|value]
177+
end
178+
def to_iodata({:comments, _, value}, _context, _indent) do
179+
[?\\,?*,value|[?*, ?\\]]
180+
end
181+
def to_iodata({:double_quote, _, value}=node, context, _indent) do
182+
case node in context.errors do
183+
true -> [[?",:red,value|[:reset, ?"]]]
184+
false -> value
185+
end
186+
end
187+
def to_iodata({:quote, _, value}, _context, _indent) do
188+
[?',value|[?']]
189+
end
190+
def to_iodata({:paren, _, [{_,[{:type, :reserved}|_],_}|_] = value}, context, indent) do
191+
[?(,?\n, context.module.to_iodata(value, context, indent+1)|?)]
192+
end
193+
def to_iodata({:paren, _, value}, context, indent) do
194+
[?(,context.module.to_iodata(value, context, indent)|[?)]]
195+
end
196+
def to_iodata({:bracket, _, value}, context, indent) do
197+
[?[,context.module.to_iodata(value, context, indent)|[?]]]
198+
end
199+
def to_iodata({:colon, _, value}, context, indent) do
200+
[context.module.to_iodata(value, context, indent)|[?;,?\n]]
201+
end
202+
def to_iodata({:comma, _, value}, context, indent) do
203+
[context.module.to_iodata(value, context, indent), ?,, ?\s]
204+
end
205+
def to_iodata({:dot, _, [left, right]}, context, indent) do
206+
[context.module.to_iodata(left, context, indent),?\.|context.module.to_iodata(right, context, indent)]
207+
end
208+
def to_iodata({tag, _, value} = node, context, _indent) when tag in ~w[ident numeric]a do
209+
case node in context.errors do
210+
true -> [:red, value, :reset]
211+
false -> value
212+
end
213+
end
214+
def to_iodata(value, _context, _indent) when is_atom(value) do
215+
~c"#{value}"
216+
end
217+
def to_iodata(value, _context, _indent) when is_binary(value) do
218+
[?',value|[?']]
219+
end
220+
def to_iodata(value, _context, _indent) when is_integer(value) do
221+
[value]
222+
end
223+
def to_iodata(value, _context, _indent) when is_struct(value) do
224+
to_string(value)
225+
end
226+
def to_iodata({tag, _, [left, right]}, context, indent) when tag in ~w[like ilike union except intersect between and or is not in cursor for to]a do
227+
[context.module.to_iodata(left, context, indent),?\s,context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(right, context, indent)]
228+
end
229+
def to_iodata({tag, [{:type, :reserved}|_], values}, context, indent) do
230+
[context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(values, context, indent)]
231+
end
232+
def to_iodata({tag, [{:type, :non_reserved}|_], values}, context, indent) when tag != :ident do
233+
[context.module.to_iodata(tag, context, indent),?\s|context.module.to_iodata(values, context, indent)]
234+
end
235+
def to_iodata({tag, _, []}, context, indent) do
236+
[indention(indent), context.module.to_iodata(tag, context, indent)]
237+
end
238+
def to_iodata([[{_,_,_}|_]|_]=tokens, context, indent) do
239+
to_iodata(tokens, context, indent, [])
240+
end
241+
def to_iodata([{_,_,_}|_]=tokens, context, indent) do
242+
to_iodata(tokens, context, indent, [])
243+
end
244+
def to_iodata([]=tokens, _context, _indent) do
245+
tokens
246+
end
116247
end

lib/adapters/mysql.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ defmodule SQL.Adapters.MySQL do
1212
@doc false
1313
def token_to_string(value, mod \\ __MODULE__)
1414
def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod)
15+
16+
@doc false
17+
def to_iodata(token, context, indent), do: SQL.Adapters.ANSI.to_iodata(token, context, indent)
1518
end

lib/adapters/postgres.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,25 @@ defmodule SQL.Adapters.Postgres do
1818
"#{mod.token_to_string(left)} #{mod.token_to_string(tag)} #{mod.token_to_string(right)}"
1919
end
2020
def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod)
21+
22+
@doc false
23+
def to_iodata({:not, _, [left, {:in, _, [{:binding, _, [idx]}]}]}, %{format: true, binding: binding} = context, indent) do
24+
[context.module.to_iodata(left, context, indent), ?\s, ?{,?{,Macro.to_string(Enum.at(binding, idx-1)),?},?}]
25+
end
26+
def to_iodata({:not, _, [left, {:in, _, [{:binding, _, _} = right]}]}, context, indent) do
27+
[context.module.to_iodata(left, context, indent), ?!, ?=, ?A,?N,?Y,?(, context.module.to_iodata(right, context, indent), ?)]
28+
end
29+
def to_iodata({:in, _, [left, {:binding, _, [idx]}]}, %{format: true, binding: binding} = context, indent) do
30+
[context.module.to_iodata(left, context, indent), ?\s, ?{,?{,Macro.to_string(Enum.at(binding, idx-1)),?},?}]
31+
end
32+
def to_iodata({:in, _, [left, {:binding, _, _} = right]}, context, indent) do
33+
[context.module.to_iodata(left, context, indent), ?=, ?A,?N,?Y,?(, context.module.to_iodata(right, context, indent), ?)]
34+
end
35+
def to_iodata({:binding, _, [idx]}, %{format: true, binding: binding}, _indent) do
36+
[?{,?{,Macro.to_string(Enum.at(binding, idx-1))|[?},?}]]
37+
end
38+
def to_iodata({:binding, _, [idx]}, _context, _indent) do
39+
~c"$#{idx}"
40+
end
41+
def to_iodata(token, context, indent), do: SQL.Adapters.ANSI.to_iodata(token, context, indent)
2142
end

lib/adapters/tds.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ defmodule SQL.Adapters.TDS do
1313
def token_to_string(value, mod \\ __MODULE__)
1414
def token_to_string({:binding, _, [idx]}, _mod) when is_integer(idx), do: "@#{idx}"
1515
def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod)
16+
17+
@doc false
18+
def to_iodata({:binding, _, [idx]}, %{format: true, binding: binding}, _indent) do
19+
[?{,?{,Macro.to_string(Enum.at(binding, idx-1))|[?},?}]]
20+
end
21+
def to_iodata({:binding, _, [idx]}, _context, _indent) when is_integer(idx), do: ~c"@#{idx}"
22+
def to_iodata(token, context, indent), do: SQL.Adapters.ANSI.to_iodata(token, context, indent)
1623
end

lib/bnf.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ defmodule SQL.BNF do
4343
cond do
4444
String.ends_with?(r, "word>") == true ->
4545
e = if is_map_key(opts, r), do: e ++ opts[r], else: e
46-
{[{r, (for v <- e, v != "|", do: {atom(v), match(v), guard(v)})} | keywords], operators, letters, digits, terminals}
46+
{[{r, (for v <- e, v not in ["|", "AS"], do: {atom(v), match(v), guard(v)})} | keywords], operators, letters, digits, terminals}
4747
String.ends_with?(r, "letter>") == true -> {keywords, operators, [{r, Enum.reject(e, &(&1 == "|"))}|letters], digits, terminals}
4848
String.ends_with?(r, "digit>") == true -> {keywords, operators, letters, [{r, Enum.reject(e, &(&1 == "|"))}|digits], terminals}
4949
String.ends_with?(r, "operator>") == true -> {keywords, [rule | operators], letters, digits, terminals}

lib/formatter.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ defmodule SQL.MixFormatter do
99
def features(opts), do: [sigils: [:SQL], extensions: get_in(opts, [:sql, :extensions])]
1010

1111
@impl Mix.Tasks.Format
12-
def format(source, _opts), do: "#{SQL.parse(source)}"
12+
def format(source, _opts), do: SQL.parse(source)
1313
end

0 commit comments

Comments
 (0)