diff --git a/.gitignore b/.gitignore index b6012c7..c60243c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez + +.elixir_ls \ No newline at end of file diff --git a/lib/triton/cql/delete.ex b/lib/triton/cql/delete.ex index 84b0560..945fb02 100644 --- a/lib/triton/cql/delete.ex +++ b/lib/triton/cql/delete.ex @@ -1,28 +1,43 @@ defmodule Triton.CQL.Delete do def build(query) do delete(query[:delete], query[:where], query[:__table__]) <> - constrain(query[:constrain]) <> - if_exists(query[:if_exists]) + constrain(query[:constrain]) <> if_exists(query[:if_exists]) end - defp delete(fields, where, table) when is_list(fields) and is_list(where), do: "DELETE #{fields |> Enum.join(", ")} FROM #{table}#{where(where)}" - defp delete(:all, where, table) when is_list(where), do: "DELETE FROM #{table}#{where(where)}" + defp delete(fields, where, table) when is_list(fields) and is_list(where), + do: "DELETE " <> Enum.join(fields, ", ") <> " FROM #{table}" <> where(where) + + defp delete(:all, where, table) when is_list(where), do: "DELETE FROM #{table}" <> where(where) defp delete(_, _, _), do: "" - defp where(fragments) when is_list(fragments), do: " WHERE " <> (fragments |> Enum.flat_map(fn fragment -> where_fragment(fragment) end) |> Enum.join(" AND ")) + defp where(fragments) when is_list(fragments), + do: + " WHERE " <> + (fragments + |> Enum.flat_map(fn fragment -> where_fragment(fragment) end) + |> Enum.join(" AND ")) + defp where(_), do: "" - defp where_fragment({k, v}) when is_list(v), do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}) end) - defp where_fragment({k, v}), do: ["#{k} = #{value(v)}"] - defp where_fragment({k, :in, v}), do: "#{k} IN (#{v |> Enum.map(fn v -> value(v) end) |> Enum.join(", ")})" - defp where_fragment({k, c, v}), do: "#{k} #{c} #{value(v)}" - defp constrain(constraints) when is_list(constraints), do: " IF #{constraints |> Enum.map(fn {k, v} -> "#{k} = #{value(v)}" end) |> Enum.join(" AND ")}" + defp where_fragment({k, v}) when is_list(v), + do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}) end) + + defp where_fragment({k, v}), do: ["#{k} = " <> value(v)] + + defp where_fragment({k, :in, v}), + do: "#{k} IN (" <> Enum.map_join(v, ", ", fn v -> value(v) end) <> ")" + + defp where_fragment({k, c, v}), do: "#{k} #{c} " <> value(v) + + defp constrain(constraints) when is_list(constraints), + do: " IF " <> Enum.map_join(constraints, ", ", fn {k, v} -> "#{k} = " <> value(v) end) + defp constrain(_), do: "" defp if_exists(flag) when flag == true, do: " IF EXISTS" defp if_exists(_), do: "" - defp value(v) when is_binary(v), do: "'#{v}'" + defp value(v) when is_binary(v), do: "'" <> v <> "'" defp value(v) when is_atom(v), do: ":#{v}" - defp value(v), do: v + defp value(v), do: to_string(v) end diff --git a/lib/triton/cql/helper.ex b/lib/triton/cql/helper.ex new file mode 100644 index 0000000..20f4c23 --- /dev/null +++ b/lib/triton/cql/helper.ex @@ -0,0 +1,42 @@ +defmodule Triton.CQL.Helper do + def field_value(nil, _), do: "NULL" + def field_value(field, {:map, _}) when is_map(field), do: binary_value(field) + + def field_value(field, :timestamp) when is_binary(field) do + {:ok, timestamp, _} = DateTime.from_iso8601(field) + + timestamp + |> DateTime.to_unix(:millisecond) + |> to_string() + end + + def field_value(v, :counter), do: v + + def field_value(field, {_, _}), do: field + def field_value(field, _) when is_boolean(field), do: to_string(field) + def field_value(field, _) when is_binary(field), do: binary_value(field) + def field_value(field, _) when is_atom(field), do: ":#{field}" + def field_value(%DateTime{} = d, _), do: DateTime.to_unix(d, :millisecond) + def field_value(field, _), do: to_string(field) + + def if_not_exists(flag) when flag == true, do: " IF NOT EXISTS" + def if_not_exists(_), do: "" + + def if_exists(flag) when flag == true, do: " IF EXISTS" + def if_exists(_), do: "" + + def binary_value(v) when is_binary(v) do + if String.valid?(v) && String.contains?(v, ["'", "\""]) do + "$$" <> v <> "$$" + else + "'" <> v <> "'" + end + end + + def binary_value(v) when is_map(v), do: "{" <> Enum.map_join(v, ",", &binary_value/1) <> "}" + + # This will fail if v `is_map`, nested maps are generally not OK + def binary_value({k, v}), do: binary_value(k) <> ": " <> binary_value(v) + + def binary_value(v), do: v |> to_string() |> binary_value() +end diff --git a/lib/triton/cql/insert.ex b/lib/triton/cql/insert.ex index 7b30abb..6980b26 100644 --- a/lib/triton/cql/insert.ex +++ b/lib/triton/cql/insert.ex @@ -1,29 +1,20 @@ defmodule Triton.CQL.Insert do + import Triton.CQL.Helper + def build(query) do schema = query[:__schema__].__fields__ - insert(query[:insert], query[:__table__], schema) <> - if_not_exists(query[:if_not_exists]) + insert(query[:insert], query[:__table__], schema) <> if_not_exists(query[:if_not_exists]) end - defp insert(fields, table, schema) when is_list(fields), do: "INSERT INTO #{table} (#{field_keys(fields)}) VALUES (#{field_values(fields, schema)})" - defp field_keys(fields) when is_list(fields), do: fields |> Enum.map(fn {k, _} -> k end) |> Enum.join(", ") - defp field_values(fields, schema) when is_list(fields), do: fields |> Enum.map(fn {k, v} -> field_value(v, schema[k][:type]) end) |> Enum.join(", ") - defp field_value(nil, _), do: "NULL" - defp field_value(field, {_, _}), do: field - defp field_value(field, _) when is_boolean(field), do: "#{field}" - defp field_value(field, _) when is_binary(field), do: binary_value(field) - defp field_value(field, _) when is_atom(field), do: ":#{field}" - defp field_value(%DateTime{} = d, _), do: DateTime.to_unix(d, :millisecond) - defp field_value(field, _), do: field + defp insert(fields, table, schema) when is_list(fields), + do: + "INSERT INTO #{table} (" <> + field_keys(fields) <> ") VALUES (" <> field_values(fields, schema) <> ")" - defp if_not_exists(flag) when flag == true, do: " IF NOT EXISTS" - defp if_not_exists(_), do: "" + defp field_keys(fields) when is_list(fields), + do: fields |> Keyword.keys() |> Enum.join(", ") - defp binary_value(v) do - cond do - String.valid?(v) && String.contains?(v, ["'", "\""]) -> "$$#{v}$$" - true -> "'#{v}'" - end - end + defp field_values(fields, schema) when is_list(fields), + do: Enum.map_join(fields, ", ", fn {k, v} -> field_value(v, schema[k][:type]) end) end diff --git a/lib/triton/cql/select.ex b/lib/triton/cql/select.ex index ef0a35b..d4d6d45 100644 --- a/lib/triton/cql/select.ex +++ b/lib/triton/cql/select.ex @@ -2,28 +2,45 @@ defmodule Triton.CQL.Select do def build(query) do schema = query[:__schema__].__fields__ - select(query[:select], query[:count], query[:__table__], schema) <> - where(query[:where]) <> - order_by(query[:order_by] && List.first(query[:order_by])) <> - limit(query[:limit]) <> - allow_filtering(query[:allow_filtering]) + select_s = select(query[:select], query[:count], query[:__table__], schema) + where_s = where(query[:where]) + order_by_s = order_by(query[:order_by] && List.first(query[:order_by])) + limit_s = limit(query[:limit]) + filter_s = allow_filtering(query[:allow_filtering]) + + (select_s <> where_s <> order_by_s <> limit_s <> filter_s) end - defp select(_, count, table, _) when count === true, do: "SELECT COUNT(*) FROM #{table}" + defp select(_, count, table, _) when count == true, do: "SELECT COUNT(*) FROM #{table}" + defp select(fields, _, table, schema) when is_list(fields) do - schema_fields = schema |> Enum.map(fn {k, _} -> "#{k}" end) - req_fields = fields |> Enum.map(fn k -> "#{k}" end) - filtered_fields = MapSet.intersection(MapSet.new(req_fields), MapSet.new(schema_fields)) |> Enum.into([]) - "SELECT #{Enum.join(filtered_fields, ", ")} FROM #{table}" + schema_fields = schema |> Enum.into(MapSet.new(), fn {k, _} -> to_string(k) end) + req_fields = fields |> Enum.into(MapSet.new(), &to_string/1) + filtered_fields = MapSet.intersection(req_fields, schema_fields) |> Enum.join(", ") + + "SELECT " <> filtered_fields <> " FROM #{table}" end + defp select(_, _, table, _), do: "SELECT * FROM #{table}" - defp where(fragments) when is_list(fragments), do: " WHERE " <> (fragments |> Enum.flat_map(fn fragment -> where_fragment(fragment) end) |> Enum.join(" AND ")) + defp where(fragments) when is_list(fragments) do + " WHERE " <> + (fragments + |> Enum.flat_map(&where_fragment/1) + |> Enum.join(" AND ")) + end + defp where(_), do: "" - defp where_fragment({k, v}) when is_list(v), do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}) end) - defp where_fragment({k, v}), do: ["#{k} = #{value(v)}"] - defp where_fragment({k, :in, v}), do: "#{k} IN (#{v |> Enum.map(fn v -> value(v) end) |> Enum.join(", ")})" - defp where_fragment({k, c, v}), do: "#{k} #{c} #{value(v)}" + + defp where_fragment({k, v}) when is_list(v), + do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}) end) + + defp where_fragment({k, v}), do: ["#{k} = " <> value(v)] + + defp where_fragment({k, :in, v}), + do: ["#{k} IN ( " <> Enum.map_join(v, ", ", fn v -> value(v) end) <> ")"] + + defp where_fragment({k, c, v}), do: ["#{k} #{c} " <> value(v)] defp order_by({field, direction}), do: " ORDER BY #{field} #{direction}" defp order_by(_), do: "" @@ -35,8 +52,8 @@ defmodule Triton.CQL.Select do defp allow_filtering(true), do: " ALLOW FILTERING" defp allow_filtering(_), do: "" - defp value(v) when is_binary(v), do: "'#{v}'" - defp value(v) when is_boolean(v), do: "#{v}" + defp value(v) when is_binary(v), do: "'" <> v <> "'" + defp value(v) when is_boolean(v), do: to_string(v) defp value(v) when is_atom(v), do: ":#{v}" - defp value(v), do: v + defp value(v), do: to_string(v) end diff --git a/lib/triton/cql/update.ex b/lib/triton/cql/update.ex index 109b927..2508696 100644 --- a/lib/triton/cql/update.ex +++ b/lib/triton/cql/update.ex @@ -1,43 +1,45 @@ defmodule Triton.CQL.Update do + import Triton.CQL.Helper + def build(query) do schema = query[:__schema__].__fields__ update(query[:__table__]) <> - set(query[:update], schema) <> - where(query[:where], schema) <> - constrain(query[:constrain], schema) <> - if_exists(query[:if_exists]) + set(query[:update], schema) <> + where(query[:where], schema) <> + constrain(query[:constrain], schema) <> if_exists(query[:if_exists]) end defp update(table), do: "UPDATE #{table}" - defp set(assignments, schema) when is_list(assignments), do: " SET #{assignments |> Enum.map(fn {k, v} -> "#{k} = #{value(v, schema[k][:type])}" end) |> Enum.join(", ")}" - defp where(fragments, schema) when is_list(fragments), do: " WHERE " <> (fragments |> Enum.flat_map(fn fragment -> where_fragment(fragment, schema) end) |> Enum.join(" AND ")) + defp set(assignments, schema) when is_list(assignments), + do: " SET " <> Enum.map_join(assignments, ", ", &key_eq_field_value(&1, schema)) + + defp where(fragments, schema) when is_list(fragments), + do: + " WHERE " <> + (fragments + |> Enum.flat_map(& where_fragment(&1, schema)) + |> Enum.join(" AND ")) + defp where(_, _), do: "" - defp where_fragment({k, v}, schema) when is_list(v), do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}, schema) end) - defp where_fragment({k, v}, schema), do: ["#{k} = #{value(v, schema[k][:type])}"] - defp where_fragment({k, :in, v}, schema), do: "#{k} IN (#{v |> Enum.map(fn v -> value(v, schema[k][:type]) end) |> Enum.join(", ")})" - defp where_fragment({k, c, v}, schema), do: "#{k} #{c} #{value(v, schema[k][:type])}" - defp constrain(constraints, schema) when is_list(constraints), do: " IF #{constraints |> Enum.map(fn {k, v} -> "#{k} = #{value(v, schema[k][:type])}" end) |> Enum.join(" AND ")}" + defp where_fragment({k, v}, schema) when is_list(v), + do: v |> Enum.map(fn {c, v} -> where_fragment({k, c, v}, schema) end) + + defp where_fragment({k, v}, schema), do: ["#{k} = " <> field_value(v, schema[k][:type])] + + defp where_fragment({k, :in, v}, schema), + do: "#{k} IN (" <> Enum.map_join(v, ", ", fn v -> field_value(v, schema[k][:type]) end) <> ")" + + defp where_fragment({k, c, v}, schema), do: "#{k} #{c} " <> field_value(v, schema[k][:type]) + + defp constrain(constraints, schema) when is_list(constraints), + do: " IF " <> Enum.map_join(constraints, " AND ", &key_eq_field_value(&1, schema)) + defp constrain(_, _), do: "" - defp if_exists(flag) when flag == true, do: " IF EXISTS" - defp if_exists(_), do: "" - - defp value(nil, _), do: "NULL" - defp value(v, :counter), do: v - defp value(v, {_, _}), do: v - defp value(v, _) when is_boolean(v), do: "#{v}" - defp value(v, _) when is_binary(v), do: binary_value(v) - defp value(v, _) when is_atom(v), do: ":#{v}" - defp value(%DateTime{} = d, _), do: DateTime.to_unix(d, :millisecond) - defp value(v, _), do: v - - defp binary_value(v) do - cond do - String.valid?(v) && String.contains?(v, ["'", "\""]) -> "$$#{v}$$" - true -> "'#{v}'" - end + defp key_eq_field_value({k, v}, schema) do + "#{k} = " <> field_value(v, schema[k][:type]) end end diff --git a/lib/triton/validate.ex b/lib/triton/validate.ex index a3ef755..0a8e018 100644 --- a/lib/triton/validate.ex +++ b/lib/triton/validate.ex @@ -41,9 +41,14 @@ defmodule Triton.Validate do defp coerce_fragment({k, v}, fields) when is_list(v), do: {k, v |> Enum.map(fn {c, v} -> coerce_fragment({k, c, v}, fields) end)} defp coerce_fragment({k, v}, fields), do: {k, coerced_value(v, fields[k][:type])} + defp coerce_fragment({k, "like", v}, _fields), do: {k, :like, v} + defp coerce_fragment({k, :like, v}, _fields), do: {k, :like, v} defp coerce_fragment({k, c, v}, fields), do: {c, coerced_value(v, fields[k][:type])} defp coerce_fragment(x, _), do: x + defp coerced_value(value, type) when is_list(value), + do: Enum.map(value, &coerced_value(&1, type)) + defp coerced_value(value, _) when is_atom(value), do: value defp coerced_value(value, :text) when not is_binary(value), do: to_string(value) defp coerced_value(value, :bigint) when is_binary(value), do: String.to_integer(value)