Skip to content

Commit 9999a4f

Browse files
authored
Refactor SQL generation, part 2 (#15)
* release_savepoint and with_savepoint don't need to be public. * Make the instances of `expr` as consistent as possible with Postgres. * Make select and distinct handling more like Postgres.Connection. * Make group by and having generation more like Postgres.Connection. * Make order by generation more like Postgres.Connection. * Make limit and offset generation more like Postgres.Connection. * Remove temporary error! clause. * Formatting consistency.
1 parent 657ad8d commit 9999a4f

File tree

2 files changed

+102
-87
lines changed

2 files changed

+102
-87
lines changed

lib/sqlite_ecto/connection.ex

Lines changed: 100 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,6 @@ if Code.ensure_loaded?(Sqlitex.Server) do
7979
"ROLLBACK TO SAVEPOINT " <> savepoint
8080
end
8181

82-
def release_savepoint(name) do
83-
"RELEASE " <> name
84-
end
85-
86-
# Initiate a transaction with a savepoint. If any error occurs when we call
87-
# the func parameter, rollback our changes. Returns the result of the call
88-
# to func.
89-
def with_savepoint(pid, func) do
90-
sp = "sp_" <> random_id
91-
:ok = exec(pid, savepoint(sp))
92-
result = safe_call(pid, func, sp)
93-
if is_tuple(result) and elem(result, 0) == :error do
94-
:ok = exec(pid, rollback_to_savepoint(sp))
95-
end
96-
:ok = exec(pid, release_savepoint(sp))
97-
result
98-
end
99-
10082
## Query
10183

10284
alias Ecto.Query
@@ -109,16 +91,19 @@ if Code.ensure_loaded?(Sqlitex.Server) do
10991
end
11092
def all(query) do
11193
sources = create_names(query, :select)
94+
distinct_exprs = distinct_exprs(query, sources)
11295

113-
select = select(query.select, query.distinct, sources)
11496
from = from(sources)
97+
select = select(query, distinct_exprs, sources)
11598
join = join(query, sources)
11699
where = where(query, sources)
117-
group_by = group_by(query.group_bys, query.havings, sources)
118-
order_by = order_by(query.order_bys, sources)
119-
limit = limit(query.limit, query.offset, sources)
100+
group_by = group_by(query, sources)
101+
having = having(query, sources)
102+
order_by = order_by(query, distinct_exprs, sources)
103+
limit = limit(query, sources)
104+
offset = offset(query, sources)
120105

121-
assemble [select, from, join, where, group_by, order_by, limit]
106+
assemble([select, from, join, where, group_by, having, order_by, limit, offset])
122107
end
123108

124109
def update_all(%Ecto.Query{joins: [_ | _]}) do
@@ -349,17 +334,23 @@ if Code.ensure_loaded?(Sqlitex.Server) do
349334

350335
defp handle_call(fun, _arity), do: {:fun, Atom.to_string(fun)}
351336

352-
defp select(%SelectExpr{fields: fields}, distinct, sources) do
353-
fields = Enum.map_join(fields, ", ", fn (f) ->
354-
assemble(expr(f, sources, "query"))
355-
end)
356-
["SELECT", distinct(distinct), fields]
337+
defp select(%Query{select: %SelectExpr{fields: fields}, distinct: distinct} = query,
338+
distinct_exprs, sources) do
339+
"SELECT " <>
340+
distinct(distinct, distinct_exprs) <>
341+
Enum.map_join(fields, ", ", &expr(&1, sources, query))
357342
end
358343

359-
defp distinct(nil), do: []
360-
defp distinct(%QueryExpr{expr: true}), do: "DISTINCT"
361-
defp distinct(%QueryExpr{expr: false}), do: []
362-
defp distinct(%QueryExpr{expr: exprs}) when is_list(exprs) do
344+
defp distinct_exprs(%Query{distinct: %QueryExpr{expr: exprs}} = query, sources)
345+
when is_list(exprs) do
346+
Enum.map_join(exprs, ", ", &expr(&1, sources, query))
347+
end
348+
defp distinct_exprs(_, _), do: ""
349+
350+
defp distinct(nil, _sources), do: ""
351+
defp distinct(%QueryExpr{expr: true}, _exprs), do: "DISTINCT "
352+
defp distinct(%QueryExpr{expr: false}, _exprs), do: ""
353+
defp distinct(_query, exprs) do
363354
raise ArgumentError, "DISTINCT with multiple columns is not supported by SQLite"
364355
end
365356

@@ -376,7 +367,7 @@ if Code.ensure_loaded?(Sqlitex.Server) do
376367
where = expr(expr, sources, query)
377368
"USING #{table} AS #{name} WHERE " <> where
378369
%JoinExpr{qual: qual} ->
379-
error!(query, "PostgreSQL supports only inner joins on delete_all, got: `#{qual}`")
370+
error!(query, "SQLite supports only inner joins on delete_all, got: `#{qual}`")
380371
end)
381372
end
382373

@@ -459,49 +450,56 @@ if Code.ensure_loaded?(Sqlitex.Server) do
459450
["WHERE" | Enum.intersperse(filters, "AND")]
460451
end
461452

462-
defp having([], _sources), do: []
463-
defp having(havings, sources) do
464-
exprs = map_intersperse havings, "AND", fn %QueryExpr{expr: expr} ->
465-
["(", expr(expr, sources, "query"), ")"]
466-
end
467-
["HAVING" | exprs]
453+
defp having(%Query{havings: havings} = query, sources) do
454+
boolean("HAVING", havings, sources, query)
468455
end
469456

470-
defp group_by(group_bys, havings, sources) do
471-
Enum.map_join(group_bys, ", ", fn %QueryExpr{expr: expr} ->
472-
Enum.map_join(expr, ", ", &assemble(expr(&1, sources, "query")))
473-
end)
474-
|> group_by_clause(havings, sources)
475-
end
457+
defp group_by(%Query{group_bys: group_bys} = query, sources) do
458+
exprs =
459+
Enum.map_join(group_bys, ", ", fn
460+
%QueryExpr{expr: expr} ->
461+
Enum.map_join(expr, ", ", &expr(&1, sources, query))
462+
end)
476463

477-
defp group_by_clause("", _, _), do: []
478-
defp group_by_clause(exprs, havings, sources) do
479-
["GROUP BY", exprs, having(havings, sources)]
464+
case exprs do
465+
"" -> []
466+
_ -> "GROUP BY " <> exprs
467+
end
480468
end
481469

482-
defp order_by(order_bys, sources) do
483-
Enum.map_join(order_bys, ", ", fn %QueryExpr{expr: expr} ->
484-
Enum.map_join(expr, ", ", &ordering_term(&1, sources))
485-
end)
486-
|> order_by_clause
487-
end
470+
defp order_by(%Query{order_bys: order_bys} = query, distinct_exprs, sources) do
471+
exprs =
472+
Enum.map_join(order_bys, ", ", fn
473+
%QueryExpr{expr: expr} ->
474+
Enum.map_join(expr, ", ", &order_by_expr(&1, sources, query))
475+
end)
488476

489-
defp order_by_clause(""), do: []
490-
defp order_by_clause(exprs), do: ["ORDER BY", exprs]
477+
case {distinct_exprs, exprs} do
478+
{_, ""} ->
479+
[]
480+
{"", _} ->
481+
"ORDER BY " <> exprs
482+
{_, _} ->
483+
"ORDER BY " <> distinct_exprs <> ", " <> exprs
484+
end
485+
end
491486

492-
defp ordering_term({:asc, expr}, sources), do: assemble(expr(expr, sources, "query"))
493-
defp ordering_term({:desc, expr}, sources) do
494-
assemble(expr(expr, sources, "query")) <> " DESC"
487+
defp order_by_expr({dir, expr}, sources, query) do
488+
str = expr(expr, sources, query)
489+
case dir do
490+
:asc -> str
491+
:desc -> str <> " DESC"
492+
end
495493
end
496494

497-
defp limit(nil, _offset, _sources), do: []
498-
defp limit(%QueryExpr{expr: expr}, offset, sources) do
499-
["LIMIT", expr(expr, sources, "query"), offset(offset, sources)]
495+
defp limit(%Query{limit: nil}, _sources), do: []
496+
defp limit(%Query{limit: %QueryExpr{expr: expr}} = query, sources) do
497+
"LIMIT " <> expr(expr, sources, query)
500498
end
501499

502-
defp offset(nil, _sources), do: []
503-
defp offset(%QueryExpr{expr: expr}, sources) do
504-
["OFFSET", expr(expr, sources, "query")]
500+
defp offset(%Query{offset: nil}, _sources), do: []
501+
defp offset(%Query{offset: %QueryExpr{expr: expr}} = query, sources) do
502+
"OFFSET " <> expr(expr, sources, query)
505503
end
506504

507505
defp boolean(_name, [], _sources, _query), do: []
@@ -519,17 +517,19 @@ if Code.ensure_loaded?(Sqlitex.Server) do
519517

520518
defp expr({{:., _, [{:&, _, [idx]}, field]}, _, []}, sources, _query) when is_atom(field) do
521519
{_, name, _} = elem(sources, idx)
522-
"#{name}.#{quote_id(field)}"
520+
"#{name}.#{quote_name(field)}"
523521
end
524522

525-
defp expr({:&, _, [idx]}, sources, _query) do
523+
defp expr({:&, _, [idx]}, sources, query) do
526524
{table, name, model} = elem(sources, idx)
527525
unless model do
528-
raise ArgumentError, "SQLite requires a model when using selector #{inspect name} but " <>
529-
"only the table #{inspect table} was given. Please specify a model " <>
530-
"or specify exactly which fields from #{inspect name} you desire"
526+
error!(query, "SQLite requires a model when using selector " <>
527+
"#{inspect name} but only the table #{inspect table} was given. " <>
528+
"Please specify a model or specify exactly which fields from " <>
529+
"#{inspect name} you desire")
531530
end
532-
map_intersperse(model.__schema__(:fields), ",", &"#{name}.#{quote_id(&1)}")
531+
fields = model.__schema__(:fields)
532+
Enum.map_join(fields, ", ", &"#{name}.#{quote_name(&1)}")
533533
end
534534

535535
defp expr({:in, _, [left, right]}, sources, query) when is_list(right) do
@@ -554,8 +554,8 @@ if Code.ensure_loaded?(Sqlitex.Server) do
554554
"NOT (" <> expr(expr, sources, query) <> ")"
555555
end
556556

557-
defp expr({:fragment, _, [kw]}, _sources, _query) when is_list(kw) or tuple_size(kw) == 3 do
558-
raise ArgumentError, "SQLite adapter does not support keyword or interpolated fragments"
557+
defp expr({:fragment, _, [kw]}, _sources, query) when is_list(kw) or tuple_size(kw) == 3 do
558+
error!(query, "SQLite adapter does not support keyword or interpolated fragments")
559559
end
560560

561561
defp expr({:fragment, _, parts}, sources, query) do
@@ -565,20 +565,16 @@ if Code.ensure_loaded?(Sqlitex.Server) do
565565
end)
566566
end
567567

568-
# start of SQLite function to display date
569-
# NOTE the open parenthesis must be closed
570-
@date_format "strftime('%Y-%m-%d'"
568+
@datetime_format "strftime('%Y-%m-%d %H:%M:%f000'" # NOTE: Open paren must be closed
571569

572-
defp expr({:date_add, _, [date, count, interval]}, sources, query) do
573-
"CAST (#{@date_format},#{expr(date, sources, query)},#{interval(count, interval, sources)}) AS TEXT_DATE)"
570+
defp expr({:datetime_add, _, [datetime, count, interval]}, sources, query) do
571+
"CAST (#{@datetime_format},#{expr(datetime, sources, query)},#{interval(count, interval, sources)}) AS TEXT_DATETIME)"
574572
end
575573

576-
# start of SQLite function to display datetime
577-
# NOTE the open parenthesis must be closed
578-
@datetime_format "strftime('%Y-%m-%d %H:%M:%f000'"
574+
@date_format "strftime('%Y-%m-%d'" # NOTE: Open paren must be closed
579575

580-
defp expr({:datetime_add, _, [datetime, count, interval]}, sources, query) do
581-
"CAST (#{@datetime_format},#{expr(datetime, sources, query)},#{interval(count, interval, sources)}) AS TEXT_DATETIME)"
576+
defp expr({:date_add, _, [date, count, interval]}, sources, query) do
577+
"CAST (#{@date_format},#{expr(date, sources, query)},#{interval(count, interval, sources)}) AS TEXT_DATE)"
582578
end
583579

584580
defp expr({fun, _, args}, sources, query) when is_atom(fun) and is_list(args) do
@@ -604,7 +600,8 @@ if Code.ensure_loaded?(Sqlitex.Server) do
604600
Decimal.to_string(decimal, :normal)
605601
end
606602

607-
defp expr(%Ecto.Query.Tagged{value: binary, type: :binary}, _sources, _query) when is_binary(binary) do
603+
defp expr(%Ecto.Query.Tagged{value: binary, type: :binary}, _sources, _query)
604+
when is_binary(binary) do
608605
"X'#{Base.encode16(binary, case: :upper)}'"
609606
end
610607

@@ -888,6 +885,24 @@ if Code.ensure_loaded?(Sqlitex.Server) do
888885
# Use Ecto's JSON library (currently Poison) for embedded JSON datatypes.
889886
def json_library, do: Application.get_env(:ecto, :json_library)
890887

888+
# Initiate a transaction with a savepoint. If any error occurs when we call
889+
# the func parameter, rollback our changes. Returns the result of the call
890+
# to func.
891+
defp with_savepoint(pid, func) do
892+
sp = "sp_" <> random_id
893+
:ok = exec(pid, savepoint(sp))
894+
result = safe_call(pid, func, sp)
895+
if is_tuple(result) and elem(result, 0) == :error do
896+
:ok = exec(pid, rollback_to_savepoint(sp))
897+
end
898+
:ok = exec(pid, release_savepoint(sp))
899+
result
900+
end
901+
902+
defp release_savepoint(name) do
903+
"RELEASE " <> name
904+
end
905+
891906
# Call func.() and return the result. If any exceptions are encountered,
892907
# safely rollback and release the transaction.
893908
defp safe_call(pid, func, sp) do
@@ -977,7 +992,7 @@ if Code.ensure_loaded?(Sqlitex.Server) do
977992
end
978993

979994
defp error!(nil, message) do
980-
raise ArgumentError, message
995+
raise ArgumentError, message: message
981996
end
982997
defp error!(query, message) do
983998
raise Ecto.QueryError, query: query, message: message

test/sqlite_ecto_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ defmodule Sqlite.Ecto.Test do
357357
query = "posts" |> select([r], r.x) |> normalize
358358
assert SQL.all(query) == ~s{SELECT p0."x" FROM "posts" AS p0}
359359

360-
assert_raise ArgumentError, ~r"SQLite requires a model", fn ->
360+
assert_raise Ecto.QueryError, ~r"SQLite requires a model", fn ->
361361
SQL.all from(p in "posts", select: p) |> normalize()
362362
end
363363
end
@@ -473,7 +473,7 @@ defmodule Sqlite.Ecto.Test do
473473
assert SQL.all(query) == ~s{SELECT ltrim(m0."x", ?) FROM "model" AS m0}
474474

475475
query = Model |> select([], fragment(title: 2)) |> normalize
476-
assert_raise ArgumentError, "SQLite adapter does not support keyword or interpolated fragments", fn ->
476+
assert_raise Ecto.QueryError, ~r"SQLite adapter does not support keyword or interpolated fragments", fn ->
477477
SQL.all(query)
478478
end
479479
end

0 commit comments

Comments
 (0)