Skip to content

Commit fc19401

Browse files
authored
lex at compile time (#12)
1 parent 1419d31 commit fc19401

File tree

2 files changed

+84
-58
lines changed

2 files changed

+84
-58
lines changed

lib/sql.ex

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ defmodule SQL do
88
|> Enum.fetch!(1)
99
@moduledoc since: "0.1.0"
1010

11+
@adapters [SQL.Adapters.ANSI, SQL.Adapters.MySQL, SQL.Adapters.Postgres, SQL.Adapters.TDS]
12+
1113
defmacro __using__(opts) do
1214
quote bind_quoted: [opts: opts] do
1315
@doc false
1416
@behaviour SQL
1517
import SQL
18+
@sql_adapter opts[:adapter]
1619
def sql_config, do: unquote(opts)
1720
def token_to_sql(token), do: token_to_sql(token)
1821
defoverridable token_to_sql: 1
@@ -27,7 +30,15 @@ defmodule SQL do
2730
@doc deprecated: "Use SQL.Token.token_to_string/1 instead"
2831
@callback token_to_sql(token :: {atom, keyword, list}) :: String.t()
2932

30-
defstruct [:tokens, :params, :module, :id]
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
3142

3243
@doc """
3344
Returns a parameterized SQL.
@@ -38,7 +49,7 @@ defmodule SQL do
3849
{"select id, email from users where email = ?", ["[email protected]"]}
3950
"""
4051
@doc since: "0.1.0"
41-
def to_sql(%{params: params, id: id, module: module}), do: {:persistent_term.get({module, id, :plan}), params}
52+
def to_sql(sql), do: {sql.string, sql.params}
4253

4354
@doc """
4455
Handles the sigil `~SQL` for SQL.
@@ -59,11 +70,11 @@ defmodule SQL do
5970
@doc false
6071
@doc since: "0.1.0"
6172
def parse(binary) do
62-
{:ok, _opts, _, _, _, _, tokens} = SQL.Lexer.lex(binary, {1, 0, nil}, 0, [format: true])
73+
{:ok, _opts, _, _, _, _, tokens} = SQL.Lexer.lex(binary, __ENV__.file, 0, [format: true])
6374
tokens
6475
|> SQL.Parser.parse()
6576
|> to_query()
66-
|> to_string(SQL.String)
77+
|> to_string(SQL.Adapters.ANSI)
6778
end
6879

6980
@doc false
@@ -85,16 +96,19 @@ defmodule SQL do
8596
token
8697
end
8798

88-
defimpl Inspect, for: SQL do
89-
def inspect(sql, _opts), do: Inspect.Algebra.concat(["~SQL\"\"\"\n", :persistent_term.get({sql.id, :inspect}), "\n\"\"\""])
90-
end
91-
92-
defimpl String.Chars, for: SQL do
93-
def to_string(%{id: id, module: module}), do: :persistent_term.get({module, id, :plan})
94-
def to_string(%{tokens: tokens, module: module}), do: SQL.to_string(tokens, module)
95-
end
96-
9799
@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
98112
def to_string(tokens, module) do
99113
fun = cond do
100114
Kernel.function_exported?(module, :sql_config, 0) -> &module.sql_config()[:adapter].token_to_string(&1)
@@ -115,34 +129,46 @@ defmodule SQL do
115129

116130
@doc false
117131
def build(left, {:<<>>, _, _} = right, _modifiers, env) do
118-
data = build(left, right)
119-
quote bind_quoted: [module: env.module, left: Macro.unpipe(left), right: right, file: env.file, id: id(data), data: data] do
120-
plan_inspect(data, id)
121-
{t, p} = Enum.reduce(left, {[], []}, fn
122-
{[], 0}, acc -> acc
123-
{v, 0}, {t, p} ->
124-
{t ++ v.tokens, p ++ v.params}
125-
end)
126-
{tokens, params} = tokens(right, file, length(p), id)
127-
tokens = t ++ tokens
128-
plan(tokens, id, module)
129-
struct(SQL, params: cast_params(params, p, binding()), tokens: tokens, id: id, module: module)
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
130157
end
131158
end
132159

133160
@doc false
134161
def build(left, {:<<>>, _, right}) do
135162
left
136163
|> Macro.unpipe()
137-
|> Enum.reduce({:iodata, right}, fn
164+
|> Enum.reduce({:static, right}, fn
138165
{[], 0}, acc -> acc
139166
{{:sigil_SQL, _meta, [{:<<>>, _, value}, []]}, 0}, {type, acc} -> {type, [value, ?\s, acc]}
140-
{{_, _, _} = var, 0}, {_, acc} ->
141-
{:dynamic, [var, ?\s, acc]}
167+
{{_, _, _} = var, 0}, {_, acc} -> {:dynamic, [var, ?\s, acc]}
142168
end)
143169
|> case do
144-
{:iodata, data} -> IO.iodata_to_binary(data)
145-
{:dynamic, data} -> data
170+
{:static, data} -> {:static, IO.iodata_to_binary(data)}
171+
{:dynamic, data} -> {:dynamic, data}
146172
end
147173
end
148174

@@ -181,31 +207,31 @@ defmodule SQL do
181207
@doc false
182208
def plan(tokens, id, module) do
183209
key = {module, id, :plan}
184-
if :persistent_term.get(key, nil) do
185-
id
210+
if string = :persistent_term.get(key, nil) do
211+
string
186212
else
187-
:persistent_term.put(key, to_string(SQL.to_query(SQL.Parser.parse(tokens)), module))
188-
id
213+
string = to_string(SQL.to_query(SQL.Parser.parse(tokens)), module)
214+
:persistent_term.put(key, string)
215+
string
189216
end
190217
end
191218

192219
@doc false
193220
def plan_inspect(data, id) do
194221
key = {id, :inspect}
195-
if !:persistent_term.get(key, nil) do
196-
data = case data do
197-
data when is_list(data) ->
198-
data
199-
|> Enum.map(fn
200-
ast when is_struct(ast) -> :persistent_term.get({ast.id, :inspect}, nil)
201-
x -> x
202-
end)
203-
|> IO.iodata_to_binary()
204-
205-
data -> data
206-
end
207-
208-
:persistent_term.put(key, data)
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
209235
end
210236
end
211237
end

test/sql_test.exs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,46 +59,46 @@ defmodule SQLTest do
5959
describe "error" do
6060
test "missing )" do
6161
assert_raise TokenMissingError, ~r"token missing on", fn ->
62-
~SQL[select id in (1, 2]
62+
SQL.parse("select id in (1, 2")
6363
end
6464
assert_raise TokenMissingError, ~r"token missing on", fn ->
65-
~SQL[select id from users join orgs on (id = id]
65+
SQL.parse("select id from users join orgs on (id = id")
6666
end
6767
end
6868

6969
test "missing ]" do
7070
assert_raise TokenMissingError, ~r"token missing on", fn ->
71-
~SQL{select id in ([1)}
71+
SQL.parse("select id in ([1)")
7272
end
7373
assert_raise TokenMissingError, ~r"token missing on", fn ->
74-
~SQL{select id from users join orgs on ([1)}
74+
SQL.parse("select id from users join orgs on ([1)")
7575
end
7676
end
7777

7878
test "missing }" do
7979
assert_raise TokenMissingError, ~r"token missing on", fn ->
80-
~SQL[select id in {{1]
80+
SQL.parse("select id in {{1")
8181
end
8282
assert_raise TokenMissingError, ~r"token missing on", fn ->
83-
~SQL[select id from users join orgs on {{id]
83+
SQL.parse("select id from users join orgs on {{id")
8484
end
8585
end
8686

8787
test "missing \"" do
8888
assert_raise TokenMissingError, ~r"token missing on", fn ->
89-
~SQL[select id in "1]
89+
SQL.parse("select id in \"1")
9090
end
9191
assert_raise TokenMissingError, ~r"token missing on", fn ->
92-
~SQL[select id from users join orgs on "id]
92+
SQL.parse("select id from users join orgs on \"id")
9393
end
9494
end
9595

9696
test "missing \'" do
9797
assert_raise TokenMissingError, ~r"token missing on", fn ->
98-
~SQL[select id in '1]
98+
SQL.parse("select id in '1")
9999
end
100100
assert_raise TokenMissingError, ~r"token missing on", fn ->
101-
~SQL[select id from users join orgs on 'id]
101+
SQL.parse("select id from users join orgs on 'id")
102102
end
103103
end
104104
end

0 commit comments

Comments
 (0)