├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── config └── config.exs ├── lib ├── ecdo.ex └── ecdo │ └── builder │ ├── data.ex │ ├── from.ex │ ├── join.ex │ ├── load.ex │ ├── order_by.ex │ ├── query_expr.ex │ ├── select.ex │ └── where.ex ├── mix.exs ├── mix.lock └── test ├── ecdo_query_test.exs ├── ecdo_test.exs ├── ecdo_type_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: elixir 3 | elixir: 4 | - 1.0.5 5 | otp_release: 6 | - 17.5 7 | - 18.0 8 | script: "MIX_ENV=test mix do deps.get, deps.compile, test --cover" 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.4 2 | 3 | * Bug fixes 4 | * fix possiblity to use more than one field in where with or/and 5 | 6 | # 0.1.3 7 | 8 | * Enhancements 9 | * update to ecto ~> 1.0.0 10 | 11 | # 0.1.2 12 | 13 | * Enhancements 14 | * add possibiity to mix parsed and unparsed form in where in query 15 | * update to ecto ~> 0.16.0 16 | 17 | # 0.1.1 18 | 19 | * Enhancements 20 | * add possibility to pass queries to preload 21 | 22 | * Bug fixes 23 | * fix possibility to use :like and :ilike in parsed tree 24 | 25 | # 0.1.0 26 | 27 | First release! 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ecdo [![Build Status](https://travis-ci.org/xerions/ecdo.svg)](https://travis-ci.org/xerions/ecdo) [![Coverage Status](https://coveralls.io/repos/xerions/ecdo/badge.svg?branch=master&service=github)](https://coveralls.io/github/xerions/ecdo?branch=master) 2 | ==== 3 | 4 | Like an [ecto](https://github.com/elixir-lang/ecto), but dynamic. EcDo. 5 | Ecto is a domain specific language for writing queries and interacting with databases in Elixir. 6 | Ecdo is a dynamic interface for ecto. 7 | 8 | Ecdo was build for accessing it from erlang(elixir need to be in path) 9 | or simplify building dynamic interface, for example API. 10 | 11 | Example of using from erlang: 12 | 13 | ``` 14 | 'Elixir.Ecdo':query({<<"weather">>, 'Elixir.Weather'}, #{<<"where">> => [{'==', <<"weather.id">>, 1}]}) 15 | 'Elixir.Ecdo':query({<<"weather">>, 'Elixir.Weather'}, #{<<"where">> => <<"weather.id == 1">>}) 16 | ``` 17 | 18 | Example of use from elixir: 19 | 20 | ```elixir 21 | Ecdo.query {"weather", Weather}, %{"where" => "weather.id == 1"} 22 | ``` 23 | 24 | Simple example of building API: 25 | 26 | ```elixir 27 | defmodule Weather.Api do 28 | def json(json) do 29 | map = Poison.decode! json 30 | # may be restrict something 31 | Ecdo.query({"weather", Weather}, map) |> Repo.all 32 | end 33 | end 34 | 35 | # Example of use: 36 | 37 | Weather.Api.json ~S({"where": "weather.temp_lo > 25", "limit": 10}) 38 | ``` 39 | 40 | Due to some direct manipulations with ecto intern AST, ecdo aims to have always close to 100% of test cover. 41 | 42 | ## Usage 43 | 44 | You need to add both Ecdo and the database adapter as a dependency to your `mix.exs` file. The supported databases and their adapters are: 45 | 46 | Database | Ecto Adapter | Dependency 47 | :---------------------- | :--------------------- | :------------------- 48 | PostgreSQL | Ecto.Adapters.Postgres | [postgrex][postgrex] 49 | MySQL | Ecto.Adapters.MySQL | [mariaex][mariaex] 50 | MSSQL | Tds.Ecto | [tds_ecto][tds_ecto] 51 | SQLite3 | Sqlite.Ecto | [sqlite_ecto][sqlite_ecto] 52 | 53 | [postgrex]: http://github.com/ericmj/postgrex 54 | [mariaex]: http://github.com/xerions/mariaex 55 | [tds_ecto]: https://github.com/livehelpnow/tds_ecto 56 | [sqlite_ecto]: https://github.com/jazzyb/sqlite_ecto 57 | 58 | For example, if you want to use MySQL, add to your `mix.exs` file: 59 | 60 | ```elixir 61 | defp deps do 62 | [{:mariaex, ">= 0.0.0"}, 63 | {:ecdo, "~> 0.1.0"}] 64 | end 65 | ``` 66 | 67 | and update your applications list to include both projects: 68 | 69 | ```elixir 70 | def application do 71 | [applications: [:mariaex, :ecdo]] 72 | end 73 | ``` 74 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, 14 | # level: :info 15 | # 16 | # config :logger, :console, 17 | # format: "$date $time [$level] $metadata$message\n", 18 | # metadata: [:user_id] 19 | 20 | # It is also possible to import configuration files, relative to this 21 | # directory. For example, you can emulate configuration per environment 22 | # by uncommenting the line below and defining dev.exs, test.exs and such. 23 | # Configuration from the imported file will override the ones defined 24 | # here (which is why it is important to import them last). 25 | # 26 | # import_config "#{Mix.env}.exs" 27 | -------------------------------------------------------------------------------- /lib/ecdo.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo do 2 | @moduledoc """ 3 | Provides the Query function. 4 | 5 | Queries are used to retrieve and manipulate data in a repository (see 6 | Ecto.Repo). Although this module provides a complete API, supporting 7 | expressions like `where`, `select` and so forth. 8 | """ 9 | defstruct [sources: %{}, modules: %{}, count: 0, param: nil, query: %Ecto.Query{}] 10 | 11 | @doc """ 12 | Ecdo is dynamic query interface to ecto. It implements `Ecto.Queryable` protocol, and can be 13 | mixed and used with other ecto queries. 14 | 15 | Let's see an example: 16 | 17 | defmodule Weather do 18 | use Ecto.Model 19 | 20 | # weather is the DB table 21 | schema "weather" do 22 | field :city, :string 23 | field :temp_lo, :integer 24 | field :temp_hi, :integer 25 | field :prcp, :float, default: 0.0 26 | end 27 | end 28 | 29 | ## Query 30 | 31 | Ecdo simplify you to write dinamic query engines and send 32 | them to the repository, which translates them to the underlying database. 33 | 34 | Let's see an example: 35 | 36 | import Ecdo, only: [query: 2] 37 | 38 | query {"w", Weather}, %{where: "w.prcp > 0"} 39 | 40 | # Returns %Weather{} structs matching the query 41 | Repo.all(query) 42 | 43 | For easy of use, keywords can be as binaries or as atoms. select can be defined as 44 | list (`["w.id", "w.name"]`) or as string (`"w.id, w.name"`). All existing functionality 45 | is build to be easy used from CLI, as all of arguments can be unparsed strings. Ecdo take 46 | care of it. 47 | 48 | The supported keywords are: 49 | 50 | * `:distinct` 51 | * `:where` 52 | * `:order_by` 53 | * `:offset` 54 | * `:limit` 55 | * `:join` 56 | * `:select` 57 | * `:preload` 58 | 59 | Let's see more examples: 60 | 61 | query {"w", Weather}, %{where: "w.prcp > 0", distinct: true} 62 | 63 | query {"w", Weather}, %{"where" => "w.prcp > 0", "order_by" => "w.id"} 64 | 65 | query {"w", Weather}, %{where: "w.prcp > 0", limit: 10, offset: 10} 66 | 67 | query {"w", Weather}, %{where: "w.prcp > 0", select: "w.prpc"} 68 | 69 | query {"w", Weather}, %{where: "w.prcp > 0", preload: ["city"]} 70 | 71 | query {"w", Weather}, %{where: "w.prcp > 0 and city.id < 10", join: "city"} 72 | 73 | query {"w", Weather}, %{where: "w.prcp > 0 and city.id < 10", join: "city, foobar"} 74 | 75 | """ 76 | def query(source, query) do 77 | query = Map.keys(query) |> Enum.reduce(%{}, &check_string(&1, query, &2)) 78 | %Ecdo{} |> Ecdo.Builder.From.apply(source) 79 | |> Ecdo.Builder.Join.apply(query) 80 | |> Ecdo.Builder.Where.apply(query) 81 | |> Ecdo.Builder.Select.apply(query) 82 | |> Ecdo.Builder.OrderBy.apply(query) 83 | |> Ecdo.Builder.QueryExpr.apply(query) 84 | |> Ecdo.Builder.Load.apply(query) 85 | end 86 | 87 | @keys [:where, :select, :select_as, :count, :avg, :sum, :min, :max, :limit, :offset, :distinct, 88 | :order_by, :load, :preload, :left_join, :right_join, :full_join, :join] 89 | for key <- @keys do 90 | defp check_string(unquote(key), %{unquote(key) => value}, acc), 91 | do: Map.put(acc, unquote(key), value) 92 | defp check_string(unquote(to_string(key)), %{unquote(to_string(key)) => value}, acc), 93 | do: Map.put(acc, unquote(key), value) 94 | end 95 | defp check_string(_key, _, acc), do: acc 96 | end 97 | 98 | defimpl Ecto.Queryable, for: Ecdo do 99 | def to_query(ecdo), do: ecdo.query 100 | end 101 | -------------------------------------------------------------------------------- /lib/ecdo/builder/data.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.Data do 2 | @moduledoc """ 3 | Different helpers, to transform string or other structure to ecto query. 4 | """ 5 | 6 | @doc false 7 | defmacro __using__(_) do 8 | quote do 9 | alias Ecto.Query.SelectExpr 10 | alias Ecto.Query.JoinExpr 11 | alias Ecto.Query.QueryExpr 12 | import Ecto.Query 13 | import Ecdo.Builder.Data 14 | end 15 | end 16 | 17 | @doc """ 18 | Replace query in `Ecdo` or apply a fun on it. 19 | 20 | Example: 21 | 22 | iex> Ecdo.Builder.Data.put_in_query ecdo, fn(query) -> from x in query, limit: 10 end 23 | """ 24 | def put_in_query(%{query: query} = ecdo, fun) when is_function(fun), do: %{ecdo | query: fun.(query)} 25 | def put_in_query(%{query: _query} = ecdo, value), do: %{ecdo | query: value} 26 | 27 | @doc """ 28 | Parse field to intermediate structure, where it possible to get some additional 29 | information about this field. 30 | 31 | Output format: {`field name`, `type`, `index in sources`} 32 | 33 | Example: 34 | 35 | iex> Ecdo.Builder.Data.field_ecto("w.id", ecdo) 36 | {:id, :integer, 0} 37 | """ 38 | def field_ecto(strings, %{sources: sources, modules: modules}) when is_list(strings) do 39 | {index, field} = case strings do 40 | [field] -> {0, field} 41 | [key, field] -> {sources[key], field} 42 | end 43 | field = String.to_atom(field) 44 | type = modules[index].__schema__(:type, field) 45 | # May be not cast to atom? 46 | {field, type, index} 47 | end 48 | def field_ecto(string, ecdo) when is_binary(string), 49 | do: String.split(string, ".") |> field_ecto(ecdo) 50 | def field_ecto({:., _, [model, value]}, ecdo), 51 | do: [Macro.to_string(model), to_string(value)] |> field_ecto(ecdo) 52 | 53 | @doc """ 54 | Transform intermediate format, which gives `field_ecto` function to an AST. 55 | """ 56 | def field_ast({field, type, index}, with_type? \\ true) do 57 | ast = (quote do: (&unquote(index)).unquote(field)) 58 | type = if Ecto.Type.primitive?(type) do type else type.type end 59 | if with_type?, 60 | do: put_elem(ast, 1, [ecto_type: type]), 61 | else: ast 62 | end 63 | 64 | @doc """ 65 | As parameters will be saved for ecto in form of count (0, 1) it put count 66 | to an appropriate AST, which ecto waits. 67 | """ 68 | def param_ast(count) do 69 | quote do: ^unquote(count) 70 | end 71 | 72 | @doc """ 73 | Transform AST of keyword to map. 74 | """ 75 | def map_ast(list) do 76 | quote do: %{unquote_splicing(list)} 77 | end 78 | 79 | @doc """ 80 | Get from map a value. If value is nil or there is no key in this map, it returns empty 81 | list. 82 | """ 83 | def get(map, key), do: Map.get(map, key) || [] 84 | 85 | @doc """ 86 | Parse string to otkens. 87 | 88 | Example: 89 | 90 | iex> Ecdo.Builder.Data.tokens("abc.a, abc.b") 91 | ["abc.a", "abc.b"] 92 | """ 93 | def tokens(string) when is_binary(string) do 94 | string |> String.split(",") |> Enum.map(&String.strip/1) 95 | end 96 | def tokens(list) when is_list(list), do: list 97 | def tokens(map) when is_map(map), do: map 98 | end 99 | -------------------------------------------------------------------------------- /lib/ecdo/builder/from.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.From do 2 | @moduledoc """ 3 | Used to build `from` sources 4 | """ 5 | import Kernel, except: [apply: 2] 6 | import Ecto.Query 7 | 8 | @doc false 9 | def apply(%Ecdo{sources: sources, modules: modules, count: count} = ecdo, {name, model}), 10 | do: %{ecdo | sources: Map.put(sources, name, count), 11 | modules: Map.put(modules, count, model), 12 | count: count + 1, 13 | query: from(c in model)} 14 | def apply(%Ecdo{sources: sources, modules: modules, count: count} = ecdo, model), 15 | do: %{ecdo | sources: Map.put(sources, model.__schema__(:source), count), 16 | modules: Map.put(modules, count, model), 17 | count: count + 1, 18 | query: from(c in model)} 19 | end 20 | -------------------------------------------------------------------------------- /lib/ecdo/builder/join.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.Join do 2 | @moduledoc """ 3 | Used to build `join` sources 4 | """ 5 | import Kernel, except: [apply: 2] 6 | import Ecto.Query 7 | use Ecdo.Builder.Data 8 | 9 | @join_direction [:left_join, :right_join, :full_join, :join] 10 | @mapping [left_join: :left, right_join: :right, full_join: :full_join, join: :inner] 11 | 12 | @doc false 13 | def apply(ecdo, query) do 14 | Map.keys(query) 15 | |> Enum.filter(fn(k) -> k in @join_direction end) 16 | |> Enum.reduce(ecdo, &build(&1, &2, query)) 17 | end 18 | 19 | defp build(direction, ecdo, params) do 20 | root = ecdo.modules[0] 21 | associations = root.__schema__(:associations) |> Enum.map(&Atom.to_string(&1)) 22 | Map.get(params, direction) 23 | |> tokens 24 | |> Stream.filter(&(&1 in associations)) 25 | |> Enum.map(&String.to_atom/1) 26 | |> Enum.reduce(ecdo, fn(table, ecdo) -> 27 | join_exp = %Ecto.Query.JoinExpr{ 28 | assoc: {0, table}, 29 | on: %Ecto.Query.QueryExpr{expr: :true, params: []}, 30 | qual: @mapping[direction]} 31 | %{ecdo | sources: Map.put(ecdo.sources, Atom.to_string(table), ecdo.count), 32 | modules: Map.put(ecdo.modules, ecdo.count, assoc(root, table)), 33 | count: ecdo.count + 1, 34 | query: Map.put(ecdo.query, :joins, get(ecdo.query, :joins) ++ [join_exp])} 35 | end) 36 | end 37 | 38 | defp assoc(root, table), do: root.__schema__(:association, table).related 39 | end 40 | -------------------------------------------------------------------------------- /lib/ecdo/builder/load.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.Load do 2 | @moduledoc """ 3 | Used to load sources 4 | """ 5 | import Kernel, except: [apply: 2] 6 | use Ecdo.Builder.Data 7 | 8 | @doc false 9 | def apply(ecdo, content) do 10 | root = ecdo.modules[0] 11 | put_in_query(ecdo, build(root, ecdo.query, content)) 12 | end 13 | 14 | defp build(model, query, %{preload: preload}) when is_list(preload) or is_binary(preload) or is_map(preload) do 15 | preload |> tokens |> Enum.reduce(query, fn(el, q) -> build1(model, q, %{preload: el}) end) 16 | end 17 | defp build(model, query, %{load: list}), do: build(model, query, %{preload: list}) 18 | defp build(_, query, _), do: query 19 | 20 | defp build1(model, query, %{preload: {table, preload_query}}) do 21 | table = to_atom(table) 22 | if table in model.__schema__(:associations) do 23 | preload_query = Ecdo.query({to_string(table), model.__schema__(:association, table).queryable}, preload_query) 24 | from(x in query, preload: [{^table, ^preload_query.query}]) 25 | else 26 | query 27 | end 28 | end 29 | 30 | defp build1(model, query, %{preload: table}) do 31 | table = to_atom(table) 32 | if table in model.__schema__(:associations) do 33 | from(x in query, preload: ^table) 34 | else 35 | query 36 | end 37 | end 38 | 39 | defp to_atom(table) when is_atom(table), do: table 40 | defp to_atom(table) when is_binary(table), do: String.to_atom(table) 41 | defp to_atom(table) when is_list(table), do: String.to_atom(to_string(table)) 42 | end 43 | -------------------------------------------------------------------------------- /lib/ecdo/builder/order_by.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.OrderBy do 2 | @moduledoc """ 3 | Used to build 'order_by' 4 | """ 5 | 6 | use Ecdo.Builder.Data 7 | 8 | @doc false 9 | def apply(ecdo, %{order_by: order_by}) do 10 | order_by |> tokens |> Enum.reduce(ecdo, &build(&2, &1)) 11 | end 12 | def apply(ecdo, _query), do: ecdo 13 | 14 | def build(ecdo, order_by_content) do 15 | [field | next] = String.split(order_by_content, ":") 16 | direction = case next do 17 | [] -> :asc 18 | [direction] -> direction |> String.to_atom 19 | end 20 | field_ast = field_ecto(field, ecdo) |> field_ast() 21 | query_expr = %Ecto.Query.QueryExpr{expr: [{direction, field_ast}], params: []} 22 | %{ecdo | query: Map.put(ecdo.query, :order_bys, get(ecdo.query, :order_bys) ++[query_expr])} 23 | end 24 | 25 | def quoted_expr(direction, index, field) do 26 | {direction, {{:., [], [{:&, [], [index]}, field]}, [], []}} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ecdo/builder/query_expr.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.QueryExpr do 2 | @moduledoc """ 3 | Add to query limit, offset and disticts 4 | """ 5 | use Ecdo.Builder.Data 6 | import Ecto.Query 7 | @supported_expr [:limit, :distinct, :offset] 8 | 9 | @doc false 10 | def apply(ecdo, content) do 11 | put_in_query ecdo, fn(query) -> Enum.reduce(@supported_expr, query, &expr(&2, content, &1)) end 12 | end 13 | 14 | defp expr(query, content, query_field) do 15 | case content[query_field] do 16 | nil -> query 17 | value -> eval(query, query_field, value) 18 | end 19 | end 20 | 21 | # don't generate code for distinct with true and false because it works incorrectly 22 | defp eval(query, :distinct, true), do: from(x in query, distinct: true) 23 | defp eval(query, :distinct, false), do: from(x in query, distinct: false) 24 | defp eval(query, :distinct, "true"), do: from(x in query, distinct: true) 25 | defp eval(query, :distinct, "false"), do: from(x in query, distinct: false) 26 | for key <- @supported_expr do 27 | defp eval(query, unquote(key), value), do: from(x in query, [{unquote(key), ^value}]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ecdo/builder/select.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.Select do 2 | @moduledoc """ 3 | Add to query select 4 | """ 5 | use Ecdo.Builder.Data 6 | 7 | @aggr_funs [:count, :avg, :sum, :min, :max] 8 | 9 | @doc false 10 | def apply(ecdo, content) do 11 | case build_select(content, ecdo) do 12 | [] -> ecdo 13 | select -> put_in_query(ecdo, &(%{&1 | select: select})) 14 | end 15 | end 16 | 17 | defp build_select(content, ecdo) do 18 | select = Map.get(content, :select) || "" 19 | select_as = content[:select_as] || :map 20 | ast = select 21 | |> tokens 22 | |> add_funs(content) 23 | |> Enum.map(&transform(&1, ecdo, select_as)) 24 | case ast do 25 | [] -> [] 26 | _ -> %SelectExpr{expr: to_expr(ast, select_as)} 27 | end 28 | end 29 | 30 | defp transform({fun, arg}, ecdo, select_as), 31 | do: {fun, nil, [field_ecto(arg, ecdo) |> field_ast()]} |> maybe_map(fun, select_as) 32 | defp transform(value, ecdo, select_as), 33 | do: field_ecto(value, ecdo) |> field_ast() |> maybe_map(value, select_as) 34 | 35 | defp maybe_map(field_ast, value, select_as) do 36 | case select_as do 37 | is when is in [:map, :keyword] -> {value, field_ast} 38 | is when is in [:list, :one] -> field_ast 39 | end 40 | end 41 | 42 | defp funs(params) do 43 | for {p, vals} <- params, p in @aggr_funs do 44 | {p, vals} 45 | end 46 | end 47 | 48 | defp add_funs([""], content), do: funs(content) 49 | defp add_funs(select, content), do: select ++ funs(content) 50 | 51 | defp to_expr(ast, :map), do: map_ast(ast) 52 | defp to_expr([ast], :one), do: ast 53 | defp to_expr(ast, _), do: ast 54 | end 55 | -------------------------------------------------------------------------------- /lib/ecdo/builder/where.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Builder.Where do 2 | @moduledoc """ 3 | Analyse where conditions and add to a query 4 | """ 5 | use Ecdo.Builder.Data 6 | 7 | require Record 8 | Record.defrecordp :params, [dot: false, values: [], count: 0, last: nil, operator: :default] 9 | 10 | @doc false 11 | def apply(ecdo, %{where: where}), do: put_in_query(ecdo, &(%{&1 | wheres: build(where, ecdo)})) 12 | def apply(ecdo, _), do: ecdo 13 | 14 | def build(where, ecdo) when is_binary(where) do 15 | {:ok, conditions} = Code.string_to_quoted(where) 16 | conditions |> List.wrap |> build_ast(ecdo) 17 | end 18 | def build(where, ecdo) when is_list(where) do 19 | Enum.map(where, &build_conditions(&1, ecdo)) 20 | end 21 | 22 | @like_functions [:like, :ilike] 23 | @type_operators [:==, :>, :<, :!=, :>=, :<=] 24 | @allowed_operations [:not, :or, :and | @type_operators] 25 | @allowed_operations_with_funs @like_functions ++ @allowed_operations 26 | 27 | defp build_conditions({op, _, _} = opspec, ecdo) when op in @allowed_operations_with_funs do 28 | %QueryExpr{expr: op_ast(opspec, ecdo)} 29 | end 30 | defp build_conditions(where, ecdo) when is_binary(where), 31 | do: List.first build(where, ecdo) 32 | 33 | defp op_ast({op, left, right}, ecdo) do 34 | # Build from condition list an AST 35 | field_ast = case is_binary(left) do 36 | true -> field_ecto(left, ecdo) |> field_ast() 37 | false -> op_ast(left, ecdo) 38 | end 39 | quote do: unquote(op)(unquote_splicing([field_ast, op_ast(right, ecdo)])) 40 | end 41 | defp op_ast(other, _ecdo), do: other 42 | 43 | defp build_ast([ast], ecdo) do 44 | {expr, params(values: values)} = Macro.prewalk(ast, params(), &to_ecto_ast(&1, &2, ecdo)) 45 | [%QueryExpr{expr: expr, params: Enum.reverse(values)}] 46 | end 47 | 48 | defp to_ecto_ast({op, _, _} = opast, params, _ecdo) when op in @allowed_operations, 49 | do: {opast, params(params, operator: :default)} 50 | defp to_ecto_ast({op, _, _} = opast, params, _ecdo) when op in @like_functions, 51 | do: {opast, params(params, operator: :function)} 52 | defp to_ecto_ast({{:., _, _} = field, _, _}, params(operator: operator) = params, ecdo) do 53 | # We ignore the outer AST, because field_ast add wrapper back 54 | {field, _type, _index} = field_spec = field_ecto(field, ecdo) 55 | # Next element in form of {atom, _, nil} is a first part of atom.value call, and should be ignored 56 | {field_ast(field_spec, operator == :default), params(params, last: field, dot: true)} 57 | end 58 | defp to_ecto_ast({_, _, nil} = other, params(dot: true) = params, _ecdo), do: {other, params(params, dot: false)} 59 | # If it is not a part of atom.value (dot: false), than it should be transformed to field 60 | defp to_ecto_ast({field_name, _, nil}, params(dot: false, operator: operator) = params, ecdo) do 61 | {field, _type, _index} = field_spec = field_name |> to_string() |> field_ecto(ecdo) 62 | # We do not need to set type, if operator is like, we set operator to :function, if we see like 63 | {field_ast(field_spec, operator == :default), params(params, last: field)} 64 | end 65 | defp to_ecto_ast(string, params(operator: :default, last: field, values: values) = params, ecdo) when is_binary(string) do 66 | {_, _, count} = field_ecto([string], ecdo) 67 | # as string i our parameter, we should replace it with name of the field and count 68 | new_params = params(params, last: nil, count: count, values: [{string, {count, field}} | values]) 69 | {param_ast(count), new_params} 70 | end 71 | defp to_ecto_ast(other, acc, _ecdo) do 72 | {other, acc} 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :ecdo, 6 | version: "0.1.4", 7 | build_embedded: Mix.env == :prod, 8 | start_permanent: Mix.env == :prod, 9 | test_coverage: [tool: Coverex.Task, coveralls: true], 10 | deps: deps, 11 | description: description, 12 | package: package] 13 | end 14 | 15 | def application do 16 | [applications: [:logger, :ecto]] 17 | end 18 | 19 | defp deps do 20 | [{:mariaex, ">= 0.0.0", optional: true}, 21 | {:postgrex, ">= 0.0.0", optional: true}, 22 | {:ecto, "~> 1.0.0"}, 23 | {:coverex, "~> 1.4.1", only: :test}] 24 | end 25 | 26 | defp description do 27 | "Ecdo is a dynamic interface for ecto aims to simplify building dynamic query API based on ecto models." 28 | end 29 | 30 | defp package do 31 | [contributors: ["Dmitry Russ(Aleksandrov)", "Yury Gargay"], 32 | links: %{"Github" => "https://github.com/xerions/ecdo"}] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"coverex": {:hex, :coverex, "1.4.3"}, 2 | "decimal": {:hex, :decimal, "1.1.0"}, 3 | "ecto": {:hex, :ecto, "1.0.3"}, 4 | "hackney": {:hex, :hackney, "1.3.2"}, 5 | "httpoison": {:hex, :httpoison, "0.7.2"}, 6 | "idna": {:hex, :idna, "1.0.2"}, 7 | "mariaex": {:hex, :mariaex, "0.4.3"}, 8 | "poison": {:hex, :poison, "1.4.0"}, 9 | "poolboy": {:hex, :poolboy, "1.5.1"}, 10 | "postgrex": {:hex, :postgrex, "0.9.1"}, 11 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}} 12 | -------------------------------------------------------------------------------- /test/ecdo_query_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Integration.QueryTest do 2 | use Ecto.Integration.Case 3 | 4 | import Ecto.Query 5 | import Ecdo 6 | 7 | alias Ecto.Integration.Post 8 | alias Ecto.Integration.Tag 9 | alias Ecto.Integration.Permalink 10 | alias Ecto.Integration.Comment 11 | alias Ecto.Integration.Custom 12 | alias Ecto.Integration.TestRepo 13 | alias Ecto.Integration.PoolRepo 14 | 15 | test "query without select" do 16 | p1 = TestRepo.insert!(%Post{title: "1"}) 17 | query = query({"p", Post}, %{where: "p.title == 1"} ) 18 | assert [p1] = TestRepo.all(query) 19 | end 20 | 21 | test "joins" do 22 | p1 = TestRepo.insert!(%Post{text: "text", title: "1"}) 23 | p2 = TestRepo.insert!(%Post{title: "2"}) 24 | c1 = TestRepo.insert!(%Permalink{url: "1", post_id: p2.id}) 25 | c2 = TestRepo.insert!(%Comment{text: "a", post_id: p2.id}) 26 | 27 | query = query({"p", Post}, %{join: ["permalink"], order_by: "id", select: "p.title,permalink.url", select_as: :list} ) 28 | assert [["2", "1"]] == TestRepo.all(query) 29 | 30 | query = query({"p", Post}, %{join: "permalink", order_by: "id", select: "p.title,permalink.url", select_as: :list} ) 31 | assert [["2", "1"]] == TestRepo.all(query) 32 | 33 | # try to join unavailable talble 34 | query = query({"p", Post}, %{join: ["permalink", "abc123"], order_by: "p.id", select: "p.title,permalink.url", select_as: :list} ) 35 | assert [["2", "1"]] == TestRepo.all(query) 36 | query = query({"p", Post}, %{join: "permalink, abc123", order_by: "p.id", select: ["p.title", "permalink.url"], select_as: :list} ) 37 | assert [["2", "1"]] == TestRepo.all(query) 38 | 39 | query = query({"p", Post}, %{left_join: ["permalink"], order_by: "p.id", select: "p.title,permalink.url", select_as: :list} ) 40 | assert [["1", nil], ["2", "1"]] == TestRepo.all(query) 41 | 42 | # sort by joined table 43 | c3 = TestRepo.insert!(%Permalink{url: "2", post_id: p2.id}) 44 | query = query({"p", Post}, %{left_join: ["permalink"], order_by: "permalink.url", select: "p.title,permalink.url", select_as: :list} ) 45 | assert [["1", nil], ["2", "1"], ["2", "2"]] == TestRepo.all(query) 46 | 47 | # multiple join 48 | query = query({"p", Post}, %{join: ["permalink", "comments"], select: "p.title,permalink.url,comments.text", select_as: :list} ) 49 | assert [["2", "1", "a"], ["2", "2", "a"]] == TestRepo.all(query) 50 | 51 | # multiple orderby 52 | query = query({"p", Post}, %{join: ["permalink"], order_by: "id,permalink.url:desc", select: "p.title,permalink.url", select_as: :list} ) 53 | assert [["2", "2"], ["2", "1"]] == TestRepo.all(query) 54 | query = query({"p", Post}, %{join: ["permalink"], order_by: ["id", "permalink.url:desc"], select: "p.title,permalink.url", select_as: :list} ) 55 | assert [["2", "2"], ["2", "1"]] == TestRepo.all(query) 56 | 57 | TestRepo.insert!(%Comment{text: p1.text, post_id: p1.id}) 58 | TestRepo.insert!(%Comment{text: p1.text, lock_version: 2, post_id: p1.id}) 59 | query = query({"p", Post}, %{select: "text, comments.lock_version", 60 | where: "p.text == comments.text and comments.lock_version == 2", 61 | join: ["comments"], select_as: :one}) 62 | assert [p1.text, 2] == TestRepo.one(query) 63 | 64 | query = query(Post, %{"where" => "text == \"test1\" or text == \"test2\""}) 65 | result = TestRepo.all(query) 66 | assert [] = result 67 | end 68 | 69 | test "funs" do 70 | for i <- 1..3 do 71 | p = TestRepo.insert!(%Post{title: "test", visits: i}) 72 | TestRepo.insert!(%Permalink{url: "test_url", post_id: p.id}) 73 | end 74 | 75 | query = query({"p", Post}, %{where: "title == \"test\"", count: "id", select_as: :one} ) 76 | assert TestRepo.one(from(p in Post, where: p.title == "test", select: count(p.id))) == TestRepo.one(query) 77 | 78 | # with join 79 | query = query({"p", Post}, %{join: ["permalink"], where: "title == \"test\"", count: "permalink.url", select_as: :one} ) 80 | assert TestRepo.one(from(p in Post, join: permalink in assoc(p, :permalink), 81 | where: p.title == "test", 82 | select: count(permalink.url))) == TestRepo.one(query) 83 | 84 | query = query({"p", Post}, %{where: "title == \"test\"", max: "visits", select_as: :one} ) 85 | assert TestRepo.one(from(p in Post, where: p.title == "test", select: max(p.visits))) == TestRepo.one(query) 86 | 87 | query = query({"p", Post}, %{where: "title == \"test\"", min: "visits", select_as: :one} ) 88 | assert TestRepo.one(from(p in Post, where: p.title == "test", select: min(p.visits))) == TestRepo.one(query) 89 | 90 | query = query({"p", Post}, %{where: "title == \"test\"", avg: "visits", select_as: :one} ) 91 | assert TestRepo.one(from(p in Post, where: p.title == "test", select: avg(p.visits))) == TestRepo.one(query) 92 | end 93 | 94 | test "limit, offset and distinct" do 95 | for i <- 1..4, do: TestRepo.insert!(%Post{title: "test_expr", visits: i}) 96 | 97 | query = query({"p", Post}, %{select: "id,title", limit: "2", where: "title == \"test_expr\"", select_as: :list}) 98 | assert TestRepo.all(from(p in Post, select: [p.id, p.title], limit: 2, where: p.title == "test_expr")) == TestRepo.all(query) 99 | 100 | query = query({"p", Post}, %{select: "id,title", limit: "2", offset: 2, where: "title == \"test_expr\"", select_as: :list}) 101 | assert TestRepo.all(from(p in Post, select: [p.id, p.title], limit: 2, offset: 2, where: p.title == "test_expr")) == TestRepo.all(query) 102 | 103 | query = query({"p", Post}, %{select: "id,title", limit: 2, offset: 4, where: "title == \"test_expr\"", select_as: :list}) 104 | assert TestRepo.all(from(p in Post, select: [p.id, p.title], limit: 2, offset: 4, where: p.title == "test_expr")) == TestRepo.all(query) 105 | 106 | query = query({"p", Post}, %{select: "id", distinct: true, where: "title == \"test_expr\"", select_as: :one}) 107 | assert TestRepo.all(from(p in Post, select: p.id, distinct: true, where: p.title == "test_expr")) == TestRepo.all(query) 108 | 109 | query = query({"p", Post}, %{select: "id", distinct: "true", where: "title == \"test_expr\"", select_as: :one}) 110 | assert TestRepo.all(from(p in Post, select: p.id, distinct: true, where: p.title == "test_expr")) == TestRepo.all(query) 111 | 112 | query = query({"p", Post}, %{select: "id", distinct: "false", where: "title == \"test_expr\"", select_as: :one}) 113 | assert TestRepo.all(from(p in Post, select: p.id, distinct: false, where: p.title == "test_expr")) == TestRepo.all(query) 114 | 115 | query = query({"p", Post}, %{select: "id", distinct: false, where: "title == \"test_expr\"", select_as: :one}) 116 | assert TestRepo.all(from(p in Post, select: p.id, distinct: false, where: p.title == "test_expr")) == TestRepo.all(query) 117 | end 118 | 119 | test "load" do 120 | p = TestRepo.insert!(%Post{title: "test_load"}) 121 | TestRepo.insert!(%Permalink{url: "test_load_url", post_id: p.id}) 122 | TestRepo.insert!(%Comment{text: "test_load_comment1", post_id: p.id}) 123 | p = TestRepo.insert!(%Post{title: "test_load2"}) 124 | TestRepo.insert!(%Comment{text: "test_load_comment2", post_id: p.id}) 125 | 126 | query = query({"p", Post}, %{where: "title == \"test_load\"", load: ["permalink"]}) 127 | post = TestRepo.one(query) 128 | assert post.title == "test_load" 129 | assert post.permalink.url == "test_load_url" 130 | 131 | query = query({"p", Post}, %{where: "title == \"test_load\"", load: ['permalink', :comments, :not_exists]}) 132 | post = TestRepo.one(query) 133 | assert post.title == "test_load" 134 | assert post.permalink.url == "test_load_url" 135 | assert hd(post.comments).text == "test_load_comment1" 136 | query = query({"p", Post}, %{where: "title == \"test_load\"", load: "permalink, comments"}) 137 | assert post == TestRepo.one(query) 138 | 139 | query = query({"p", Post}, %{load: [{"comments", %{where: "text == \"test_load_comment1\""}}, {:not_exists, %{}}]}) 140 | TestRepo.insert!(%Comment{text: "test_load_comment1_1", post_id: p.id}) 141 | 142 | [post1, post2] = TestRepo.all(query) 143 | [comments1] = post1.comments 144 | assert comments1.text == "test_load_comment1" 145 | assert post2.comments == [] 146 | end 147 | 148 | test "preload query" do 149 | p = TestRepo.insert!(%Post{title: "test_load_query"}) 150 | TestRepo.insert!(%Comment{text: "test_load_query_comment1", post_id: p.id}) 151 | TestRepo.insert!(%Comment{text: "test_load_query_comment2", post_id: p.id}) 152 | 153 | query = query({"p", Post}, %{"load" => %{"comments" => %{"where" => "text == \"test_load_query_comment1\""}}}) 154 | 155 | [post1] = TestRepo.all(query) 156 | [comments1] = post1.comments 157 | assert comments1.text == "test_load_query_comment1" 158 | end 159 | 160 | test "like" do 161 | TestRepo.insert!(%Post{title: "abcd", visits: 10}) 162 | TestRepo.insert!(%Post{title: "adee", visits: 100}) 163 | 164 | query = query({"p", Post}, %{select: "title", where: "like(title, \"%bc%\")", select_as: :one} ) 165 | assert "abcd" == TestRepo.one(query) 166 | 167 | query = query({"p", Post}, %{select: "title", where: "like(title, \"%bc%\") or like(title, \"%de%\")or like(title, \"%d%\")", select_as: :list} ) 168 | assert [["abcd"], ["adee"]] == TestRepo.all(query) 169 | 170 | query = query({"p", Post}, %{select: "title", where: [{:or, {:or, {:like, "title", "%bc%"}, {:like, "title", "%de%"}}, 171 | {:like, "title", "%d%"}}], select_as: :list} ) 172 | assert [["abcd"], ["adee"]] == TestRepo.all(query) 173 | 174 | query = query({"p", Post}, %{select: "title", where: ["visits > 50", 175 | {:or, {:or, {:like, "title", "%bc%"}, {:like, "title", "%de%"}}, 176 | {:like, "title", "%d%"}}], select_as: :list} ) 177 | assert [["adee"]] == TestRepo.all(query) 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /test/ecdo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule City do 2 | use Ecto.Model 3 | 4 | schema "city" do 5 | field :name, :string 6 | field :country, :string 7 | has_many :weathers, Weather 8 | end 9 | end 10 | 11 | defmodule Weather do 12 | use Ecto.Model 13 | 14 | schema "weather" do 15 | belongs_to :city, City 16 | field :name, :string 17 | field :temp_lo, :integer 18 | field :temp_hi, :integer 19 | field :meta, :map 20 | field :prcp, :float, default: 0.0 21 | timestamps 22 | end 23 | end 24 | 25 | import Ecto.Query 26 | import Ecdo 27 | 28 | defmodule EcdoTest do 29 | use ExUnit.Case 30 | doctest Ecdo 31 | 32 | test "basic query check" do 33 | assert from(w in Weather) == query({"w", Weather}, %{}).query 34 | # basic list interface 35 | assert inspect(from(w in Weather, where: like(w.name, "aa"))) == inspect(query({"w", Weather}, %{where: [{:like, "w.name", "aa"}]}).query) 36 | 37 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query({"w", Weather}, %{where: [{:==, "w.temp_lo", 20}]}).query) 38 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query(Weather, %{where: [{:==, "temp_lo", 20}]}).query) 39 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query(Weather, %{where: [{:==, "weather.temp_lo", 20}]}).query) 40 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query({"w", Weather}, %{"where" => "w.temp_lo == 20"}).query) 41 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query({"w", Weather}, %{"where" => "w.temp_lo == 20", unknow_key: 1}).query) 42 | assert inspect(from(w in Weather, where: w.temp_lo == 20)) == inspect(query({"w", Weather}, %{"where" => "w.temp_lo == 20", "unknow_key" => 1}).query) 43 | 44 | assert inspect(from(w in Weather, where: like(w.name,"abc") or like(w.name, "fff") or like(w.name, "ccc"))) 45 | == inspect(query({"w", Weather}, %{"where" => [{:or, {:or, {:like, "w.name", "abc"}, {:like, "w.name", "fff"}}, 46 | {:like, "w.name", "ccc"}}]}).query) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/ecdo_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecdo.Integration.TypeTest do 2 | use Ecto.Integration.Case 3 | 4 | import Ecto.Query 5 | import Ecdo 6 | 7 | alias Ecto.Integration.Post 8 | alias Ecto.Integration.Tag 9 | alias Ecto.Integration.Custom 10 | alias Ecto.Integration.TestRepo, as: Repo 11 | alias Ecto.Integration.PoolRepo 12 | 13 | test "primitive types" do 14 | integer = 1 15 | float = 0.1 16 | text = <<0,1>> 17 | uuid = "00010203-0405-0607-0809-0a0b0c0d0e0f" 18 | datetime = %Ecto.DateTime{year: 2014, month: 1, day: 16, 19 | hour: 20, min: 26, sec: 51, usec: 0} 20 | 21 | Repo.insert!(%Post{text: text, public: true, visits: integer, uuid: uuid, 22 | counter: integer, inserted_at: datetime, intensity: float}) 23 | 24 | # ID 25 | assert [1] = Repo.all query({"p", Post}, %{where: [{:==, "p.counter", integer}], select: "p.counter", select_as: :one}) 26 | assert [1] = Repo.all query({"p", Post}, %{where: "p.counter == 1", select: "p.counter", select_as: :one}) 27 | assert [1] = Repo.all query({"p", Post}, %{where: "counter == 1", select: "p.counter", select_as: :one}) 28 | 29 | # Integers 30 | assert [1] = Repo.all query({"p", Post}, %{where: [{:==, "p.visits", integer}], select: "p.visits", select_as: :one}) 31 | assert [1] = Repo.all query({"p", Post}, %{where: "p.visits == 1", select: "p.visits", select_as: :one}) 32 | 33 | # Floats 34 | assert [0.1] = Repo.all query({"p", Post}, %{where: [{:==, "p.intensity", float}], select: "p.intensity", select_as: :one}) 35 | assert [0.1] = Repo.all query({"p", Post}, %{where: "p.intensity == 0.1", select: "p.intensity", select_as: :one}) 36 | 37 | # Booleans 38 | assert [true] = Repo.all query({"p", Post}, %{where: [{:==, "p.public", true}], select: "p.public", select_as: :one}) 39 | assert [true] = Repo.all query({"p", Post}, %{where: "p.public == true", select: "p.public", select_as: :one}) 40 | 41 | # Binaries 42 | assert [^text] = Repo.all query({"p", Post}, %{where: [{:==, "p.text", <<0,1>>}], select: "p.text", select_as: :one}) 43 | # Do not work 44 | # assert [^text] = Repo.all query([{"p", Post}], %{where: "p.text == <<0,1>>", select: "p.text", select_as: :one}) 45 | 46 | # UUID 47 | assert [^uuid] = Repo.all query({"p", Post}, %{where: "p.uuid == \"#{uuid}\"", select: "p.uuid", select_as: :one}) 48 | 49 | # Datetime 50 | assert [^datetime] = Repo.all query({"p", Post}, %{where: "p.inserted_at == \"#{Ecto.DateTime.to_iso8601(datetime)}\"", select: "p.inserted_at", select_as: :one}) 51 | end 52 | 53 | # test "binary id type" do 54 | # assert %Custom{} = custom = Repo.insert!(%Custom{}) 55 | # bid = custom.bid 56 | # assert [^bid] = Repo.all(from c in Custom, select: c.bid) 57 | # assert [^bid] = Repo.all(from c in Custom, select: type(^bid, :binary_id)) 58 | # end 59 | # 60 | test "composite types in select" do 61 | assert %Post{} = Repo.insert!(%Post{title: "1", text: "hai"}) 62 | 63 | assert [["1", "hai"]] == Repo.all query({"p", Post}, %{select: "p.title,p.text", select_as: :one}) 64 | 65 | assert [[{"p.title", "1"}, {"p.text", "hai"}]] == Repo.all query({"p", Post}, %{select: "p.title,p.text", select_as: :keyword}) 66 | 67 | assert [%{"p.title" => "1", "p.text" => "hai"}] == Repo.all query({"p", Post}, %{select: "p.title,p.text", select_as: :map}) 68 | 69 | #assert [%Post{}] == Repo.all query([{"p", Post}], %{select: "p"}) 70 | # TODO: define, if we need such complex case 71 | #assert [%{:title => "1", 3 => "hai", "text" => "hai"}] == 72 | # Repo.all(from p in Post, select: %{ 73 | # :title => p.title, 74 | # "text" => p.text, 75 | # 3 => p.text 76 | # }) 77 | end 78 | 79 | @tag :map_type 80 | test "map type" do 81 | post1 = Repo.insert!(%Post{meta: %{"foo" => "bar", "baz" => "bat"}}) 82 | post2 = Repo.insert!(%Post{meta: %{foo: "bar", baz: "bat"}}) 83 | 84 | assert Repo.all(query({"p", Post}, %{where: "p.id == #{post1.id}", select: "p.meta", select_as: :one})) == 85 | [%{"foo" => "bar", "baz" => "bat"}] 86 | 87 | assert Repo.all(query({"p", Post}, %{where: "p.id == #{post2.id}", select: "p.meta", select_as: :one})) == 88 | [%{"foo" => "bar", "baz" => "bat"}] 89 | end 90 | 91 | @tag :decimal_type 92 | test "decimal type" do 93 | decimal = Decimal.new("1.0") 94 | 95 | Repo.insert!(%Post{cost: decimal}) 96 | 97 | assert [^decimal] = Repo.all query({"p", Post}, %{where: "p.cost == 1.0", select: "p.cost", select_as: :one}) 98 | assert [^decimal] = Repo.all query({"p", Post}, %{where: "p.cost == 1", select: "p.cost", select_as: :one}) 99 | assert [^decimal] = Repo.all query({"p", Post}, %{where: [{:==, "p.cost", 1.0}], select: "p.cost", select_as: :one}) 100 | assert [^decimal] = Repo.all query({"p", Post}, %{where: [{:==, "p.cost", 1}], select: "p.cost", select_as: :one}) 101 | 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | ExUnit.start exclude: [:array_type, :read_after_writes, :case_sensitive, 3 | :uses_usec, :strict_savepoint] 4 | 5 | # Configure Ecto for support and tests 6 | Application.put_env(:ecto, :primary_key_type, :id) 7 | 8 | # Load support files 9 | Code.require_file "../deps/ecto/integration_test/support/repo.exs", __DIR__ 10 | Code.require_file "../deps/ecto/integration_test/support/models.exs", __DIR__ 11 | Code.require_file "../deps/ecto/integration_test/support/migration.exs", __DIR__ 12 | 13 | # Basic test repo 14 | alias Ecto.Integration.TestRepo 15 | 16 | Application.put_env(:ecto, TestRepo, 17 | adapter: Ecto.Adapters.MySQL, 18 | url: "ecto://root@localhost/ecto_test", 19 | pool: Ecto.Adapters.SQL.Sandbox) 20 | 21 | defmodule Ecto.Integration.TestRepo do 22 | use Ecto.Integration.Repo, otp_app: :ecto 23 | end 24 | 25 | # Pool repo for transaction and lock tests 26 | alias Ecto.Integration.PoolRepo 27 | 28 | Application.put_env(:ecto, PoolRepo, 29 | adapter: Ecto.Adapters.MySQL, 30 | url: "ecto://root@localhost/ecto_test", 31 | pool_size: 10) 32 | 33 | defmodule Ecto.Integration.PoolRepo do 34 | use Ecto.Integration.Repo, otp_app: :ecto 35 | end 36 | 37 | defmodule Ecto.Integration.Case do 38 | use ExUnit.CaseTemplate 39 | 40 | setup_all do 41 | Ecto.Adapters.SQL.begin_test_transaction(TestRepo, []) 42 | on_exit fn -> Ecto.Adapters.SQL.rollback_test_transaction(TestRepo, []) end 43 | :ok 44 | end 45 | 46 | setup do 47 | Ecto.Adapters.SQL.restart_test_transaction(TestRepo, []) 48 | :ok 49 | end 50 | end 51 | 52 | # Load up the repository, start it, and run migrations 53 | _ = Ecto.Storage.down(TestRepo) 54 | :ok = Ecto.Storage.up(TestRepo) 55 | 56 | {:ok, _pid} = TestRepo.start_link 57 | {:ok, _pid} = PoolRepo.start_link 58 | 59 | :ok = Ecto.Migrator.up(TestRepo, 0, Ecto.Integration.Migration, log: false) 60 | --------------------------------------------------------------------------------