Skip to content

Commit 41565d7

Browse files
authored
ensure inspect follows the standard representation (#4)
# Conflicts: # CHANGELOG.md # lib/sql.ex # Conflicts: # CHANGELOG.md # lib/sql.ex
1 parent 9eb7c2a commit 41565d7

File tree

6 files changed

+123
-72
lines changed

6 files changed

+123
-72
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- PostgreSQL adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5).
1717
- TDS adapter [#5](https://github.com/elixir-dbvisor/sql/pull/5).
1818
- Improve SQL generation with 4-600x compared to Ecto [#7](https://github.com/elixir-dbvisor/sql/pull/7).
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)
1920

2021
### Deprecation
2122
- token_to_sql/2 is deprecated in favor of SQL.Token behaviour token_to_string/2 [#5](https://github.com/elixir-dbvisor/sql/pull/5).

README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@ Brings an extensible SQL parser and sigil to Elixir, confidently write SQL with
1818
```elixir
1919
iex(1)> email = "[email protected]"
2020
21-
iex(2)> select = ~SQL"select id, email"
22-
"select id, email"
23-
iex(3)> ~SQL[from users] |> ~SQL[where email = #{email}] |> select
24-
"select id, email from users where email = \"[email protected]\""
25-
iex(4)> sql = ~SQL[from users where email = #{email} select id, email]
26-
"select id, email from users where email = \"[email protected]\""
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(4)> sql = ~SQL[from users where email = {{email}} select id, email]
26+
~SQL"""
27+
from users where email = {{email}} select id, email
28+
"""
2729
iex(5)> to_sql(sql)
28-
{"select id, email from users where email = $0", ["[email protected]"]}
30+
{"select id, email from users where email = ?", ["[email protected]"]}
2931
iex(6)> to_string(sql)
30-
"select id, email from users where email = $0"
32+
"select id, email from users where email = ?"
3133
iex(7)> inspect(sql)
32-
"select id, email from users where email = \"[email protected]\""
34+
"~SQL\"\"\"\nfrom users where email = {{email}} select id, email\n\"\"\""
3335
```
3436

3537
### Leverage the Enumerable protocol in your repository

lib/lexer.ex

+5-5
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ defmodule SQL.Lexer do
2424
def expected_delimiter(:backtick), do: :"`"
2525
def expected_delimiter(type) when type in ~w[var code braces]a, do: :"}"
2626

27-
def lex(binary, meta, params \\ 0, opts \\ [metadata: true]) do
28-
case lex(binary, binary, [{:binding, []}, {:params, params}, {:meta, meta} | opts], 0, 0, nil, [], [], 0) do
27+
def lex(binary, file, params \\ 0, opts \\ [metadata: true]) do
28+
case lex(binary, binary, [{:binding, []}, {:params, params}, {:file, file} | opts], 0, 0, nil, [], [], 0) do
2929
{"", _binary, opts, line, column, nil = type, data, acc, _n} ->
3030
{:ok, opts, line, column, type, data, acc}
3131

3232
{"", 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 ->
33-
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)
33+
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)
3434

3535
{"", _binary, opts, line, column, type, data, acc, _n} ->
3636
{:ok, opts, line, column, type, data, insert_node(node(ident(type, data), line, column, data, opts), acc)}
@@ -51,7 +51,7 @@ defmodule SQL.Lexer do
5151
def lex(<<b, rest::binary>>, binary, opts, line, column, :comments, data, acc, n) do
5252
lex(rest, binary, opts, line, column+1, :comments, [data | [b]], acc, n)
5353
end
54-
def lex(<<?{, ?{, rest::binary>>, binary, opts, line, column, _type, data, acc, n) do
54+
def lex(<<?{, ?{, rest::binary>>, binary, opts, line, column, nil, data, acc, n) do
5555
lex(rest, binary, opts, line, column+2, :var, data, acc, n)
5656
end
5757
def lex(<<?}, ?}, rest::binary>>, 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)
@@ -266,7 +266,7 @@ defmodule SQL.Lexer do
266266
def type(_b, _type), do: :ident
267267

268268
def meta(_line, _column, [_,_,_,{_,false}|_]), do: []
269-
def meta(line, column, [_, _, {_, {_, _, file}} |_]), do: [line: line, column: column, file: file]
269+
def meta(line, column, [_, _, {_, file} |_]), do: [line: line, column: column, file: file]
270270

271271
def node(:binding = tag, line, column, [idx], [{:binding, false}, {:params, params}|_] = opts) do
272272
{tag, meta(line, column, opts), Enum.at(params, idx)}

lib/mix/tasks/sql.gen.parser.ex

+5-5
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,13 @@ defmodule Mix.Tasks.Sql.Gen.Parser do
302302
def expected_delimiter(:backtick), do: :"`"
303303
def expected_delimiter(type) when type in ~w[var code braces]a, do: :"}"
304304
305-
def lex(binary, meta, params \\\\ 0, opts \\\\ [metadata: true]) do
306-
case lex(binary, binary, [{:binding, []}, {:params, params}, {:meta, meta} | opts], 0, 0, nil, [], [], 0) do
305+
def lex(binary, file, params \\\\ 0, opts \\\\ [metadata: true]) do
306+
case lex(binary, binary, [{:binding, []}, {:params, params}, {:file, file} | opts], 0, 0, nil, [], [], 0) do
307307
{"", _binary, opts, line, column, nil = type, data, acc, _n} ->
308308
{:ok, opts, line, column, type, data, acc}
309309
310310
{"", 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 ->
311-
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)
311+
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)
312312
313313
{"", _binary, opts, line, column, type, data, acc, _n} ->
314314
{:ok, opts, line, column, type, data, insert_node(node(ident(type, data), line, column, data, opts), acc)}
@@ -329,7 +329,7 @@ defmodule Mix.Tasks.Sql.Gen.Parser do
329329
def lex(<<b, rest::binary>>, binary, opts, line, column, :comments, data, acc, n) do
330330
lex(rest, binary, opts, line, column+1, :comments, [data | [b]], acc, n)
331331
end
332-
def lex(<<?{, ?{, rest::binary>>, binary, opts, line, column, _type, data, acc, n) do
332+
def lex(<<?{, ?{, rest::binary>>, binary, opts, line, column, nil, data, acc, n) do
333333
lex(rest, binary, opts, line, column+2, :var, data, acc, n)
334334
end
335335
def lex(<<?}, ?}, rest::binary>>, 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)
@@ -544,7 +544,7 @@ defmodule Mix.Tasks.Sql.Gen.Parser do
544544
def type(_b, _type), do: :ident
545545
546546
def meta(_line, _column, [_,_,_,{_,false}|_]), do: []
547-
def meta(line, column, [_, _, {_, {_, _, file}} |_]), do: [line: line, column: column, file: file]
547+
def meta(line, column, [_, _, {_, file} |_]), do: [line: line, column: column, file: file]
548548
549549
def node(:binding = tag, line, column, [idx], [{:binding, false}, {:params, params}|_] = opts) do
550550
{tag, meta(line, column, opts), Enum.at(params, idx)}

lib/sql.ex

+100-52
Original file line numberDiff line numberDiff line change
@@ -38,48 +38,7 @@ defmodule SQL do
3838
{"select id, email from users where email = $0", ["[email protected]"]}
3939
"""
4040
@doc since: "0.1.0"
41-
def to_sql(sql), do: {:persistent_term.get(sql.id), sql.params}
42-
43-
@doc false
44-
def build(right, {:<<>>, meta, _} = left, _modifiers, env) do
45-
quote bind_quoted: [right: Macro.unpipe(right), left: left, meta: Macro.escape({meta[:line], meta[:column] || 0, env.file}), e: Macro.escape(env)] do
46-
{t, p} = Enum.reduce(right, {[], []}, fn
47-
{[], 0}, acc -> acc
48-
{v, 0}, {tokens, params} -> {tokens ++ v.tokens, params ++ v.params}
49-
end)
50-
binding = binding()
51-
id = {__MODULE__, :binary.decode_unsigned(left), meta}
52-
{tokens, params} = tokens(left, meta, length(p), id)
53-
tokens = t ++ tokens
54-
params = Enum.reduce(params, p, fn
55-
{:var, var}, acc -> acc ++ [binding[String.to_atom(var)]]
56-
{:code, code}, acc -> acc ++ [elem(Code.eval_string(code, binding), 0)]
57-
end)
58-
struct(SQL, params: params, tokens: tokens, id: plan(id, tokens), module: __MODULE__)
59-
end
60-
end
61-
62-
def tokens(left, meta, p, id) do
63-
if result = :persistent_term.get(id, nil) do
64-
result
65-
else
66-
{:ok, opts, _, _, _, _, tokens} = SQL.Lexer.lex(left, meta, p)
67-
result = {tokens, opts[:binding]}
68-
:persistent_term.put(id, result)
69-
result
70-
end
71-
end
72-
73-
def plan(id, tokens) do
74-
if uid = :persistent_term.get(tokens, nil) do
75-
uid
76-
else
77-
uid = System.unique_integer([:positive])
78-
:persistent_term.put(tokens, uid)
79-
:persistent_term.put(uid, to_string(SQL.to_query(SQL.Parser.parse(tokens)), elem(id, 0)))
80-
uid
81-
end
82-
end
41+
def to_sql(%{params: params, id: id, module: module}), do: {:persistent_term.get({module, id, :plan}), params}
8342

8443
@doc """
8544
Handles the sigil `~SQL` for SQL.
@@ -124,7 +83,16 @@ defmodule SQL do
12483
token
12584
end
12685

86+
defimpl Inspect, for: SQL do
87+
def inspect(sql, _opts), do: Inspect.Algebra.concat(["~SQL\"\"\"\n", :persistent_term.get({sql.id, :inspect}), "\n\"\"\""])
88+
end
89+
90+
defimpl String.Chars, for: SQL do
91+
def to_string(%{id: id, module: module}), do: :persistent_term.get({module, id, :plan})
92+
def to_string(%{tokens: tokens, module: module}), do: SQL.to_string(tokens, module)
93+
end
12794

95+
@doc false
12896
def to_string(tokens, module) do
12997
fun = cond do
13098
Kernel.function_exported?(module, :sql_config, 0) -> &module.sql_config()[:adapter].token_to_string(&1)
@@ -143,19 +111,99 @@ defmodule SQL do
143111
|> IO.iodata_to_binary()
144112
end
145113

114+
@doc false
115+
def build(left, {:<<>>, _, _} = right, _modifiers, env) do
116+
data = build(left, right)
117+
quote bind_quoted: [module: env.module, left: Macro.unpipe(left), right: right, file: env.file, id: id(data), data: data] do
118+
plan_inspect(data, id)
119+
{t, p} = Enum.reduce(left, {[], []}, fn
120+
{[], 0}, acc -> acc
121+
{v, 0}, {t, p} ->
122+
{t ++ v.tokens, p ++ v.params}
123+
end)
124+
{tokens, params} = tokens(right, file, length(p), id)
125+
tokens = t ++ tokens
126+
plan(tokens, id, module)
127+
struct(SQL, params: cast_params(params, p, binding()), tokens: tokens, id: id, module: module)
128+
end
129+
end
130+
131+
@doc false
132+
def build(left, {:<<>>, _, right}) do
133+
left
134+
|> Macro.unpipe()
135+
|> Enum.reduce({:iodata, right}, fn
136+
{[], 0}, acc -> acc
137+
{{:sigil_SQL, _meta, [{:<<>>, _, value}, []]}, 0}, {type, acc} -> {type, [value, ?\s, acc]}
138+
{{_, _, _} = var, 0}, {_, acc} ->
139+
{:dynamic, [var, ?\s, acc]}
140+
end)
141+
|> case do
142+
{:iodata, data} -> IO.iodata_to_binary(data)
143+
{:dynamic, data} -> data
144+
end
145+
end
146146

147-
defimpl Inspect, for: SQL do
148-
def inspect(sql, _opts) do
149-
if Kernel.function_exported?(sql.module, :sql_config, 0) do
150-
Enum.reduce(0..length(sql.params), :persistent_term.get(sql.id), &String.replace(&2, sql.module.sql_config()[:adapter].token_to_string({:binding, [], [&1]}), sql.module.sql_config()[:adapter].token_to_string(Enum.at(sql.params, &1)), global: false))
151-
else
152-
Enum.reduce(0..length(sql.params), :persistent_term.get(sql.id), &String.replace(&2, SQL.String.token_to_sql({:binding, [], [&1]}), SQL.String.token_to_sql(Enum.at(sql.params, &1))))
153-
end
147+
@doc false
148+
def id(data) do
149+
if id = :persistent_term.get(data, nil) do
150+
id
151+
else
152+
id = System.unique_integer([:positive])
153+
:persistent_term.put(data, id)
154+
id
154155
end
155156
end
156157

157-
defimpl String.Chars, for: SQL do
158-
def to_string(%{id: id}), do: :persistent_term.get(id)
159-
def to_string(%{tokens: tokens, module: module}), do: SQL.to_string(tokens, module)
158+
@doc false
159+
def cast_params(bindings, params, binding) do
160+
Enum.reduce(bindings, params, fn
161+
{:var, var}, acc -> if v = binding[String.to_atom(var)], do: acc ++ [v], else: acc
162+
{:code, code}, acc -> acc ++ [elem(Code.eval_string(code, binding), 0)]
163+
end)
164+
end
165+
166+
@doc false
167+
def tokens(binary, file, count, id) do
168+
key = {id, :lex}
169+
if result = :persistent_term.get(key, nil) do
170+
result
171+
else
172+
{:ok, opts, _, _, _, _, tokens} = SQL.Lexer.lex(binary, file, count)
173+
result = {tokens, opts[:binding]}
174+
:persistent_term.put(key, result)
175+
result
176+
end
177+
end
178+
179+
@doc false
180+
def plan(tokens, id, module) do
181+
key = {module, id, :plan}
182+
if :persistent_term.get(key, nil) do
183+
id
184+
else
185+
:persistent_term.put(key, to_string(SQL.to_query(SQL.Parser.parse(tokens)), module))
186+
id
187+
end
188+
end
189+
190+
@doc false
191+
def plan_inspect(data, id) do
192+
key = {id, :inspect}
193+
if !:persistent_term.get(key, nil) do
194+
data = case data do
195+
data when is_list(data) ->
196+
data
197+
|> Enum.map(fn
198+
ast when is_struct(ast) -> :persistent_term.get({ast.id, :inspect}, nil)
199+
x -> x
200+
end)
201+
|> IO.iodata_to_binary()
202+
203+
data -> data
204+
end
205+
206+
:persistent_term.put(key, data)
207+
end
160208
end
161209
end

test/sql_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ defmodule SQLTest do
3333
end
3434

3535
test "inspect/1" do
36-
assert "select +1000" == inspect(~SQL[select +1000])
36+
assert ~s(~SQL"""\nselect +1000\n""") == inspect(~SQL[select +1000])
3737
end
3838

3939
test "to_sql/1" do

0 commit comments

Comments
 (0)