├── .gitignore ├── examples └── simple │ ├── .gitignore │ ├── config │ └── config.exs │ ├── mix.exs │ ├── priv │ └── repo │ │ └── migrations │ │ └── 20150115192936_create_weather.exs │ ├── README.md │ └── lib │ └── simple.ex ├── test ├── ecto │ ├── query │ │ ├── builder │ │ │ ├── from_test.exs │ │ │ ├── limit_offset_test.exs │ │ │ ├── lock_test.exs │ │ │ ├── distinct_test.exs │ │ │ ├── group_by_test.exs │ │ │ ├── join_test.exs │ │ │ ├── preload_test.exs │ │ │ ├── select_test.exs │ │ │ └── order_by_test.exs │ │ ├── builder_test.exs │ │ └── inspect_test.exs │ ├── adapters │ │ ├── sql_test.exs │ │ └── sql │ │ │ └── worker_test.exs │ ├── uuid_test.exs │ ├── type_test.exs │ ├── model │ │ ├── timestamps_test.exs │ │ └── callbacks_test.exs │ ├── datetime_test.exs │ ├── repo │ │ └── config_test.exs │ ├── repo_test.exs │ └── migration_test.exs ├── test_helper.exs ├── support │ ├── eval_helpers.exs │ ├── types.exs │ ├── file_helpers.exs │ └── mock_repo.exs └── mix │ ├── tasks │ ├── ecto.migrate_test.exs │ ├── ecto.rollback_test.exs │ ├── ecto.gen.migration_test.exs │ ├── ecto.gen.repo_test.exs │ └── ecto.create_drop_test.exs │ └── ecto_test.exs ├── integration_test ├── mysql │ ├── all_test.exs │ ├── storage_test.exs │ └── test_helper.exs ├── pg │ ├── all_test.exs │ ├── storage_test.exs │ └── test_helper.exs ├── cases │ ├── sql_escape.exs │ ├── lock.exs │ ├── transaction.exs │ └── migration.exs └── support │ ├── migration.exs │ └── models.exs ├── mix.lock ├── .travis.yml ├── lib ├── ecto │ ├── adapter │ │ ├── transactions.ex │ │ ├── storage.ex │ │ └── migrations.ex │ ├── storage.ex │ ├── queryable.ex │ ├── migration │ │ ├── schema_migration.ex │ │ └── runner.ex │ ├── query │ │ ├── builder │ │ │ ├── where.ex │ │ │ ├── having.ex │ │ │ ├── group_by.ex │ │ │ ├── lock.ex │ │ │ ├── limit_offset.ex │ │ │ ├── distinct.ex │ │ │ ├── select.ex │ │ │ ├── from.ex │ │ │ ├── order_by.ex │ │ │ ├── preload.ex │ │ │ └── join.ex │ │ └── inspect.ex │ ├── model │ │ └── timestamps.ex │ ├── repo │ │ ├── config.ex │ │ ├── assoc.ex │ │ ├── model.ex │ │ ├── preloader.ex │ │ └── queryable.ex │ ├── uuid.ex │ ├── adapters │ │ ├── mysql.ex │ │ ├── sql │ │ │ └── connection.ex │ │ └── postgres.ex │ ├── exceptions.ex │ ├── adapter.ex │ ├── model.ex │ └── migrator.ex └── mix │ ├── tasks │ ├── ecto.drop.ex │ ├── ecto.create.ex │ ├── ecto.migrate.ex │ ├── ecto.rollback.ex │ ├── ecto.gen.repo.ex │ └── ecto.gen.migration.ex │ └── ecto.ex ├── mix.exs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | /tmp 5 | erl_crash.dump 6 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /test/ecto/query/builder/from_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.FromTest do 2 | use ExUnit.Case, async: true 3 | import Ecto.Query.Builder.From 4 | doctest Ecto.Query.Builder.From 5 | end 6 | -------------------------------------------------------------------------------- /examples/simple/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :simple, Simple.Repo, 4 | database: "ecto_simple", 5 | username: "postgres", 6 | password: "postgres", 7 | hostname: "localhost" 8 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # For tasks/generators testing 2 | Mix.start() 3 | Mix.shell(Mix.Shell.Process) 4 | System.put_env("ECTO_EDITOR", "") 5 | Logger.configure(level: :info) 6 | 7 | # Commonly used support feature 8 | Code.require_file "support/file_helpers.exs", __DIR__ 9 | 10 | ExUnit.start() 11 | -------------------------------------------------------------------------------- /integration_test/mysql/all_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../cases/lock.exs", __DIR__ 2 | Code.require_file "../cases/migration.exs", __DIR__ 3 | Code.require_file "../cases/repo.exs", __DIR__ 4 | Code.require_file "../cases/preloads.exs", __DIR__ 5 | Code.require_file "../cases/sql_escape.exs", __DIR__ 6 | Code.require_file "../cases/transaction.exs", __DIR__ 7 | -------------------------------------------------------------------------------- /integration_test/pg/all_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../cases/lock.exs", __DIR__ 2 | Code.require_file "../cases/migration.exs", __DIR__ 3 | Code.require_file "../cases/repo.exs", __DIR__ 4 | Code.require_file "../cases/preloads.exs", __DIR__ 5 | Code.require_file "../cases/sql_escape.exs", __DIR__ 6 | Code.require_file "../cases/transaction.exs", __DIR__ 7 | -------------------------------------------------------------------------------- /test/ecto/adapters/sql_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapters.SQLTest do 2 | use ExUnit.Case, async: true 3 | 4 | defmodule Adapter do 5 | use Ecto.Adapters.SQL 6 | end 7 | 8 | defmodule Repo do 9 | use Ecto.Repo, adapter: Adapter, otp_app: :ecto 10 | end 11 | 12 | test "stores __pool__ metadata" do 13 | assert Repo.__pool__ == Repo.Pool 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/support/eval_helpers.exs: -------------------------------------------------------------------------------- 1 | defmodule Support.EvalHelpers do 2 | @doc """ 3 | Delay the evaluation of the code snippet so 4 | we can verify compile time behaviour via eval. 5 | """ 6 | defmacro quote_and_eval(quoted, binding \\ []) do 7 | quoted = Macro.escape(quoted) 8 | quote do 9 | Code.eval_quoted(unquote(quoted), unquote(binding), __ENV__) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/simple/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Simple.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :simple, 6 | version: "0.0.1", 7 | deps: deps] 8 | end 9 | 10 | def application do 11 | [mod: {Simple.App, []}, 12 | applications: [:postgrex, :ecto]] 13 | end 14 | 15 | defp deps do 16 | [{:postgrex, ">= 0.0.0"}, 17 | {:ecto, path: "../.."}] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"decimal": {:hex, :decimal, "1.1.0"}, 2 | "earmark": {:hex, :earmark, "0.1.12"}, 3 | "ex_doc": {:hex, :ex_doc, "0.6.2"}, 4 | "inch_ex": {:hex, :inch_ex, "0.2.3"}, 5 | "mariaex": {:git, "git://github.com/liveforeverx/mariaex.git", "ae72fc1d4b812459d1539c6997ac7746f546180b", []}, 6 | "poison": {:hex, :poison, "1.3.0"}, 7 | "poolboy": {:hex, :poolboy, "1.4.2"}, 8 | "postgrex": {:hex, :postgrex, "0.7.0"}} 9 | -------------------------------------------------------------------------------- /examples/simple/priv/repo/migrations/20150115192936_create_weather.exs: -------------------------------------------------------------------------------- 1 | defmodule Repo.CreatePosts do 2 | use Ecto.Migration 3 | 4 | def up do 5 | create table(:weather) do 6 | add :city, :string, size: 40 7 | add :temp_lo, :integer 8 | add :temp_hi, :integer 9 | add :prcp, :float 10 | timestamps 11 | end 12 | end 13 | 14 | def down do 15 | drop table(:weather) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple 2 | 3 | To run this example, you need to ensure postgres is up and running with a `postgres` username and `postgres` password. If you want to run with another credentials, just change the settings in the `config/config.exs` file. 4 | 5 | Then, from the command line: 6 | 7 | * `mix do deps.get, compile` 8 | * `mix ecto.create` 9 | * `mix ecto.migrate` 10 | * `iex -S mix` 11 | 12 | Inside IEx, run: 13 | 14 | * `Simple.sample_query` 15 | -------------------------------------------------------------------------------- /test/ecto/query/builder/limit_offset_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.LimitOffsetTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query 5 | 6 | test "overrides on duplicated limit and offset" do 7 | %Ecto.Query{limit: %Ecto.Query.QueryExpr{expr: limit}} = %Ecto.Query{} |> limit([], 1) |> limit([], 2) 8 | assert limit == 2 9 | 10 | %Ecto.Query{offset: %Ecto.Query.QueryExpr{expr: offset}} = %Ecto.Query{} |> offset([], 1) |> offset([], 2) |> select([], 3) 11 | assert offset == 2 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/types.exs: -------------------------------------------------------------------------------- 1 | defmodule Custom.Permalink do 2 | def type, do: :integer 3 | 4 | def cast(string) when is_binary(string) do 5 | case Integer.parse(string) do 6 | {int, _} -> {:ok, int} 7 | :error -> :error 8 | end 9 | end 10 | 11 | def cast(integer) when is_integer(integer), do: {:ok, integer} 12 | 13 | def cast(_), do: :error 14 | def blank?(_), do: false 15 | 16 | def load(integer) when is_integer(integer), do: {:ok, integer} 17 | def dump(integer) when is_integer(integer), do: {:ok, integer} 18 | end 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 17.0 4 | before_install: 5 | - wget http://s3.hex.pm/builds/elixir/v1.0.0.zip 6 | - unzip -d elixir v1.0.0.zip 7 | before_script: 8 | - export PATH=`pwd`/elixir/bin:$PATH 9 | - mix local.hex --force 10 | - mix deps.get 11 | script: 12 | - mix test.all 13 | after_script: 14 | - mix deps.get --only docs 15 | - MIX_ENV=docs mix inch.report 16 | notifications: 17 | recipients: 18 | - jquadrin@gmail.com 19 | # - jose.valim@plataformatec.com.br 20 | # - eric.meadows.jonsson@gmail.com 21 | -------------------------------------------------------------------------------- /test/ecto/query/builder/lock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.LockTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder.Lock 5 | doctest Ecto.Query.Builder.Lock 6 | 7 | import Ecto.Query 8 | 9 | test "lock interpolation" do 10 | lock = true 11 | assert lock("posts", ^lock).lock == true 12 | 13 | lock = "FOR UPDATE" 14 | assert lock("posts", ^lock).lock == "FOR UPDATE" 15 | end 16 | 17 | test "invalid lock" do 18 | assert_raise Ecto.Query.CompileError, ~r"invalid lock `1`", fn -> 19 | %Ecto.Query{} |> lock(^1) |> select([], 0) 20 | end 21 | end 22 | 23 | test "overrides on duplicated lock" do 24 | query = %Ecto.Query{} |> lock(false) |> lock(true) 25 | assert query.lock == true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/ecto/query/builder/distinct_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.DistinctTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder.Distinct 5 | doctest Ecto.Query.Builder.Distinct 6 | 7 | test "escape" do 8 | assert {Macro.escape(quote do [&0.y] end), %{}} == 9 | escape(quote do x.y end, [x: 0]) 10 | 11 | assert {Macro.escape(quote do [&0.x, &1.y] end), %{}} == 12 | escape(quote do [x.x, y.y] end, [x: 0, y: 1]) 13 | 14 | assert {Macro.escape(quote do [1 == 2] end), %{}} == 15 | escape(quote do 1 == 2 end, []) 16 | end 17 | 18 | test "escape raise" do 19 | assert_raise Ecto.Query.CompileError, "unbound variable `x` in query", fn -> 20 | escape(quote do x.y end, []) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/ecto/query/builder/group_by_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.GroupByTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder.GroupBy 5 | doctest Ecto.Query.Builder.GroupBy 6 | 7 | test "escape" do 8 | assert {Macro.escape(quote do [&0.y] end), %{}} == 9 | escape(quote do x.y end, [x: 0]) 10 | 11 | assert {Macro.escape(quote do [&0.x, &1.y] end), %{}} == 12 | escape(quote do [x.x, y.y] end, [x: 0, y: 1]) 13 | 14 | assert {Macro.escape(quote do [1 < 2] end), %{}} == 15 | escape(quote do 1 < 2 end, []) 16 | end 17 | 18 | test "escape raise" do 19 | assert_raise Ecto.Query.CompileError, "unbound variable `x` in query", fn -> 20 | escape(quote do x.y end, []) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/ecto/adapter/transactions.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapter.Transactions do 2 | @moduledoc """ 3 | Specifies the adapter transactions API. 4 | """ 5 | 6 | use Behaviour 7 | 8 | @doc """ 9 | Runs the given function inside a transaction. Returns `{:ok, value}` if the 10 | transaction was successful where `value` is the value return by the function 11 | or `{:error, value}` if the transaction was rolled back where `value` is the 12 | value given to `rollback/1`. 13 | 14 | See `Ecto.Repo.transaction/1`. 15 | """ 16 | defcallback transaction(Ecto.Repo.t, Keyword.t, fun) :: {:ok, any} | {:error, any} 17 | 18 | @doc """ 19 | Rolls back the current transaction. The transaction will return the value 20 | given as `{:error, value}`. 21 | 22 | See `Ecto.Repo.rollback/1`. 23 | """ 24 | defcallback rollback(Ecto.Repo.t, any) :: no_return 25 | end 26 | -------------------------------------------------------------------------------- /examples/simple/lib/simple.ex: -------------------------------------------------------------------------------- 1 | defmodule Simple.App do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec 6 | tree = [worker(Simple.Repo, [])] 7 | opts = [name: Simple.Sup, strategy: :one_for_one] 8 | Supervisor.start_link(tree, opts) 9 | end 10 | end 11 | 12 | defmodule Simple.Repo do 13 | use Ecto.Repo, 14 | otp_app: :simple, 15 | adapter: Ecto.Adapters.Postgres 16 | end 17 | 18 | defmodule Weather do 19 | use Ecto.Model 20 | 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 | timestamps 27 | end 28 | end 29 | 30 | defmodule Simple do 31 | import Ecto.Query 32 | 33 | def sample_query do 34 | query = from w in Weather, 35 | where: w.prcp > 0.0 or is_nil(w.prcp), 36 | select: w 37 | Simple.Repo.all(query) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/ecto/storage.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Storage do 2 | @moduledoc """ 3 | Convenience functions around the data store of a repository. 4 | """ 5 | 6 | @doc """ 7 | Create the storage in the data store and return `:ok` if it was created 8 | successfully. 9 | 10 | Returns `{:error, :already_up}` if the storage has already been created or 11 | `{:error, term}` in case anything else goes wrong. 12 | """ 13 | @spec up(Ecto.Repo.t) :: :ok | {:error, :already_up} | {:error, term} 14 | def up(repo) do 15 | repo.adapter.storage_up(repo.config) 16 | end 17 | 18 | @doc """ 19 | Drop the storage in the data store and return `:ok` if it was dropped 20 | successfully. 21 | 22 | Returns `{:error, :already_down}` if the storage has already been dropped or 23 | `{:error, term}` in case anything else goes wrong. 24 | """ 25 | @spec down(Ecto.Repo.t) :: :ok | {:error, :already_down} | {:error, term} 26 | def down(repo) do 27 | repo.adapter.storage_down(repo.config) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/ecto/uuid_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.UUIDTest do 2 | use ExUnit.Case, async: true 3 | 4 | @test_uuid "601D74E4-A8D3-4B6E-8365-EDDB4C893327" 5 | @test_uuid_binary << 0x60, 0x1D, 0x74, 0xE4, 0xA8, 0xD3, 0x4B, 0x6E, 0x83, 0x65, 0xED, 0xDB, 0x4C, 0x89, 0x33, 0x27 >> 6 | 7 | test "cast" do 8 | assert Ecto.UUID.cast(@test_uuid) == {:ok, @test_uuid} 9 | assert Ecto.UUID.cast(@test_uuid_binary) == {:ok, @test_uuid} 10 | assert Ecto.UUID.cast(nil) == :error 11 | end 12 | 13 | test "load" do 14 | assert Ecto.UUID.load(@test_uuid_binary) == {:ok, @test_uuid} 15 | assert Ecto.UUID.load(@test_uuid) == :error 16 | end 17 | 18 | test "dump" do 19 | assert Ecto.UUID.dump(@test_uuid) == {:ok, @test_uuid_binary} 20 | assert Ecto.UUID.dump(@test_uuid_binary) == :error 21 | end 22 | 23 | test "generate" do 24 | assert << _::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96 >> = Ecto.UUID.generate 25 | end 26 | 27 | test "blank?" do 28 | assert Ecto.UUID.blank?("") 29 | refute Ecto.UUID.blank?("hello") 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/ecto/queryable.ex: -------------------------------------------------------------------------------- 1 | defprotocol Ecto.Queryable do 2 | @moduledoc """ 3 | Converts a data structure into an `Ecto.Query`. 4 | """ 5 | 6 | @doc """ 7 | Converts the given `data` into an `Ecto.Query`. 8 | """ 9 | def to_query(data) 10 | end 11 | 12 | defimpl Ecto.Queryable, for: Ecto.Query do 13 | def to_query(query), do: query 14 | end 15 | 16 | defimpl Ecto.Queryable, for: BitString do 17 | def to_query(source) when is_binary(source), 18 | do: %Ecto.Query{from: {source, nil}} 19 | end 20 | 21 | defimpl Ecto.Queryable, for: Atom do 22 | def to_query(module) do 23 | try do 24 | %Ecto.Query{from: {module.__schema__(:source), module}} 25 | rescue 26 | UndefinedFunctionError -> 27 | message = if :code.is_loaded(module) do 28 | "the given module is not queryable" 29 | else 30 | "the given module does not exist" 31 | end 32 | 33 | raise Protocol.UndefinedError, 34 | protocol: @protocol, 35 | value: module, 36 | description: message 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/ecto/query/builder/join_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.JoinTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder.Join 5 | doctest Ecto.Query.Builder.Join 6 | 7 | import Ecto.Query 8 | 9 | test "invalid joins" do 10 | assert_raise Ecto.Query.CompileError, 11 | ~r/invalid join qualifier `:whatever`/, fn -> 12 | qual = :whatever 13 | join("posts", qual, [p], c in "comments", true) 14 | end 15 | 16 | assert_raise Ecto.Query.CompileError, 17 | "expected join to be a string or atom, got: `123`", fn -> 18 | source = 123 19 | join("posts", :left, [p], c in ^source, true) 20 | end 21 | end 22 | 23 | test "join interpolation" do 24 | qual = :left 25 | source = "comments" 26 | assert %{joins: [%{source: {"comments", nil}}]} = 27 | join("posts", qual, [p], c in ^source, true) 28 | 29 | qual = :right 30 | source = Comment 31 | assert %{joins: [%{source: {nil, Comment}}]} = 32 | join("posts", qual, [p], c in ^source, true) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/mix/tasks/ecto.migrate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.MigrateTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mix.Tasks.Ecto.Migrate, only: [run: 2] 5 | 6 | defmodule Repo do 7 | def start_link do 8 | Process.put(:started, true) 9 | :ok 10 | end 11 | 12 | def __repo__ do 13 | true 14 | end 15 | 16 | def config do 17 | [priv: "hello", otp_app: :ecto] 18 | end 19 | end 20 | 21 | test "runs the migrator without starting" do 22 | run ["-r", to_string(Repo), "--no-start"], fn _, _, _, _ -> 23 | Process.put(:migrated, true) 24 | end 25 | assert Process.get(:migrated) 26 | refute Process.get(:started) 27 | end 28 | 29 | test "runs the migrator yielding the repository and migrations path" do 30 | run ["-r", to_string(Repo)], fn repo, path, direction, strategy -> 31 | assert repo == Repo 32 | assert path == Application.app_dir(:ecto, "hello/migrations") 33 | assert direction == :up 34 | assert strategy[:all] == true 35 | end 36 | assert Process.get(:started) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/mix/tasks/ecto.rollback_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.RollbackTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mix.Tasks.Ecto.Rollback, only: [run: 2] 5 | 6 | defmodule Repo do 7 | def start_link do 8 | Process.put(:started, true) 9 | :ok 10 | end 11 | 12 | def __repo__ do 13 | true 14 | end 15 | 16 | def config do 17 | [priv: "hello", otp_app: :ecto] 18 | end 19 | end 20 | 21 | test "runs the migrator without starting" do 22 | run ["-r", to_string(Repo), "--no-start"], fn _, _, _, _ -> 23 | Process.put(:migrated, true) 24 | end 25 | assert Process.get(:migrated) 26 | refute Process.get(:started) 27 | end 28 | 29 | test "runs the migrator yielding the repository and migrations path" do 30 | run ["-r", to_string(Repo)], fn repo, path, direction, strategy -> 31 | assert repo == Repo 32 | assert path == Application.app_dir(:ecto, "hello/migrations") 33 | assert direction == :down 34 | assert strategy[:step] == 1 35 | end 36 | assert Process.get(:started) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/ecto/query/builder/preload_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../support/eval_helpers.exs", __DIR__ 2 | 3 | defmodule Ecto.Query.Builder.PreloadTest do 4 | use ExUnit.Case, async: true 5 | 6 | import Ecto.Query.Builder.Preload 7 | doctest Ecto.Query.Builder.Preload 8 | 9 | import Ecto.Query 10 | import Support.EvalHelpers 11 | 12 | test "invalid preload" do 13 | assert_raise Ecto.Query.CompileError, ~r"`1` is not a valid preload expression", fn -> 14 | quote_and_eval(%Ecto.Query{} |> preload(1)) 15 | end 16 | end 17 | 18 | test "preload accumulates" do 19 | query = %Ecto.Query{} |> preload(:foo) |> preload(:bar) 20 | assert query.preloads == [:foo, :bar] 21 | end 22 | 23 | test "preload interpolation" do 24 | comments = :comments 25 | assert preload("posts", ^comments).preloads == [:comments] 26 | assert preload("posts", ^[comments]).preloads == [[:comments]] 27 | assert preload("posts", [users: ^comments]).preloads == [users: [:comments]] 28 | assert preload("posts", [users: ^[comments]]).preloads == [users: [[:comments]]] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.drop.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Drop do 2 | use Mix.Task 3 | import Mix.Ecto 4 | 5 | @shortdoc "Drop the storage for the repo" 6 | 7 | @moduledoc """ 8 | Drop the storage for the repository. 9 | 10 | ## Examples 11 | 12 | mix ecto.drop 13 | mix ecto.drop -r Custom.Repo 14 | 15 | ## Command line options 16 | 17 | * `-r`, `--repo` - the repo to drop (defaults to `YourApp.Repo`) 18 | * `--no-start` - do not start applications 19 | 20 | """ 21 | 22 | @doc false 23 | def run(args) do 24 | Mix.Task.run "app.start", args 25 | 26 | repo = parse_repo(args) 27 | ensure_repo(repo) 28 | ensure_implements(repo.adapter, Ecto.Adapter.Storage, "to create storage for #{inspect repo}") 29 | 30 | case Ecto.Storage.down(repo) do 31 | :ok -> 32 | Mix.shell.info "The database for repo #{inspect repo} has been dropped." 33 | {:error, :already_down} -> 34 | Mix.shell.info "The database for repo #{inspect repo} has already been dropped." 35 | {:error, term} -> 36 | Mix.raise "The database for repo #{inspect repo} couldn't be dropped, reason given: #{term}." 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.create.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Create do 2 | use Mix.Task 3 | import Mix.Ecto 4 | 5 | @shortdoc "Create the storage for the repo" 6 | 7 | @moduledoc """ 8 | Create the storage for the repository. 9 | 10 | ## Examples 11 | 12 | mix ecto.create 13 | mix ecto.create -r Custom.Repo 14 | 15 | ## Command line options 16 | 17 | * `-r`, `--repo` - the repo to create (defaults to `YourApp.Repo`) 18 | * `--no-start` - do not start applications 19 | 20 | """ 21 | 22 | @doc false 23 | def run(args) do 24 | Mix.Task.run "app.start", args 25 | 26 | repo = parse_repo(args) 27 | ensure_repo(repo) 28 | ensure_implements(repo.adapter, Ecto.Adapter.Storage, "to create storage for #{inspect repo}") 29 | 30 | case Ecto.Storage.up(repo) do 31 | :ok -> 32 | Mix.shell.info "The database for repo #{inspect repo} has been created." 33 | {:error, :already_up} -> 34 | Mix.shell.info "The database for repo #{inspect repo} has already been created." 35 | {:error, term} -> 36 | Mix.raise "The database for repo #{inspect repo} couldn't be created, reason given: #{term}." 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/ecto/adapter/storage.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapter.Storage do 2 | @moduledoc """ 3 | Specifies the adapter storage API. 4 | """ 5 | 6 | use Behaviour 7 | 8 | @doc """ 9 | Create the storage in the data store and return `:ok` if it was created 10 | successfully. 11 | 12 | Returns `{:error, :already_up}` if the storage has already been created or 13 | `{:error, term}` in case anything else goes wrong. 14 | 15 | ## Examples 16 | 17 | storage_up(username: postgres, 18 | database: 'ecto_test', 19 | hostname: 'localhost') 20 | 21 | """ 22 | defcallback storage_up(Keyword.t) :: :ok | {:error, :already_up} | {:error, term} 23 | 24 | @doc """ 25 | Drop the storage in the data store and return `:ok` if it was dropped 26 | successfully. 27 | 28 | Returns `{:error, :already_down}` if the storage has already been dropped or 29 | `{:error, term}` in case anything else goes wrong. 30 | 31 | ## Examples 32 | 33 | storage_down(username: postgres, 34 | database: 'ecto_test', 35 | hostname: 'localhost') 36 | 37 | """ 38 | defcallback storage_down(Keyword.t) :: :ok | {:error, :already_down} | {:error, term} 39 | end 40 | -------------------------------------------------------------------------------- /test/support/file_helpers.exs: -------------------------------------------------------------------------------- 1 | defmodule Support.FileHelpers do 2 | import ExUnit.Assertions 3 | 4 | @doc """ 5 | Returns the `tmp_path` for tests. 6 | """ 7 | def tmp_path do 8 | Path.expand("../../tmp", __DIR__) 9 | end 10 | 11 | @doc """ 12 | Executes the given function in a temp directory 13 | tailored for this test case and test. 14 | """ 15 | defmacro in_tmp(fun) do 16 | path = Path.join([tmp_path, "#{__CALLER__.module}", "#{elem(__CALLER__.function, 0)}"]) 17 | quote do 18 | path = unquote(path) 19 | File.rm_rf!(path) 20 | File.mkdir_p!(path) 21 | File.cd!(path, fn -> unquote(fun).(path) end) 22 | end 23 | end 24 | 25 | @doc """ 26 | Asserts a file was generated. 27 | """ 28 | def assert_file(file) do 29 | assert File.regular?(file), "Expected #{file} to exist, but does not" 30 | end 31 | 32 | @doc """ 33 | Asserts a file was generated and that it matches a given pattern. 34 | """ 35 | def assert_file(file, callback) when is_function(callback, 1) do 36 | assert_file(file) 37 | callback.(File.read!(file)) 38 | end 39 | 40 | def assert_file(file, match) do 41 | assert_file file, &(assert &1 =~ match) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/ecto/migration/schema_migration.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.SchemaMigration do 2 | # Define a schema that works with the schema_migrations table 3 | @moduledoc false 4 | use Ecto.Model 5 | 6 | @primary_key {:version, :integer, []} 7 | schema "schema_migrations" do 8 | timestamps updated_at: false 9 | end 10 | 11 | @table %Ecto.Migration.Table{name: :schema_migrations} 12 | @opts [timeout: :infinity, log: false] 13 | 14 | def ensure_schema_migrations_table!(repo) do 15 | adapter = repo.adapter 16 | 17 | # DLL queries do not log, so we do not need 18 | # to pass log: false here. 19 | unless adapter.ddl_exists?(repo, @table, @opts) do 20 | adapter.execute_ddl(repo, 21 | {:create, @table, [ 22 | {:add, :version, :bigint, primary_key: true}, 23 | {:add, :inserted_at, :datetime, []}]}, @opts) 24 | end 25 | 26 | :ok 27 | end 28 | 29 | def migrated_versions(repo) do 30 | repo.all from(p in __MODULE__, select: p.version), @opts 31 | end 32 | 33 | def up(repo, version) do 34 | repo.insert %__MODULE__{version: version}, @opts 35 | end 36 | 37 | def down(repo, version) do 38 | repo.delete %__MODULE__{version: version}, @opts 39 | end 40 | end -------------------------------------------------------------------------------- /lib/ecto/query/builder/where.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Where do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Builds a quoted expression. 8 | 9 | The quoted expression should evaluate to a query at runtime. 10 | If possible, it does all calculations at compile time to avoid 11 | runtime work. 12 | """ 13 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 14 | def build(query, binding, expr, env) do 15 | binding = Builder.escape_binding(binding) 16 | {expr, params} = Builder.escape(expr, :boolean, %{}, binding) 17 | params = Builder.escape_params(params) 18 | 19 | where = quote do: %Ecto.Query.QueryExpr{ 20 | expr: unquote(expr), 21 | params: unquote(params), 22 | file: unquote(env.file), 23 | line: unquote(env.line)} 24 | Builder.apply_query(query, __MODULE__, [where], env) 25 | end 26 | 27 | @doc """ 28 | The callback applied by `build/4` to build the query. 29 | """ 30 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 31 | def apply(query, expr) do 32 | query = Ecto.Queryable.to_query(query) 33 | %{query | wheres: query.wheres ++ [expr]} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/ecto/query/builder/select_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.SelectTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query 5 | import Ecto.Query.Builder.Select 6 | doctest Ecto.Query.Builder.Select 7 | 8 | test "escape" do 9 | assert {Macro.escape(quote do &0 end), %{}} == 10 | escape(quote do x end, [x: 0]) 11 | 12 | assert {Macro.escape(quote do &0.y end), %{}} == 13 | escape(quote do x.y end, [x: 0]) 14 | 15 | assert {{:{}, [], [:{}, [], [0, 1, 2]]}, %{}} == 16 | escape(quote do {0, 1, 2} end, []) 17 | 18 | assert {[Macro.escape(quote do &0.y end), Macro.escape(quote do &0.z end)], %{}} == 19 | escape(quote do [x.y, x.z] end, [x: 0]) 20 | 21 | assert {{:{}, [], [:^, [], [0]]}, %{0 => {{:+, _, [{:x, _, _}, {:y, _, _}]}, :any}}} = 22 | escape(quote do ^(x + y) end, []) 23 | 24 | assert {{:{}, [], [:^, [], [0]]}, %{0 => {quote do x.y end, :any}}} == 25 | escape(quote do ^x.y end, []) 26 | end 27 | 28 | test "only one select is allowed" do 29 | message = "only one select expression is allowed in query" 30 | assert_raise Ecto.Query.CompileError, message, fn -> 31 | %Ecto.Query{} |> select([], 1) |> select([], 2) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/having.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Having do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Builds a quoted expression. 8 | 9 | The quoted expression should evaluate to a query at runtime. 10 | If possible, it does all calculations at compile time to avoid 11 | runtime work. 12 | """ 13 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 14 | def build(query, binding, expr, env) do 15 | binding = Builder.escape_binding(binding) 16 | {expr, params} = Builder.escape(expr, :boolean, %{}, binding) 17 | params = Builder.escape_params(params) 18 | 19 | having = quote do: %Ecto.Query.QueryExpr{ 20 | expr: unquote(expr), 21 | params: unquote(params), 22 | file: unquote(env.file), 23 | line: unquote(env.line)} 24 | Builder.apply_query(query, __MODULE__, [having], env) 25 | end 26 | 27 | @doc """ 28 | The callback applied by `build/4` to build the query. 29 | """ 30 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 31 | def apply(query, expr) do 32 | query = Ecto.Queryable.to_query(query) 33 | %{query | havings: query.havings ++ [expr]} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /integration_test/cases/sql_escape.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.SQLEscapeTest do 2 | use Ecto.Integration.Case 3 | 4 | test "Repo.all escape" do 5 | TestRepo.insert(%Post{text: "hello"}) 6 | 7 | query = from(p in Post, select: "'\\") 8 | assert ["'\\"] == TestRepo.all(query) 9 | end 10 | 11 | test "Repo.insert escape" do 12 | TestRepo.insert(%Post{text: "'"}) 13 | 14 | query = from(p in Post, select: p.text) 15 | assert ["'"] == TestRepo.all(query) 16 | end 17 | 18 | test "Repo.update escape" do 19 | p = TestRepo.insert(%Post{text: "hello"}) 20 | TestRepo.update(%{p | text: "'"}) 21 | 22 | query = from(p in Post, select: p.text) 23 | assert ["'"] == TestRepo.all(query) 24 | end 25 | 26 | test "Repo.update_all escape" do 27 | TestRepo.insert(%Post{text: "hello"}) 28 | TestRepo.update_all(Post, text: "'") 29 | 30 | query = from(p in Post, select: p.text) 31 | assert ["'"] == TestRepo.all(query) 32 | 33 | TestRepo.update_all(from(Post, where: "'" != ""), text: "''") 34 | assert ["''"] == TestRepo.all(query) 35 | end 36 | 37 | test "Repo.delete_all escape" do 38 | TestRepo.insert(%Post{text: "hello"}) 39 | assert [_] = TestRepo.all(Post) 40 | 41 | TestRepo.delete_all(from(Post, where: "'" == "'")) 42 | assert [] == TestRepo.all(Post) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/mix/tasks/ecto.gen.migration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Gen.MigrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Support.FileHelpers 5 | import Mix.Tasks.Ecto.Gen.Migration, only: [run: 1] 6 | 7 | tmp_path = Path.join(tmp_path, inspect(Ecto.Gen.Migration)) 8 | @migrations_path Path.join(tmp_path, "migrations") 9 | 10 | defmodule Repo do 11 | def __repo__ do 12 | true 13 | end 14 | 15 | def config do 16 | [priv: "tmp/#{inspect(Ecto.Gen.Migration)}", otp_app: :ecto] 17 | end 18 | end 19 | 20 | setup do 21 | File.rm_rf!(unquote(tmp_path)) 22 | :ok 23 | end 24 | 25 | test "generates a new migration" do 26 | run ["-r", to_string(Repo), "my_migration"] 27 | assert [name] = File.ls!(@migrations_path) 28 | assert String.match? name, ~r/^\d{14}_my_migration\.exs$/ 29 | assert_file Path.join(@migrations_path, name), fn file -> 30 | assert String.contains? file, "defmodule Mix.Tasks.Ecto.Gen.MigrationTest.Repo.Migrations.MyMigration do" 31 | assert String.contains? file, "use Ecto.Migration" 32 | assert String.contains? file, "def up do" 33 | assert String.contains? file, "def down do" 34 | end 35 | end 36 | 37 | test "raises when missing file" do 38 | assert_raise Mix.Error, fn -> run ["-r", to_string(Repo)] end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /integration_test/support/migration.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.Migration do 2 | use Ecto.Migration 3 | 4 | def up do 5 | create table(:posts) do 6 | add :title, :string, size: 100 7 | add :counter, :integer, default: 10 8 | add :text, :string 9 | add :tags, {:array, :text} 10 | add :bin, :binary 11 | add :uuid, :uuid 12 | add :cost, :decimal, precision: 2, scale: 2 13 | timestamps 14 | end 15 | 16 | create table(:users) do 17 | add :name, :text 18 | end 19 | 20 | create table(:permalinks) do 21 | add :url 22 | add :post_id, :integer 23 | end 24 | 25 | create table(:comments) do 26 | add :text, :string, size: 100 27 | add :posted, :datetime 28 | add :day, :date 29 | add :time, :time 30 | add :bytes, :binary 31 | add :post_id, references(:posts) 32 | add :author_id, references(:users) 33 | end 34 | 35 | create table(:customs, primary_key: false) do 36 | add :foo, :uuid, primary_key: true 37 | end 38 | 39 | create table(:barebones) do 40 | add :text, :text 41 | end 42 | 43 | create table(:transactions) do 44 | add :text, :text 45 | end 46 | 47 | create table(:lock_counters) do 48 | add :count, :integer 49 | end 50 | 51 | create table(:migrations_test) do 52 | add :name, :text 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /integration_test/mysql/storage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.StorageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ecto.Adapters.MySQL 5 | 6 | def correct_params do 7 | [database: "storage_mgt", 8 | username: "root", 9 | password: nil, 10 | hostname: "localhost"] 11 | end 12 | 13 | def wrong_user do 14 | [database: "storage_mgt", 15 | username: "randomuser", 16 | password: "password1234", 17 | hostname: "localhost"] 18 | end 19 | 20 | def drop_database do 21 | :os.cmd 'mysql -u root -e "DROP DATABASE IF EXISTS storage_mgt;"' 22 | end 23 | 24 | def create_database do 25 | :os.cmd 'mysql -u root -e "CREATE DATABASE storage_mgt;"' 26 | end 27 | 28 | setup do 29 | on_exit fn -> drop_database end 30 | :ok 31 | end 32 | 33 | test "storage up (twice in a row)" do 34 | assert MySQL.storage_up(correct_params) == :ok 35 | assert MySQL.storage_up(correct_params) == {:error, :already_up} 36 | end 37 | 38 | test "storage up (wrong credentials)" do 39 | refute MySQL.storage_up(wrong_user) == :ok 40 | end 41 | 42 | test "storage down (twice in a row)" do 43 | create_database 44 | 45 | assert MySQL.storage_down(correct_params) == :ok 46 | assert MySQL.storage_down(correct_params) == {:error, :already_down} 47 | end 48 | 49 | test "storage down (wrong credentials)" do 50 | create_database 51 | refute MySQL.storage_down(wrong_user) == :ok 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /integration_test/pg/storage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.StorageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ecto.Adapters.Postgres 5 | 6 | def correct_params do 7 | [database: "storage_mgt", 8 | username: "postgres", 9 | password: "postgres", 10 | hostname: "localhost"] 11 | end 12 | 13 | def wrong_user do 14 | [database: "storage_mgt", 15 | username: "randomuser", 16 | password: "password1234", 17 | hostname: "localhost"] 18 | end 19 | 20 | def drop_database do 21 | :os.cmd 'psql -U postgres -c "DROP DATABASE IF EXISTS storage_mgt;"' 22 | end 23 | 24 | def create_database do 25 | :os.cmd 'psql -U postgres -c "CREATE DATABASE storage_mgt;"' 26 | end 27 | 28 | setup do 29 | on_exit fn -> drop_database end 30 | :ok 31 | end 32 | 33 | test "storage up (twice in a row)" do 34 | assert Postgres.storage_up(correct_params) == :ok 35 | assert Postgres.storage_up(correct_params) == {:error, :already_up} 36 | end 37 | 38 | test "storage up (wrong credentials)" do 39 | refute Postgres.storage_up(wrong_user) == :ok 40 | end 41 | 42 | test "storage down (twice in a row)" do 43 | create_database 44 | 45 | assert Postgres.storage_down(correct_params) == :ok 46 | assert Postgres.storage_down(correct_params) == {:error, :already_down} 47 | end 48 | 49 | test "storage down (wrong credentials)" do 50 | create_database 51 | refute Postgres.storage_down(wrong_user) == :ok 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/ecto/type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.TypeTest do 2 | use ExUnit.Case, async: true 3 | 4 | defmodule Custom do 5 | @behaviour Ecto.Type 6 | def type, do: :custom 7 | def load(_), do: {:ok, :load} 8 | def dump(_), do: {:ok, :dump} 9 | def cast(_), do: {:ok, :cast} 10 | def blank?(_), do: false 11 | end 12 | 13 | import Kernel, except: [match?: 2], warn: false 14 | import Ecto.Type 15 | doctest Ecto.Type 16 | 17 | test "custom types" do 18 | assert load(Custom, "foo") == {:ok, :load} 19 | assert dump(Custom, "foo") == {:ok, :dump} 20 | assert cast(Custom, "foo") == {:ok, :cast} 21 | refute blank?(Custom, "foo") 22 | 23 | assert load(Custom, nil) == {:ok, nil} 24 | assert dump(Custom, nil) == {:ok, nil} 25 | assert cast(Custom, nil) == {:ok, nil} 26 | assert blank?(Custom, nil) 27 | end 28 | 29 | test "custom types with array" do 30 | assert load({:array, Custom}, ["foo"]) == {:ok, [:load]} 31 | assert dump({:array, Custom}, ["foo"]) == {:ok, [:dump]} 32 | assert cast({:array, Custom}, ["foo"]) == {:ok, [:cast]} 33 | 34 | assert load({:array, Custom}, [nil]) == {:ok, [nil]} 35 | assert dump({:array, Custom}, [nil]) == {:ok, [nil]} 36 | assert cast({:array, Custom}, [nil]) == {:ok, [nil]} 37 | end 38 | 39 | test "decimal casting" do 40 | assert cast(:decimal, "1.0") == {:ok, Decimal.new("1.0")} 41 | assert cast(:decimal, 1.0) == {:ok, Decimal.new("1.0")} 42 | assert cast(:decimal, 1) == {:ok, Decimal.new("1")} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/mix/tasks/ecto.gen.repo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Gen.RepoTest do 2 | use ExUnit.Case 3 | 4 | import Support.FileHelpers 5 | import Mix.Tasks.Ecto.Gen.Repo, only: [run: 1] 6 | 7 | test "generates a new repo" do 8 | in_tmp fn _ -> 9 | run ["-r", "Repo"] 10 | 11 | assert_file "lib/repo.ex", fn file -> 12 | assert String.contains? file, "defmodule Repo do" 13 | assert String.contains? file, "use Ecto.Repo" 14 | assert String.contains? file, "adapter: Ecto.Adapters.Postgres," 15 | assert String.contains? file, "otp_app: :ecto" 16 | end 17 | 18 | assert_file "config/config.exs", """ 19 | use Mix.Config 20 | 21 | config :ecto, Repo, 22 | database: "ecto_repo", 23 | username: "user", 24 | password: "pass", 25 | hostname: "localhost" 26 | """ 27 | 28 | run ["-r", "AnotherRepo"] 29 | 30 | assert_file "config/config.exs", """ 31 | config :ecto, AnotherRepo, 32 | database: "ecto_another_repo", 33 | username: "user", 34 | password: "pass", 35 | hostname: "localhost" 36 | """ 37 | end 38 | end 39 | 40 | test "generates a new namespaced repo" do 41 | in_tmp fn _ -> 42 | run ["-r", "My.AppRepo"] 43 | assert_file "lib/my/app_repo.ex", "defmodule My.AppRepo do" 44 | end 45 | end 46 | 47 | test "generates default repo" do 48 | in_tmp fn _ -> 49 | run [] 50 | assert_file "lib/ecto/repo.ex", "defmodule Ecto.Repo do" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/ecto/adapter/migrations.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapter.Migrations do 2 | @moduledoc """ 3 | Specifies the adapter migrations API. 4 | """ 5 | 6 | use Behaviour 7 | 8 | alias Ecto.Migration.Table 9 | alias Ecto.Migration.Index 10 | alias Ecto.Migration.Reference 11 | 12 | @typedoc "All migration commands" 13 | @type command :: 14 | raw :: String.t | 15 | {:create, Table.t, [table_subcommand]} | 16 | {:alter, Table.t, [table_subcommand]} | 17 | {:drop, Table.t} | 18 | {:create, Index.t} | 19 | {:drop, Index.t} 20 | 21 | @type table_subcommand :: 22 | {:add, field :: atom, type :: Ecto.Type.t | Reference.t, Keyword.t} | 23 | {:modify, field :: atom, type :: Ecto.Type.t | Reference.t, Keyword.t} | 24 | {:remove, field :: atom} 25 | 26 | @type ddl_object :: Table.t | Index.t 27 | 28 | @doc """ 29 | Executes migration commands. 30 | 31 | ## Options 32 | 33 | * `:timeout` - The time in milliseconds to wait for the call to finish, 34 | `:infinity` will wait indefinitely (default: 5000); 35 | * `:log` - When false, does not log begin/commit/rollback queries 36 | """ 37 | defcallback execute_ddl(Ecto.Repo.t, command, Keyword.t) :: :ok | no_return 38 | 39 | @doc """ 40 | Checks if ddl value, like a table or index, exists. 41 | 42 | ## Options 43 | 44 | * `:timeout` - The time in milliseconds to wait for the call to finish, 45 | `:infinity` will wait indefinitely (default: 5000); 46 | * `:log` - When false, does not log begin/commit/rollback queries 47 | """ 48 | defcallback ddl_exists?(Ecto.Repo.t, ddl_object, Keyword.t) :: boolean 49 | end 50 | -------------------------------------------------------------------------------- /lib/ecto/model/timestamps.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Model.Timestamps do 2 | @moduledoc """ 3 | Automatically manage timestamps. 4 | 5 | If the user calls `Ecto.Schema.timestamps/0` in their schema, the 6 | model will automatically set callbacks based on the schema information 7 | to update the configured `:inserted_at` and `:updated_at` fields 8 | according to the given type. 9 | """ 10 | 11 | defmacro __using__(_) do 12 | quote do 13 | @before_compile Ecto.Model.Timestamps 14 | end 15 | end 16 | 17 | import Ecto.Changeset 18 | 19 | @doc """ 20 | Puts a timestamp in the changeset with the given field and type. 21 | """ 22 | def put_timestamp(changeset, field, type) do 23 | if get_change changeset, field do 24 | changeset 25 | else 26 | put_change changeset, field, Ecto.Type.load!(type, :erlang.universaltime) 27 | end 28 | end 29 | 30 | defmacro __before_compile__(env) do 31 | timestamps = Module.get_attribute(env.module, :timestamps) 32 | 33 | if timestamps do 34 | type = timestamps[:type] 35 | 36 | inserted_at = if field = Keyword.fetch!(timestamps, :inserted_at) do 37 | quote do 38 | before_insert Ecto.Model.Timestamps, :put_timestamp, [unquote(field), unquote(type)] 39 | end 40 | end 41 | 42 | updated_at = if field = Keyword.fetch!(timestamps, :updated_at) do 43 | quote do 44 | before_insert Ecto.Model.Timestamps, :put_timestamp, [unquote(field), unquote(type)] 45 | before_update Ecto.Model.Timestamps, :put_timestamp, [unquote(field), unquote(type)] 46 | end 47 | end 48 | 49 | {inserted_at, updated_at} 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /test/mix/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.EctoTest do 2 | use ExUnit.Case, async: true 3 | import Mix.Ecto 4 | 5 | test :parse_repo do 6 | assert parse_repo(["-r", "Repo"]) == Repo 7 | assert parse_repo(["--repo", Repo]) == Repo 8 | assert parse_repo([]) == Ecto.Repo 9 | 10 | Application.put_env(:ecto, :app_namespace, Foo) 11 | assert parse_repo([]) == Foo.Repo 12 | after 13 | Application.delete_env(:ecto, :app_namespace) 14 | end 15 | 16 | defmodule Repo do 17 | def start_link do 18 | Process.get(:start_link) 19 | end 20 | 21 | def __repo__ do 22 | true 23 | end 24 | 25 | def config do 26 | [priv: Process.get(:priv), otp_app: :ecto] 27 | end 28 | end 29 | 30 | test :ensure_repo do 31 | assert ensure_repo(Repo) == Repo 32 | assert_raise Mix.Error, fn -> ensure_repo(String) end 33 | assert_raise Mix.Error, fn -> ensure_repo(NotLoaded) end 34 | end 35 | 36 | test :ensure_started do 37 | Process.put(:start_link, :ok) 38 | assert ensure_started(Repo) == Repo 39 | 40 | Process.put(:start_link, {:ok, self}) 41 | assert ensure_started(Repo) == Repo 42 | 43 | Process.put(:start_link, {:error, {:already_started, self}}) 44 | assert ensure_started(Repo) == Repo 45 | 46 | Process.put(:start_link, {:error, self}) 47 | assert_raise Mix.Error, fn -> ensure_started(Repo) end 48 | end 49 | 50 | test :migrations_path do 51 | Process.put(:priv, nil) 52 | assert migrations_path(Repo) == Application.app_dir(:ecto, "priv/repo/migrations") 53 | Process.put(:priv, "hello") 54 | assert migrations_path(Repo) == Application.app_dir(:ecto, "hello/migrations") 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/group_by.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.GroupBy do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Escapes a list of quoted expressions. 8 | 9 | See `Ecto.Builder.escape/2`. 10 | 11 | iex> escape(quote do [x.x, 13] end, [x: 0]) 12 | {[{:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]}, 13 | 13], 14 | %{}} 15 | """ 16 | @spec escape(Macro.t, Keyword.t) :: Macro.t 17 | def escape(expr, vars) do 18 | expr 19 | |> List.wrap 20 | |> Builder.escape(:any, %{}, vars) 21 | end 22 | 23 | @doc """ 24 | Builds a quoted expression. 25 | 26 | The quoted expression should evaluate to a query at runtime. 27 | If possible, it does all calculations at compile time to avoid 28 | runtime work. 29 | """ 30 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 31 | def build(query, binding, expr, env) do 32 | binding = Builder.escape_binding(binding) 33 | {expr, params} = escape(expr, binding) 34 | params = Builder.escape_params(params) 35 | 36 | group_by = quote do: %Ecto.Query.QueryExpr{ 37 | expr: unquote(expr), 38 | params: unquote(params), 39 | file: unquote(env.file), 40 | line: unquote(env.line)} 41 | Builder.apply_query(query, __MODULE__, [group_by], env) 42 | end 43 | 44 | @doc """ 45 | The callback applied by `build/4` to build the query. 46 | """ 47 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 48 | def apply(query, expr) do 49 | query = Ecto.Queryable.to_query(query) 50 | %{query | group_bys: query.group_bys ++ [expr]} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ecto/repo/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.Config do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Retrieves and normalizes the configuration for `repo` in `otp_app`. 6 | """ 7 | def config(otp_app, module) do 8 | if config = Application.get_env(otp_app, module) do 9 | {url, config} = Keyword.pop(config, :url) 10 | [otp_app: otp_app] ++ Keyword.merge(config, parse_url(url || "")) 11 | else 12 | raise ArgumentError, 13 | "configuration for #{inspect module} not specified in #{inspect otp_app} environment" 14 | end 15 | end 16 | 17 | @doc """ 18 | Parses an Ecto URL. The format must be: 19 | 20 | "ecto://username:password@hostname:port/database" 21 | 22 | or 23 | 24 | {:system, "DATABASE_URL"} 25 | 26 | """ 27 | def parse_url(""), do: [] 28 | 29 | def parse_url({:system, env}) when is_binary(env) do 30 | parse_url(System.get_env(env) || "") 31 | end 32 | 33 | def parse_url(url) when is_binary(url) do 34 | info = URI.parse(url) 35 | 36 | unless info.host do 37 | raise Ecto.InvalidURLError, url: url, message: "host is not present" 38 | end 39 | 40 | unless String.match? info.path, ~r"^/([^/])+$" do 41 | raise Ecto.InvalidURLError, url: url, message: "path should be a database name" 42 | end 43 | 44 | if info.userinfo do 45 | destructure [username, password], String.split(info.userinfo, ":") 46 | end 47 | 48 | database = String.slice(info.path, 1, String.length(info.path)) 49 | 50 | opts = [username: username, 51 | password: password, 52 | database: database, 53 | hostname: info.host, 54 | port: info.port] 55 | 56 | Enum.reject(opts, fn {_k, v} -> is_nil(v) end) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/lock.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Lock do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Escapes the lock code. 8 | 9 | iex> escape(quote do: true) 10 | true 11 | 12 | iex> escape(quote do: "FOO") 13 | "FOO" 14 | 15 | """ 16 | @spec escape(Macro.t) :: Macro.t | no_return 17 | def escape(lock) when is_boolean(lock) or is_binary(lock), do: lock 18 | 19 | def escape({:^, _, [lock]}) do 20 | quote do: unquote(__MODULE__).lock!(unquote(lock)) 21 | end 22 | 23 | def escape(other) do 24 | Builder.error! "`#{Macro.to_string(other)}` is not a valid lock expression, " <> 25 | "use ^ if you want to interpolate a value" 26 | end 27 | 28 | @doc """ 29 | Validates the expression is an integer or raise. 30 | """ 31 | def lock!(lock) when is_boolean(lock) or is_binary(lock), do: lock 32 | 33 | def lock!(lock) do 34 | Builder.error! "invalid lock `#{inspect lock}`. lock must be a boolean value " <> 35 | "or a string containing the database-specific locking clause" 36 | end 37 | 38 | @doc """ 39 | Builds a quoted expression. 40 | 41 | The quoted expression should evaluate to a query at runtime. 42 | If possible, it does all calculations at compile time to avoid 43 | runtime work. 44 | """ 45 | @spec build(Macro.t, Macro.t, Macro.Env.t) :: Macro.t 46 | def build(query, expr, env) do 47 | Builder.apply_query(query, __MODULE__, [escape(expr)], env) 48 | end 49 | 50 | @doc """ 51 | The callback applied by `build/4` to build the query. 52 | """ 53 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 54 | def apply(query, value) do 55 | query = Ecto.Queryable.to_query(query) 56 | %{query | lock: value} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/ecto/uuid.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.UUID do 2 | @moduledoc """ 3 | An Ecto type for UUIDs strings. 4 | 5 | In contrast to the `:uuid` type, `Ecto.UUID` works 6 | with UUID as strings instead of binary data. 7 | """ 8 | 9 | @behaviour Ecto.Type 10 | 11 | @doc """ 12 | The Ecto primitive type. 13 | """ 14 | def type, do: :uuid 15 | 16 | @doc """ 17 | UUIDs are blank when given as strings and the string is blank. 18 | """ 19 | defdelegate blank?(value), to: Ecto.Type 20 | 21 | @doc """ 22 | Casts to UUID. 23 | """ 24 | def cast(<< _::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96 >> = u), do: {:ok, u} 25 | def cast(uuid = << _::128 >>), do: load(uuid) 26 | def cast(_), do: :error 27 | 28 | @doc """ 29 | Converts an string representing a UUID into a binary. 30 | """ 31 | def dump(<< u0::64, ?-, u1::32, ?-, u2::32, ?-, u3::32, ?-, u4::96 >>) do 32 | Base.decode16(<< u0::64, u1::32, u2::32, u3::32, u4::96 >>, case: :mixed) 33 | end 34 | def dump(_), do: :error 35 | 36 | @doc """ 37 | Converts a binary UUID into a string. 38 | """ 39 | def load(uuid = << _::128 >>) do 40 | {:ok, encode(uuid)} 41 | end 42 | def load(_), do: :error 43 | 44 | @doc """ 45 | Generates a version 4 (random) UUID. 46 | """ 47 | def generate do 48 | <> = :crypto.strong_rand_bytes(16) 49 | <> |> encode 50 | end 51 | 52 | defp encode(<>) do 53 | hex_pad(u0, 8) <> "-" <> hex_pad(u1, 4) <> "-" <> hex_pad(u2, 4) <> 54 | "-" <> hex_pad(u3, 4) <> "-" <> hex_pad(u4, 12) 55 | end 56 | 57 | defp hex_pad(hex, count) do 58 | hex = Integer.to_string(hex, 16) 59 | :binary.copy("0", count - byte_size(hex)) <> hex 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.migrate.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Migrate do 2 | use Mix.Task 3 | import Mix.Ecto 4 | 5 | @shortdoc "Runs migrations up on a repo" 6 | 7 | @moduledoc """ 8 | Runs the pending migrations for the given repository. 9 | 10 | By default, migrations are expected at "priv/YOUR_REPO/migrations" 11 | directory of the current application but it can be configured 12 | by specify the `:priv` key under the repository configuration. 13 | 14 | Runs all pending migrations by default. To migrate up 15 | to a version number, supply `--to version_number`. 16 | To migrate up a specific number of times, use `--step n`. 17 | 18 | ## Examples 19 | 20 | mix ecto.migrate 21 | mix ecto.migrate -r Custom.Repo 22 | 23 | mix ecto.migrate -n 3 24 | mix ecto.migrate --step 3 25 | 26 | mix ecto.migrate -v 20080906120000 27 | mix ecto.migrate --to 20080906120000 28 | 29 | ## Command line options 30 | 31 | * `-r`, `--repo` - the repo to migrate (defaults to `YourApp.Repo`) 32 | * `--all` - run all pending migrations 33 | * `--step` / `-n` - run n number of pending migrations 34 | * `--to` / `-v` - run all migrations up to and including version 35 | * `--no-start` - do not start applications 36 | """ 37 | 38 | @doc false 39 | def run(args, migrator \\ &Ecto.Migrator.run/4) do 40 | Mix.Task.run "app.start", args 41 | repo = parse_repo(args) 42 | 43 | {opts, _, _} = OptionParser.parse args, 44 | switches: [all: :boolean, step: :integer, to: :integer, start: :boolean], 45 | aliases: [n: :step, v: :to] 46 | 47 | ensure_repo(repo) 48 | if Keyword.get(opts, :start, true), do: ensure_started(repo) 49 | 50 | unless opts[:to] || opts[:step] || opts[:all] do 51 | opts = Keyword.put(opts, :all, true) 52 | end 53 | 54 | migrator.(repo, migrations_path(repo), :up, opts) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/support/mock_repo.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.MockAdapter do 2 | @behaviour Ecto.Adapter 3 | 4 | defmacro __using__(_opts), do: :ok 5 | def start_link(_repo, _opts), do: :ok 6 | def stop(_repo), do: :ok 7 | 8 | ## Queryable 9 | 10 | def all(_repo, %{from: {_, Ecto.Migration.SchemaMigration}}, _, _), 11 | do: Enum.map(migrated_versions(), &List.wrap/1) 12 | def all(_repo, _query, _params, _opts), 13 | do: [[1]] 14 | 15 | def update_all(_repo, _query, _values, _params, _opts), do: 1 16 | def delete_all(_repo, _query, _params, _opts), do: 1 17 | 18 | ## Model 19 | 20 | def insert(_repo, "schema_migrations", val, _, _) do 21 | version = Keyword.fetch!(val, :version) 22 | Process.put(:migrated_versions, [version|migrated_versions()]) 23 | {:ok, {1}} 24 | end 25 | 26 | def insert(_repo, _source, _fields, [_], _opts), 27 | do: {:ok, {1}} 28 | 29 | def update(_repo, _source, _filter, _fields, [_], _opts), 30 | do: {:ok, {1}} 31 | 32 | def delete(_repo, "schema_migrations", [version: version], _) do 33 | Process.put(:migrated_versions, List.delete(migrated_versions(), version)) 34 | {:ok, {}} 35 | end 36 | 37 | def delete(_repo, _source, _filter, _opts), 38 | do: {:ok, {}} 39 | 40 | ## Transactions 41 | 42 | def transaction(_repo, _opts, fun) do 43 | # Makes transactions "trackable" in tests 44 | send self, {:transaction, fun} 45 | {:ok, fun.()} 46 | end 47 | 48 | ## Migrations 49 | 50 | def execute_ddl(_repo, command, _) do 51 | Process.put(:last_command, command) 52 | :ok 53 | end 54 | 55 | def ddl_exists?(_repo, object, _) do 56 | Process.put(:last_exists, object) 57 | true 58 | end 59 | 60 | defp migrated_versions do 61 | Process.get(:migrated_versions) || [] 62 | end 63 | end 64 | 65 | defmodule Ecto.MockRepo do 66 | use Ecto.Repo, adapter: Ecto.MockAdapter, otp_app: :ecto 67 | end 68 | -------------------------------------------------------------------------------- /test/ecto/model/timestamps_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../support/mock_repo.exs", __DIR__ 2 | Code.require_file "../../support/types.exs", __DIR__ 3 | alias Ecto.MockRepo 4 | 5 | defmodule Ecto.Model.TimestampsTest do 6 | use ExUnit.Case, async: true 7 | 8 | defmodule Default do 9 | use Ecto.Model 10 | 11 | schema "default" do 12 | timestamps 13 | end 14 | end 15 | 16 | defmodule Config do 17 | use Ecto.Model 18 | 19 | @timestamps_type :datetime 20 | schema "default" do 21 | timestamps inserted_at: :created_on, updated_at: :updated_on 22 | end 23 | end 24 | 25 | test "sets inserted_at and updated_at values" do 26 | default = MockRepo.insert(%Default{}) 27 | assert %Ecto.DateTime{} = default.inserted_at 28 | assert %Ecto.DateTime{} = default.updated_at 29 | 30 | default = MockRepo.update(%Default{id: 1}) 31 | refute default.inserted_at 32 | assert %Ecto.DateTime{} = default.updated_at 33 | end 34 | 35 | test "does not set inserted_at and updated_at values if they were previoously set" do 36 | default = MockRepo.insert(%Default{inserted_at: %Ecto.DateTime{year: 2000}, 37 | updated_at: %Ecto.DateTime{year: 2000}}) 38 | assert %Ecto.DateTime{year: 2000} = default.inserted_at 39 | assert %Ecto.DateTime{year: 2000} = default.updated_at 40 | 41 | default = MockRepo.update(%Default{id: 1, updated_at: %Ecto.DateTime{year: 2000}}) 42 | refute default.inserted_at 43 | assert %Ecto.DateTime{year: 2000} = default.updated_at 44 | end 45 | 46 | test "sets custom inserted_at and updated_at values" do 47 | default = MockRepo.insert(%Config{}) 48 | assert {_, _} = default.created_on 49 | assert {_, _} = default.updated_on 50 | 51 | default = MockRepo.update(%Config{id: 1}) 52 | refute default.created_on 53 | assert {_, _} = default.updated_on 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /integration_test/cases/lock.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.LockTest do 2 | # We can keep this test async as long as it 3 | # is the only one accessing the lock_test table. 4 | use ExUnit.Case, async: true 5 | 6 | import Ecto.Query 7 | alias Ecto.Integration.PoolRepo 8 | 9 | defmodule LockCounter do 10 | use Ecto.Model 11 | 12 | schema "lock_counters" do 13 | field :count, :integer 14 | end 15 | end 16 | 17 | setup do 18 | %LockCounter{id: 42, count: 1} |> PoolRepo.insert 19 | 20 | on_exit fn -> 21 | PoolRepo.get(LockCounter, 42) |> PoolRepo.delete 22 | end 23 | 24 | :ok 25 | end 26 | 27 | test "lock for update" do 28 | query = from(p in LockCounter, where: p.id == 42, lock: true) 29 | pid = self 30 | 31 | new_pid = 32 | spawn_link fn -> 33 | receive do 34 | :select_for_update -> 35 | PoolRepo.transaction(fn -> 36 | [post] = PoolRepo.all(query) # this should block until the other trans. commits 37 | %{post | count: post.count + 1} |> PoolRepo.update 38 | end) 39 | send pid, :updated 40 | after 41 | 5000 -> raise "timeout" 42 | end 43 | end 44 | 45 | PoolRepo.transaction(fn -> 46 | [post] = PoolRepo.all(query) # select and lock the row 47 | send new_pid, :select_for_update # signal second process to begin a transaction 48 | receive do 49 | :updated -> raise "missing lock" # if we get this before committing, our lock failed 50 | after 51 | 100 -> :ok 52 | end 53 | %{post | count: post.count + 1} |> PoolRepo.update 54 | end) 55 | 56 | receive do 57 | :updated -> :ok 58 | after 59 | 5000 -> "timeout" 60 | end 61 | 62 | # Final count will be 3 if SELECT ... FOR UPDATE worked and 2 otherwise 63 | assert [%LockCounter{count: 3}] = PoolRepo.all(LockCounter) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /integration_test/support/models.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test/support/types.exs", __DIR__ 2 | 3 | defmodule Ecto.Integration.Post do 4 | use Ecto.Model 5 | 6 | schema "posts" do 7 | field :title, :string 8 | field :counter, :integer, read_after_writes: true 9 | field :text, :string 10 | field :tags, {:array, :string} 11 | field :bin, :binary 12 | field :uuid, :uuid 13 | field :temp, :string, default: "temp", virtual: true 14 | has_many :comments, Ecto.Integration.Comment 15 | has_one :permalink, Ecto.Integration.Permalink 16 | has_many :comments_authors, through: [:comments, :author] 17 | timestamps 18 | end 19 | end 20 | 21 | defmodule Ecto.Integration.Comment do 22 | use Ecto.Model 23 | 24 | schema "comments" do 25 | field :text, :string 26 | field :posted, :datetime 27 | field :day, :date 28 | field :time, :time 29 | field :bytes, :binary 30 | belongs_to :post, Ecto.Integration.Post 31 | belongs_to :author, Ecto.Integration.User 32 | has_one :post_permalink, through: [:post, :permalink] 33 | end 34 | end 35 | 36 | defmodule Ecto.Integration.Permalink do 37 | use Ecto.Model 38 | 39 | @foreign_key_type Custom.Permalink 40 | schema "permalinks" do 41 | field :url, :string 42 | belongs_to :post, Ecto.Integration.Post 43 | has_many :post_comments_authors, through: [:post, :comments_authors] 44 | end 45 | end 46 | 47 | defmodule Ecto.Integration.User do 48 | use Ecto.Model 49 | 50 | schema "users" do 51 | field :name, :string 52 | has_many :comments, Ecto.Integration.Comment, foreign_key: :author_id 53 | end 54 | end 55 | 56 | defmodule Ecto.Integration.Custom do 57 | use Ecto.Model 58 | 59 | @primary_key {:foo, :uuid, []} 60 | schema "customs" do 61 | end 62 | end 63 | 64 | defmodule Ecto.Integration.Barebone do 65 | use Ecto.Model 66 | 67 | @primary_key false 68 | schema "barebones" do 69 | field :text, :string 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /integration_test/mysql/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | ExUnit.start 3 | 4 | # Basic test repo 5 | alias Ecto.Integration.TestRepo 6 | 7 | Application.put_env(:ecto, TestRepo, 8 | url: "ecto://root@localhost/ecto_test", 9 | size: 1, 10 | max_overflow: 0) 11 | 12 | defmodule Ecto.Integration.TestRepo do 13 | use Ecto.Repo, 14 | otp_app: :ecto, 15 | adapter: Ecto.Adapters.MySQL 16 | end 17 | 18 | # Pool repo for transaction and lock tests 19 | alias Ecto.Integration.PoolRepo 20 | 21 | Application.put_env(:ecto, PoolRepo, 22 | url: "ecto://root@localhost/ecto_test", 23 | size: 10) 24 | 25 | defmodule Ecto.Integration.PoolRepo do 26 | use Ecto.Repo, 27 | otp_app: :ecto, 28 | adapter: Ecto.Adapters.MySQL 29 | end 30 | 31 | defmodule Ecto.Integration.Case do 32 | use ExUnit.CaseTemplate 33 | 34 | using do 35 | quote do 36 | import unquote(__MODULE__) 37 | require TestRepo 38 | 39 | import Ecto.Query 40 | alias Ecto.Integration.TestRepo 41 | alias Ecto.Integration.Post 42 | alias Ecto.Integration.Comment 43 | alias Ecto.Integration.Permalink 44 | alias Ecto.Integration.User 45 | alias Ecto.Integration.Custom 46 | alias Ecto.Integration.Barebone 47 | end 48 | end 49 | 50 | setup do 51 | :ok = Ecto.Adapters.SQL.begin_test_transaction(TestRepo, []) 52 | 53 | on_exit fn -> 54 | :ok = Ecto.Adapters.SQL.rollback_test_transaction(TestRepo, []) 55 | end 56 | 57 | :ok 58 | end 59 | end 60 | 61 | # Load support models and migration 62 | Code.require_file "../support/models.exs", __DIR__ 63 | Code.require_file "../support/migration.exs", __DIR__ 64 | 65 | # Load up the repository, start it, and run migrations 66 | _ = Ecto.Storage.down(TestRepo) 67 | :ok = Ecto.Storage.up(TestRepo) 68 | 69 | {:ok, _pid} = TestRepo.start_link 70 | {:ok, _pid} = PoolRepo.start_link 71 | 72 | _ = Ecto.Migrator.up(TestRepo, 0, Ecto.Integration.Migration, log: false) 73 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.rollback.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Rollback do 2 | use Mix.Task 3 | import Mix.Ecto 4 | 5 | @shortdoc "Reverts migrations down on a repo" 6 | 7 | @moduledoc """ 8 | Reverts applied migrations in the given repository. 9 | 10 | By default, migrations are expected at "priv/YOUR_REPO/migrations" 11 | directory of the current application but it can be configured 12 | by specify the `:priv` key under the repository configuration. 13 | 14 | Runs the latest applied migration by default. To roll back to 15 | to a version number, supply `--to version_number`. 16 | To roll back a specific number of times, use `--step n`. 17 | To undo all applied migrations, provide `--all`. 18 | 19 | ## Examples 20 | 21 | mix ecto.rollback 22 | mix ecto.rollback -r Custom.Repo 23 | 24 | mix ecto.rollback -n 3 25 | mix ecto.rollback --step 3 26 | 27 | mix ecto.rollback -v 20080906120000 28 | mix ecto.rollback --to 20080906120000 29 | 30 | ## Command line options 31 | 32 | * `-r`, `--repo` - the repo to rollback (defaults to `YourApp.Repo`) 33 | * `--all` - revert all applied migrations 34 | * `--step` / `-n` - rever n number of applied migrations 35 | * `--to` / `-v` - revert all migrations down to and including version 36 | * `--no-start` - do not start applications 37 | """ 38 | 39 | @doc false 40 | def run(args, migrator \\ &Ecto.Migrator.run/4) do 41 | Mix.Task.run "app.start", args 42 | repo = parse_repo(args) 43 | 44 | {opts, _, _} = OptionParser.parse args, 45 | switches: [all: :boolean, step: :integer, to: :integer, start: :boolean], 46 | aliases: [n: :step, v: :to] 47 | 48 | ensure_repo(repo) 49 | if Keyword.get(opts, :start, true), do: ensure_started(repo) 50 | 51 | unless opts[:to] || opts[:step] || opts[:all] do 52 | opts = Keyword.put(opts, :step, 1) 53 | end 54 | 55 | migrator.(repo, migrations_path(repo), :down, opts) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/limit_offset.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.LimitOffset do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Builds a quoted expression. 8 | 9 | The quoted expression should evaluate to a query at runtime. 10 | If possible, it does all calculations at compile time to avoid 11 | runtime work. 12 | """ 13 | @spec build(:limit | :offset, Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 14 | def build(type, query, binding, expr, env) do 15 | binding = Builder.escape_binding(binding) 16 | {expr, params} = Builder.escape(expr, :integer, %{}, binding) 17 | params = Builder.escape_params(params) 18 | 19 | if contains_variable?(expr) do 20 | Builder.error! "query variables are not allowed in #{type} expression" 21 | end 22 | 23 | limoff = quote do: %Ecto.Query.QueryExpr{ 24 | expr: unquote(expr), 25 | params: unquote(params), 26 | file: unquote(env.file), 27 | line: unquote(env.line)} 28 | 29 | Builder.apply_query(query, __MODULE__, [type, limoff], env) 30 | end 31 | 32 | defp contains_variable?({:&, _, _}), 33 | do: true 34 | defp contains_variable?({left, _, right}), 35 | do: contains_variable?(left) or contains_variable?(right) 36 | defp contains_variable?({left, right}), 37 | do: contains_variable?(left) or contains_variable?(right) 38 | defp contains_variable?(list) when is_list(list), 39 | do: Enum.any?(list, &contains_variable?/1) 40 | defp contains_variable?(_), 41 | do: false 42 | 43 | @doc """ 44 | The callback applied by `build/4` to build the query. 45 | """ 46 | @spec apply(Ecto.Queryable.t, :limit | :offset, term) :: Ecto.Query.t 47 | def apply(query, :limit, expr) do 48 | query = Ecto.Queryable.to_query(query) 49 | %{query | limit: expr} 50 | end 51 | 52 | def apply(query, :offset, expr) do 53 | query = Ecto.Queryable.to_query(query) 54 | %{query | offset: expr} 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /integration_test/pg/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | ExUnit.start 3 | 4 | # Basic test repo 5 | alias Ecto.Integration.TestRepo 6 | 7 | Application.put_env(:ecto, TestRepo, 8 | url: "ecto://postgres:postgres@localhost/ecto_test", 9 | size: 1, 10 | max_overflow: 0) 11 | 12 | defmodule Ecto.Integration.TestRepo do 13 | use Ecto.Repo, 14 | otp_app: :ecto, 15 | adapter: Ecto.Adapters.Postgres 16 | end 17 | 18 | # Pool repo for transaction and lock tests 19 | alias Ecto.Integration.PoolRepo 20 | 21 | Application.put_env(:ecto, PoolRepo, 22 | url: "ecto://postgres:postgres@localhost/ecto_test", 23 | size: 10) 24 | 25 | defmodule Ecto.Integration.PoolRepo do 26 | use Ecto.Repo, 27 | otp_app: :ecto, 28 | adapter: Ecto.Adapters.Postgres 29 | end 30 | 31 | defmodule Ecto.Integration.Case do 32 | use ExUnit.CaseTemplate 33 | 34 | using do 35 | quote do 36 | import unquote(__MODULE__) 37 | require TestRepo 38 | 39 | import Ecto.Query 40 | alias Ecto.Integration.TestRepo 41 | alias Ecto.Integration.Post 42 | alias Ecto.Integration.Comment 43 | alias Ecto.Integration.Permalink 44 | alias Ecto.Integration.User 45 | alias Ecto.Integration.Custom 46 | alias Ecto.Integration.Barebone 47 | end 48 | end 49 | 50 | setup_all do 51 | Ecto.Adapters.SQL.begin_test_transaction(TestRepo, []) 52 | on_exit fn -> Ecto.Adapters.SQL.rollback_test_transaction(TestRepo, []) end 53 | :ok 54 | end 55 | 56 | setup do 57 | Ecto.Adapters.SQL.restart_test_transaction(TestRepo, []) 58 | :ok 59 | end 60 | end 61 | 62 | # Load support models and migration 63 | Code.require_file "../support/models.exs", __DIR__ 64 | Code.require_file "../support/migration.exs", __DIR__ 65 | 66 | # Load up the repository, start it, and run migrations 67 | _ = Ecto.Storage.down(TestRepo) 68 | :ok = Ecto.Storage.up(TestRepo) 69 | 70 | {:ok, _pid} = TestRepo.start_link 71 | {:ok, _pid} = PoolRepo.start_link 72 | 73 | :ok = Ecto.Migrator.up(TestRepo, 0, Ecto.Integration.Migration, log: false) 74 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/distinct.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Distinct do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Escapes a list of quoted expressions. 8 | 9 | iex> escape(quote do x end, [x: 0]) 10 | {[{:{}, [], [:&, [], [0]]}], %{}} 11 | 12 | iex> escape(quote do [x.x, 13] end, [x: 0]) 13 | {[{:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]}, 14 | 13], 15 | %{}} 16 | 17 | """ 18 | @spec escape(Macro.t, Keyword.t) :: {Macro.t, %{}} 19 | def escape(expr, vars) do 20 | expr 21 | |> List.wrap 22 | |> Enum.map_reduce(%{}, &escape(&1, &2, vars)) 23 | end 24 | 25 | defp escape({var, _, context}, params, vars) when is_atom(var) and is_atom(context) do 26 | {Builder.escape_var(var, vars), params} 27 | end 28 | 29 | defp escape(expr, params, vars) do 30 | Builder.escape(expr, :any, params, vars) 31 | end 32 | 33 | @doc """ 34 | Builds a quoted expression. 35 | 36 | The quoted expression should evaluate to a query at runtime. 37 | If possible, it does all calculations at compile time to avoid 38 | runtime work. 39 | """ 40 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 41 | def build(query, binding, expr, env) do 42 | binding = Builder.escape_binding(binding) 43 | {expr, params} = escape(expr, binding) 44 | params = Builder.escape_params(params) 45 | 46 | distinct = quote do: %Ecto.Query.QueryExpr{ 47 | expr: unquote(expr), 48 | params: unquote(params), 49 | file: unquote(env.file), 50 | line: unquote(env.line)} 51 | Builder.apply_query(query, __MODULE__, [distinct], env) 52 | end 53 | 54 | @doc """ 55 | The callback applied by `build/4` to build the query. 56 | """ 57 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 58 | def apply(query, expr) do 59 | query = Ecto.Queryable.to_query(query) 60 | %{query | distincts: query.distincts ++ [expr]} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.gen.repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Gen.Repo do 2 | use Mix.Task 3 | 4 | import Mix.Ecto 5 | import Mix.Generator 6 | 7 | @shortdoc "Generates a new repository" 8 | 9 | @moduledoc """ 10 | Generates a new repository. 11 | 12 | The repository will be placed in the `lib` directory. 13 | 14 | ## Examples 15 | 16 | mix ecto.gen.repo 17 | mix ecto.gen.repo -r Custom.Repo 18 | 19 | ## Command line options 20 | 21 | * `-r`, `--repo` - the repo to generate (defaults to `YourApp.Repo`) 22 | 23 | """ 24 | 25 | @doc false 26 | def run(args) do 27 | no_umbrella!("ecto.gen.repo") 28 | repo = parse_repo(args) 29 | 30 | config = Mix.Project.config 31 | underscored = Mix.Utils.underscore(inspect(repo)) 32 | 33 | base = Path.basename(underscored) 34 | file = Path.join("lib", underscored) <> ".ex" 35 | app = config[:app] || :YOUR_APP_NAME 36 | opts = [mod: repo, app: app, base: base] 37 | 38 | create_directory Path.dirname(file) 39 | create_file file, repo_template(opts) 40 | 41 | case File.read "config/config.exs" do 42 | {:ok, contents} -> 43 | Mix.shell.info [:green, "* updating ", :reset, "config/config.exs"] 44 | File.write! "config/config.exs", contents <> config_template(opts) 45 | {:error, _} -> 46 | create_file "config/config.exs", "use Mix.Config\n" <> config_template(opts) 47 | end 48 | 49 | open?("config/config.exs") 50 | 51 | Mix.shell.info """ 52 | Don't forget to add your new repo to your supervision tree 53 | (typically in lib/#{app}.ex): 54 | 55 | worker(#{inspect repo}, []) 56 | """ 57 | end 58 | 59 | embed_template :repo, """ 60 | defmodule <%= inspect @mod %> do 61 | use Ecto.Repo, 62 | adapter: Ecto.Adapters.Postgres, 63 | otp_app: <%= inspect @app %> 64 | end 65 | """ 66 | 67 | embed_template :config, """ 68 | 69 | config <%= inspect @app %>, <%= inspect @mod %>, 70 | database: "<%= @app %>_<%= @base %>", 71 | username: "user", 72 | password: "pass", 73 | hostname: "localhost" 74 | """ 75 | end 76 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.7.2" 5 | @adapters [:pg, :mysql] 6 | 7 | def project do 8 | [app: :ecto, 9 | version: @version, 10 | elixir: "~> 1.0", 11 | deps: deps, 12 | build_per_environment: false, 13 | test_paths: test_paths(Mix.env), 14 | 15 | # Custom testing 16 | aliases: ["test.all": &test_all/1], 17 | preferred_cli_env: ["test.all": :test], 18 | 19 | # Hex 20 | description: description, 21 | package: package, 22 | 23 | # Docs 24 | name: "Ecto", 25 | docs: [source_ref: "v#{@version}", 26 | source_url: "https://github.com/elixir-lang/ecto"]] 27 | end 28 | 29 | def application do 30 | [applications: [:logger, :decimal, :poolboy]] 31 | end 32 | 33 | defp deps do 34 | [{:poolboy, "~> 1.4.1"}, 35 | {:decimal, "~> 1.0"}, 36 | {:postgrex, "~> 0.7"}, 37 | {:mariaex, github: "liveforeverx/mariaex", optional: true}, 38 | {:ex_doc, "~> 0.6", only: :docs}, 39 | {:earmark, "~> 0.1", only: :docs}, 40 | {:inch_ex, only: :docs}] 41 | end 42 | 43 | defp test_paths(adapter) when adapter in @adapters, do: ["integration_test/#{adapter}"] 44 | defp test_paths(_), do: ["test"] 45 | 46 | defp description do 47 | """ 48 | Ecto is a domain specific language for writing queries and interacting with databases in Elixir. 49 | """ 50 | end 51 | 52 | defp package do 53 | [contributors: ["Eric Meadows-Jönsson", "José Valim"], 54 | licenses: ["Apache 2.0"], 55 | links: %{"GitHub" => "https://github.com/elixir-lang/ecto"}] 56 | end 57 | 58 | defp test_all(args) do 59 | args = if IO.ANSI.enabled?, do: ["--color"|args], else: ["--no-color"|args] 60 | Mix.Task.run "test", args 61 | 62 | for adapter <- @adapters do 63 | IO.puts "==> Running integration tests for MIX_ENV=#{adapter}" 64 | 65 | {_, res} = System.cmd "mix", ["test"|args], 66 | into: IO.binstream(:stdio, :line), 67 | env: [{"MIX_ENV", to_string(adapter)}] 68 | 69 | if res > 0 do 70 | System.at_exit(fn _ -> exit({:shutdown, 1}) end) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/mix/tasks/ecto.gen.migration.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.Gen.Migration do 2 | use Mix.Task 3 | import Mix.Ecto 4 | import Mix.Generator 5 | import Mix.Utils, only: [camelize: 1] 6 | 7 | @shortdoc "Generate a new migration for the repo" 8 | 9 | @moduledoc """ 10 | Generates a migration. 11 | 12 | ## Examples 13 | 14 | mix ecto.gen.migration add_posts_table 15 | mix ecto.gen.migration add_posts_table -r Custom.Repo 16 | 17 | By default, the migration will be generated to the 18 | "priv/YOUR_REPO/migrations" directory of the current application 19 | but it can be configured by specify the `:priv` key under 20 | the repository configuration. 21 | 22 | ## Command line options 23 | 24 | * `-r`, `--repo` - the repo to generate migration for (defaults to `YourApp.Repo`) 25 | * `--no-start` - do not start applications 26 | 27 | """ 28 | 29 | @doc false 30 | def run(args) do 31 | no_umbrella!("ecto.gen.migration") 32 | 33 | Mix.Task.run "app.start", args 34 | repo = parse_repo(args) 35 | 36 | case OptionParser.parse(args) do 37 | {_, [name], _} -> 38 | ensure_repo(repo) 39 | path = Path.relative_to(migrations_path(repo), Mix.Project.app_path) 40 | file = Path.join(path, "#{timestamp}_#{name}.exs") 41 | create_directory path 42 | create_file file, migration_template(mod: Module.concat([repo, Migrations, camelize(name)])) 43 | 44 | if open?(file) && Mix.shell.yes?("Do you want to run this migration?") do 45 | Mix.Task.run "ecto.migrate", [repo] 46 | end 47 | {_, _, _} -> 48 | Mix.raise "expected ecto.gen.migration to receive the migration file name, " <> 49 | "got: #{inspect Enum.join(args, " ")}" 50 | end 51 | end 52 | 53 | defp timestamp do 54 | {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time() 55 | "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}" 56 | end 57 | 58 | defp pad(i) when i < 10, do: << ?0, ?0 + i >> 59 | defp pad(i), do: to_string(i) 60 | 61 | embed_template :migration, """ 62 | defmodule <%= inspect @mod %> do 63 | use Ecto.Migration 64 | 65 | def up do 66 | end 67 | 68 | def down do 69 | end 70 | end 71 | """ 72 | end 73 | -------------------------------------------------------------------------------- /test/ecto/datetime_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.DateTest do 2 | use ExUnit.Case, async: true 3 | 4 | @test_date "2015-12-31" 5 | @test_ecto_date %Ecto.Date{year: 2015, month: 12, day: 31} 6 | 7 | test "cast" do 8 | assert Ecto.Date.cast(@test_date) == {:ok, @test_ecto_date} 9 | assert Ecto.Date.cast("2015-00-23") == :error 10 | assert Ecto.Date.cast("2015-13-23") == :error 11 | assert Ecto.Date.cast("2015-01-00") == :error 12 | assert Ecto.Date.cast("2015-01-32") == :error 13 | end 14 | 15 | test "to_string" do 16 | assert Ecto.Date.to_string(@test_ecto_date) == @test_date 17 | end 18 | 19 | test "blank?" do 20 | assert Ecto.Date.blank?("") 21 | refute Ecto.Date.blank?(%Ecto.Date{}) 22 | end 23 | end 24 | 25 | defmodule Ecto.TimeTest do 26 | use ExUnit.Case, async: true 27 | 28 | @test_time "23:50:07" 29 | @test_ecto_time %Ecto.Time{hour: 23, min: 50, sec: 07} 30 | 31 | test "cast" do 32 | assert Ecto.Time.cast(@test_time) == {:ok, @test_ecto_time} 33 | assert Ecto.Time.cast(@test_time <> "Z") == {:ok, @test_ecto_time} 34 | assert Ecto.Time.cast(@test_time <> ".030") == {:ok, @test_ecto_time} 35 | assert Ecto.Time.cast(@test_time <> ".030Z") == {:ok, @test_ecto_time} 36 | assert Ecto.Date.cast("24:01:01") == :error 37 | assert Ecto.Date.cast("00:61:00") == :error 38 | assert Ecto.Date.cast("00:00:61") == :error 39 | assert Ecto.Date.cast("00:00:009") == :error 40 | assert Ecto.Date.cast("00:00:00.A00") == :error 41 | end 42 | 43 | test "to_string" do 44 | assert Ecto.Time.to_string(@test_ecto_time) == @test_time 45 | end 46 | 47 | test "blank?" do 48 | assert Ecto.Time.blank?("") 49 | refute Ecto.Time.blank?(%Ecto.Time{}) 50 | end 51 | end 52 | 53 | defmodule Ecto.DateTimeTest do 54 | use ExUnit.Case, async: true 55 | 56 | @test_datetime "2015-01-23T23:50:07" 57 | @test_ecto_datetime %Ecto.DateTime{year: 2015, month: 1, day: 23, hour: 23, min: 50, sec: 07} 58 | 59 | test "cast" do 60 | assert Ecto.DateTime.cast("2015-01-23 23:50:07") == {:ok, @test_ecto_datetime} 61 | assert Ecto.DateTime.cast("2015-01-23T23:50:07") == {:ok, @test_ecto_datetime} 62 | assert Ecto.DateTime.cast("2015-01-23T23:50:07Z") == {:ok, @test_ecto_datetime} 63 | assert Ecto.DateTime.cast("2015-01-23T23:50:07.000Z") == {:ok, @test_ecto_datetime} 64 | assert Ecto.DateTime.cast("2015-01-23P23:50:07") == :error 65 | end 66 | 67 | test "to_string" do 68 | assert Ecto.DateTime.to_string(@test_ecto_datetime) == (@test_datetime <> "Z") 69 | end 70 | 71 | test "blank?" do 72 | assert Ecto.DateTime.blank?("") 73 | refute Ecto.DateTime.blank?(%Ecto.DateTime{}) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/ecto/query/builder/order_by_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.OrderByTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder.OrderBy 5 | doctest Ecto.Query.Builder.OrderBy 6 | 7 | import Ecto.Query 8 | 9 | test "escape" do 10 | assert {Macro.escape(quote do [asc: &0.y] end), %{}} == 11 | escape(quote do x.y end, [x: 0]) 12 | 13 | assert {Macro.escape(quote do [asc: &0.x, asc: &1.y] end), %{}} == 14 | escape(quote do [x.x, y.y] end, [x: 0, y: 1]) 15 | 16 | assert {Macro.escape(quote do [asc: &0.x, desc: &1.y] end), %{}} == 17 | escape(quote do [asc: x.x, desc: y.y] end, [x: 0, y: 1]) 18 | 19 | assert {Macro.escape(quote do [asc: &0.x, desc: &1.y] end), %{}} == 20 | escape(quote do [x.x, desc: y.y] end, [x: 0, y: 1]) 21 | 22 | assert {Macro.escape(quote do [asc: &0.x] end), %{}} == 23 | escape(quote do :x end, [x: 0]) 24 | 25 | assert {Macro.escape(quote do [asc: &0.x, desc: &0.y] end), %{}} == 26 | escape(quote do [:x, desc: :y] end, [x: 0]) 27 | 28 | assert {Macro.escape(quote do [asc: 1 == 2] end), %{}} == 29 | escape(quote do 1 == 2 end, []) 30 | end 31 | 32 | test "invalid order_by" do 33 | assert_raise Ecto.Query.CompileError, "unbound variable `x` in query", fn -> 34 | escape(quote do x.y end, []) 35 | end 36 | 37 | message = "expected :asc, :desc or interpolated value in `order_by`, got: `:test`" 38 | assert_raise Ecto.Query.CompileError, message, fn -> 39 | escape(quote do [test: x.y] end, [x: 0]) 40 | end 41 | 42 | message = "expected :asc or :desc in `order_by`, got: `:temp`" 43 | assert_raise Ecto.Query.CompileError, message, fn -> 44 | temp = :temp 45 | order_by("posts", [p], [{^var!(temp), p.y}]) 46 | end 47 | 48 | message = "expected a field as an atom in `order_by`, got: `\"temp\"`" 49 | assert_raise Ecto.Query.CompileError, message, fn -> 50 | temp = "temp" 51 | order_by("posts", [p], [asc: ^temp]) 52 | end 53 | 54 | message = "expected a list or keyword list of fields in `order_by`, got: `\"temp\"`" 55 | assert_raise Ecto.Query.CompileError, message, fn -> 56 | temp = "temp" 57 | order_by("posts", [p], ^temp) 58 | end 59 | end 60 | 61 | test "order_by interpolation" do 62 | key = :title 63 | dir = :desc 64 | assert order_by("q", [q], ^key).order_bys == order_by("q", [q], [asc: q.title]).order_bys 65 | assert order_by("q", [q], [^key]).order_bys == order_by("q", [q], [asc: q.title]).order_bys 66 | assert order_by("q", [q], [desc: ^key]).order_bys == order_by("q", [q], [desc: q.title]).order_bys 67 | assert order_by("q", [q], [{^dir, ^key}]).order_bys == order_by("q", [q], [desc: q.title]).order_bys 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/ecto/repo/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.ConfigTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Repo.Config 5 | 6 | defp put_env(env) do 7 | Application.put_env(:ecto, __MODULE__, env) 8 | end 9 | 10 | test "reads otp app configuration" do 11 | put_env(database: "hello") 12 | assert config(:ecto, __MODULE__) == [otp_app: :ecto, database: "hello"] 13 | end 14 | 15 | test "merges url into configuration" do 16 | put_env(database: "hello", url: "ecto://eric:hunter2@host:12345/mydb") 17 | assert config(:ecto, __MODULE__) == 18 | [otp_app: :ecto, username: "eric", password: "hunter2", 19 | database: "mydb", hostname: "host", port: 12345] 20 | end 21 | 22 | test "merges system url into configuration" do 23 | System.put_env("ECTO_REPO_CONFIG_URL", "ecto://eric:hunter2@host:12345/mydb") 24 | put_env(database: "hello", url: {:system, "ECTO_REPO_CONFIG_URL"}) 25 | assert config(:ecto, __MODULE__) == 26 | [otp_app: :ecto, username: "eric", password: "hunter2", 27 | database: "mydb", hostname: "host", port: 12345] 28 | end 29 | 30 | test "parse_url options" do 31 | url = parse_url("ecto://eric:hunter2@host:12345/mydb") 32 | assert {:password, "hunter2"} in url 33 | assert {:username, "eric"} in url 34 | assert {:hostname, "host"} in url 35 | assert {:database, "mydb"} in url 36 | assert {:port, 12345} in url 37 | end 38 | 39 | test "parse_url from system env" do 40 | System.put_env("ECTO_REPO_CONFIG_URL", "ecto://eric:hunter2@host:12345/mydb") 41 | url = parse_url({:system, "ECTO_REPO_CONFIG_URL"}) 42 | assert {:password, "hunter2"} in url 43 | assert {:username, "eric"} in url 44 | assert {:hostname, "host"} in url 45 | assert {:database, "mydb"} in url 46 | assert {:port, 12345} in url 47 | end 48 | 49 | test "parse_url returns no config when blank" do 50 | assert parse_url("") == [] 51 | assert parse_url({:system, "ECTO_REPO_CONFIG_NONE_URL"}) == [] 52 | 53 | System.put_env("ECTO_REPO_CONFIG_URL", "") 54 | assert parse_url({:system, "ECTO_REPO_CONFIG_URL"}) == [] 55 | end 56 | 57 | test "parse_urls empty username/password" do 58 | url = parse_url("ecto://host:12345/mydb") 59 | assert !Dict.has_key?(url, :username) 60 | assert !Dict.has_key?(url, :password) 61 | end 62 | 63 | test "fail on invalid urls" do 64 | assert_raise Ecto.InvalidURLError, ~r"host is not present", fn -> 65 | parse_url("eric:hunter2@host:123/mydb") 66 | end 67 | 68 | assert_raise Ecto.InvalidURLError, ~r"path should be a database name", fn -> 69 | parse_url("ecto://eric:hunter2@host:123/a/b/c") 70 | end 71 | 72 | assert_raise Ecto.InvalidURLError, ~r"path should be a database name", fn -> 73 | parse_url("ecto://eric:hunter2@host:123/") 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/select.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Select do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Escapes a select. 8 | 9 | It allows tuples, lists and variables at the top level. Inside the 10 | tuples and lists query expressions are allowed. 11 | 12 | ## Examples 13 | 14 | iex> escape({1, 2}, []) 15 | {{:{}, [], [:{}, [], [1, 2]]}, %{}} 16 | 17 | iex> escape([1, 2], []) 18 | {[1, 2], %{}} 19 | 20 | iex> escape(quote(do: x), [x: 0]) 21 | {{:{}, [], [:&, [], [0]]}, %{}} 22 | 23 | iex> escape(quote(do: ^123), []) 24 | {{:{}, [], [:^, [], [0]]}, %{0 => {123, :any}}} 25 | 26 | """ 27 | @spec escape(Macro.t, Keyword.t) :: {Macro.t, %{}} 28 | def escape(other, vars) do 29 | escape(other, %{}, vars) 30 | end 31 | 32 | # Tuple 33 | defp escape({left, right}, params, vars) do 34 | escape({:{}, [], [left, right]}, params, vars) 35 | end 36 | 37 | # Tuple 38 | defp escape({:{}, _, list}, params, vars) do 39 | {list, params} = Enum.map_reduce(list, params, &escape(&1, &2, vars)) 40 | expr = {:{}, [], [:{}, [], list]} 41 | {expr, params} 42 | end 43 | 44 | # List 45 | defp escape(list, params, vars) when is_list(list) do 46 | Enum.map_reduce(list, params, &escape(&1, &2, vars)) 47 | end 48 | 49 | # var - where var is bound 50 | defp escape({var, _, context}, params, vars) 51 | when is_atom(var) and is_atom(context) do 52 | expr = Builder.escape_var(var, vars) 53 | {expr, params} 54 | end 55 | 56 | defp escape(other, params, vars) do 57 | Builder.escape(other, :any, params, vars) 58 | end 59 | 60 | @doc """ 61 | Builds a quoted expression. 62 | 63 | The quoted expression should evaluate to a query at runtime. 64 | If possible, it does all calculations at compile time to avoid 65 | runtime work. 66 | """ 67 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 68 | def build(query, binding, expr, env) do 69 | binding = Builder.escape_binding(binding) 70 | {expr, params} = escape(expr, binding) 71 | params = Builder.escape_params(params) 72 | 73 | select = quote do: %Ecto.Query.SelectExpr{ 74 | expr: unquote(expr), 75 | params: unquote(params), 76 | file: unquote(env.file), 77 | line: unquote(env.line)} 78 | Builder.apply_query(query, __MODULE__, [select], env) 79 | end 80 | 81 | @doc """ 82 | The callback applied by `build/4` to build the query. 83 | """ 84 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 85 | def apply(query, select) do 86 | query = Ecto.Queryable.to_query(query) 87 | 88 | if query.select do 89 | Builder.error! "only one select expression is allowed in query" 90 | else 91 | %{query | select: select} 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/from.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.From do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Handles from expressions. 8 | 9 | The expressions may either contain an `in` expression or not. 10 | The right side is always expected to Queryable. 11 | 12 | ## Examples 13 | 14 | iex> escape(quote do: MyModel) 15 | {[], quote(do: MyModel)} 16 | 17 | iex> escape(quote do: p in posts) 18 | {[p: 0], quote(do: posts)} 19 | 20 | iex> escape(quote do: [p, q] in posts) 21 | {[p: 0, q: 1], quote(do: posts)} 22 | 23 | iex> escape(quote do: [_, _] in abc) 24 | {[_: 0, _: 1], quote(do: abc)} 25 | 26 | iex> escape(quote do: other) 27 | {[], quote(do: other)} 28 | 29 | iex> escape(quote do: x() in other) 30 | ** (Ecto.Query.CompileError) binding list should contain only variables, got: x() 31 | 32 | """ 33 | @spec escape(Macro.t) :: {Keyword.t, Macro.t} 34 | def escape({:in, _, [var, expr]}) do 35 | {Builder.escape_binding(List.wrap(var)), expr} 36 | end 37 | 38 | def escape(expr) do 39 | {[], expr} 40 | end 41 | 42 | @doc """ 43 | Builds a quoted expression. 44 | 45 | The quoted expression should evaluate to a query at runtime. 46 | If possible, it does all calculations at compile time to avoid 47 | runtime work. 48 | """ 49 | @spec build(Macro.t, Macro.Env.t) :: {Macro.t, Keyword.t, non_neg_integer | nil} 50 | def build(expr, env) do 51 | {binds, expr} = escape(expr) 52 | 53 | {count_bind, quoted} = 54 | case Macro.expand(expr, env) do 55 | model when is_atom(model) -> 56 | # Get the source at runtime so no unnecessary compile time 57 | # dependencies between modules are added 58 | source = quote do: unquote(model).__schema__(:source) 59 | {1, query(source, model)} 60 | 61 | source when is_binary(source) -> 62 | # When a binary is used, there is no model 63 | {1, query(source, nil)} 64 | 65 | other -> 66 | {nil, other} 67 | end 68 | 69 | quoted = Builder.apply_query(quoted, __MODULE__, [length(binds)], env) 70 | {quoted, binds, count_bind} 71 | end 72 | 73 | defp query(source, model) do 74 | {:%, [], [Ecto.Query, {:%{}, [], [from: {source, model}]}]} 75 | end 76 | 77 | @doc """ 78 | The callback applied by `build/2` to build the query. 79 | """ 80 | @spec apply(Ecto.Queryable.t, non_neg_integer) :: Ecto.Query.t 81 | def apply(query, binds) do 82 | query = Ecto.Queryable.to_query(query) 83 | check_binds(query, binds) 84 | query 85 | end 86 | 87 | defp check_binds(query, count) do 88 | if count > 1 and count > Builder.count_binds(query) do 89 | Builder.error! "`from` in query expression specified #{count} " <> 90 | "binds but query contains #{Builder.count_binds(query)} binds" 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/ecto/adapters/mysql.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapters.MySQL do 2 | @moduledoc """ 3 | Adapter module for MySQL. 4 | 5 | It handles and pools the connections to the MySQL 6 | database using `mariaex` with `poolboy`. 7 | 8 | ## Options 9 | 10 | 11 | Mariaex options split in different categories described 12 | below. All options should be given via the repository 13 | configuration. 14 | 15 | ### Connection options 16 | 17 | * `:hostname` - Server hostname; 18 | * `:port` - Server port (default: 5432); 19 | * `:username` - Username; 20 | * `:password` - User password; 21 | * `:parameters` - Keyword list of connection parameters; 22 | * `:ssl` - Set to true if ssl should be used (default: false); 23 | * `:ssl_opts` - A list of ssl options, see ssl docs; 24 | 25 | ### Pool options 26 | 27 | * `:size` - The number of connections to keep in the pool; 28 | * `:max_overflow` - The maximum overflow of connections (see poolboy docs); 29 | * `:lazy` - If false all connections will be started immediately on Repo startup (default: true) 30 | 31 | ### Storage options 32 | 33 | * `:charset` - the database encoding (default: "utf8") 34 | * `:collation` - the collation order 35 | """ 36 | 37 | use Ecto.Adapters.SQL, :mariaex 38 | @behaviour Ecto.Adapter.Storage 39 | 40 | ## Storage API 41 | 42 | @doc false 43 | def storage_up(opts) do 44 | database = Keyword.fetch!(opts, :database) 45 | charset = Keyword.get(opts, :char_set, "utf8") 46 | 47 | extra = "" 48 | 49 | if collation = Keyword.get(opts, :collation) do 50 | extra = extra <> " DEFAULT COLLATE = #{collation}" 51 | end 52 | 53 | output = 54 | run_with_mysql opts, "CREATE DATABASE " <> database <> 55 | " DEFAULT CHARACTER SET = #{charset} " <> extra 56 | 57 | cond do 58 | String.length(output) == 0 -> :ok 59 | String.contains?(output, "database exists") -> {:error, :already_up} 60 | true -> {:error, output} 61 | end 62 | end 63 | 64 | 65 | @doc false 66 | def storage_down(opts) do 67 | output = run_with_mysql(opts, "DROP DATABASE #{opts[:database]}") 68 | 69 | cond do 70 | String.length(output) == 0 -> :ok 71 | String.contains?(output, "doesn't exist") -> {:error, :already_down} 72 | true -> {:error, output} 73 | end 74 | end 75 | 76 | defp run_with_mysql(database, sql_command) do 77 | env = [] 78 | 79 | if password = database[:password] do 80 | env = [{"MYSQL_PWD", password}|env] 81 | end 82 | 83 | if port = database[:port] do 84 | env = [{"MYSQL_TCP_PORT", port}|env] 85 | end 86 | 87 | args = ["--silent", "-u", database[:username], "-h", database[:hostname], "-e", sql_command] 88 | System.cmd("mysql", args, env: env, stderr_to_stdout: true) |> elem(0) 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/mix/tasks/ecto.create_drop_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Ecto.CreateDropTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Mix.Tasks.Ecto.Create 5 | alias Mix.Tasks.Ecto.Drop 6 | 7 | # Mocked adapters 8 | 9 | defmodule Adapter do 10 | @behaviour Ecto.Adapter.Storage 11 | defmacro __using__(_), do: :ok 12 | def storage_up(_), do: Process.get(:storage_up) || raise "no storage_up" 13 | def storage_down(_), do: Process.get(:storage_down) || raise "no storage_down" 14 | end 15 | 16 | defmodule NoStorageAdapter do 17 | defmacro __using__(_), do: :ok 18 | end 19 | 20 | # Mocked repos 21 | 22 | defmodule Repo do 23 | use Ecto.Repo, adapter: Adapter, otp_app: :ecto 24 | end 25 | 26 | Application.put_env(:ecto, Repo, []) 27 | 28 | defmodule NoStorageRepo do 29 | use Ecto.Repo, adapter: NoStorageAdapter, otp_app: :ecto 30 | end 31 | 32 | Application.put_env(:ecto, NoStorageRepo, []) 33 | 34 | ## Create 35 | 36 | test "runs the adapter storage_up" do 37 | Process.put(:storage_up, :ok) 38 | Create.run ["-r", to_string(Repo)] 39 | assert_received {:mix_shell, :info, ["The database for repo Mix.Tasks.Ecto.CreateDropTest.Repo has been created."]} 40 | end 41 | 42 | test "informs the user when the repo is already up" do 43 | Process.put(:storage_up, {:error, :already_up}) 44 | Create.run ["-r", to_string(Repo)] 45 | assert_received {:mix_shell, :info, ["The database for repo Mix.Tasks.Ecto.CreateDropTest.Repo has already been created."]} 46 | end 47 | 48 | test "raises an error when storage_up gives an unknown feedback" do 49 | Process.put(:storage_up, {:error, :confused}) 50 | assert_raise Mix.Error, fn -> 51 | Create.run ["-r", to_string(Repo)] 52 | end 53 | end 54 | 55 | test "raises an error on storage_up when the adapter doesn't define a storage" do 56 | assert_raise Mix.Error, ~r/to implement Ecto.Adapter.Storage/, fn -> 57 | Create.run ["-r", to_string(NoStorageRepo)] 58 | end 59 | end 60 | 61 | ## Down 62 | 63 | test "runs the adapter storage_down" do 64 | Process.put(:storage_down, :ok) 65 | Drop.run ["-r", to_string(Repo)] 66 | assert_received {:mix_shell, :info, ["The database for repo Mix.Tasks.Ecto.CreateDropTest.Repo has been dropped."]} 67 | end 68 | 69 | test "informs the user when the repo is already down" do 70 | Process.put(:storage_down, {:error, :already_down}) 71 | Drop.run ["-r", to_string(Repo)] 72 | assert_received {:mix_shell, :info, ["The database for repo Mix.Tasks.Ecto.CreateDropTest.Repo has already been dropped."]} 73 | end 74 | 75 | test "raises an error when storage_down gives an unknown feedback" do 76 | Process.put(:storage_down, {:error, :confused}) 77 | assert_raise Mix.Error, fn -> 78 | Drop.run ["-r", to_string(Repo)] 79 | end 80 | end 81 | 82 | test "raises an error on storage_down when the adapter doesn't define a storage" do 83 | assert_raise Mix.Error, ~r/to implement Ecto.Adapter.Storage/, fn -> 84 | Drop.run ["-r", to_string(NoStorageRepo)] 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/ecto/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.CompileError do 2 | @moduledoc """ 3 | Raised at compilation time when the query cannot be compiled. 4 | """ 5 | defexception [:message] 6 | end 7 | 8 | defmodule Ecto.QueryError do 9 | @moduledoc """ 10 | Raised at runtime when the query is invalid. 11 | """ 12 | defexception [:message] 13 | 14 | def exception(opts) do 15 | message = Keyword.fetch!(opts, :message) 16 | query = Keyword.fetch!(opts, :query) 17 | message = """ 18 | #{message} in query: 19 | 20 | #{Inspect.Ecto.Query.to_string(query)} 21 | """ 22 | 23 | if (file = opts[:file]) && (line = opts[:line]) do 24 | relative = Path.relative_to_cwd(file) 25 | message = Exception.format_file_line(relative, line) <> " " <> message 26 | end 27 | 28 | %__MODULE__{message: message} 29 | end 30 | end 31 | 32 | defmodule Ecto.CastError do 33 | @moduledoc """ 34 | Raised at runtime when a value cannot be cast. 35 | """ 36 | defexception [:model, :field, :type, :value, :message] 37 | 38 | def exception(opts) do 39 | model = Keyword.fetch!(opts, :model) 40 | field = Keyword.fetch!(opts, :field) 41 | value = Keyword.fetch!(opts, :value) 42 | type = Keyword.fetch!(opts, :type) 43 | msg = Keyword.fetch!(opts, :message) 44 | %__MODULE__{model: model, field: field, value: value, type: type, message: msg} 45 | end 46 | end 47 | 48 | defmodule Ecto.InvalidURLError do 49 | defexception [:message, :url] 50 | 51 | def exception(opts) do 52 | url = Keyword.fetch!(opts, :url) 53 | msg = Keyword.fetch!(opts, :message) 54 | msg = "invalid url #{url}, #{msg}" 55 | %__MODULE__{message: msg, url: url} 56 | end 57 | end 58 | 59 | defmodule Ecto.NoPrimaryKeyError do 60 | defexception [:message, :model] 61 | 62 | def exception(opts) do 63 | model = Keyword.fetch!(opts, :model) 64 | message = "model `#{inspect model}` has no primary key" 65 | %__MODULE__{message: message, model: model} 66 | end 67 | end 68 | 69 | defmodule Ecto.MissingPrimaryKeyError do 70 | defexception [:message, :struct] 71 | 72 | def exception(opts) do 73 | struct = Keyword.fetch!(opts, :struct) 74 | message = "struct `#{inspect struct}` is missing primary key value" 75 | %__MODULE__{message: message, struct: struct} 76 | end 77 | end 78 | 79 | 80 | defmodule Ecto.ChangeError do 81 | defexception [:message] 82 | end 83 | 84 | defmodule Ecto.NoResultsError do 85 | defexception [:message] 86 | 87 | def exception(opts) do 88 | query = Keyword.fetch!(opts, :queryable) |> Ecto.Queryable.to_query 89 | 90 | msg = """ 91 | expected at least one result but got none in query: 92 | 93 | #{Inspect.Ecto.Query.to_string(query)} 94 | """ 95 | 96 | %__MODULE__{message: msg} 97 | end 98 | end 99 | 100 | defmodule Ecto.MultipleResultsError do 101 | defexception [:message] 102 | 103 | def exception(opts) do 104 | query = Keyword.fetch!(opts, :queryable) |> Ecto.Queryable.to_query 105 | count = Keyword.fetch!(opts, :count) 106 | 107 | msg = """ 108 | expected at most one result but got #{count} in query: 109 | 110 | #{Inspect.Ecto.Query.to_string(query)} 111 | """ 112 | 113 | %__MODULE__{message: msg} 114 | end 115 | end 116 | 117 | defmodule Ecto.MigrationError do 118 | defexception [:message] 119 | end 120 | -------------------------------------------------------------------------------- /lib/ecto/adapters/sql/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapters.SQL.Connection do 2 | use Behaviour 3 | 4 | @doc """ 5 | Connects to the underlying database. 6 | 7 | Should return a process which is linked to 8 | the caller process or an error. 9 | """ 10 | defcallback connect(Keyword.t) :: {:ok, pid} | {:error, term} 11 | 12 | @doc """ 13 | Disconnects the given `pid`. 14 | 15 | If the given `pid` no longer exists, it should not raise. 16 | """ 17 | defcallback disconnect(pid) :: :ok 18 | 19 | @doc """ 20 | Executes the given query with params in connection. 21 | 22 | In case of success, it must return an `:ok` tuple containing 23 | a map with at least two keys: 24 | 25 | * `:num_rows` - the number of rows affected 26 | 27 | * `:rows` - the result set as a list. `nil` may be returned 28 | instead of the list if the command does not yield any row 29 | as result (but still yields the number of affected rows, 30 | like a `delete` command without returning would) 31 | """ 32 | defcallback query(pid, query :: binary, params :: [term], opts :: Keyword.t) :: 33 | {:ok, %{rows: nil | [tuple], num_rows: non_neg_integer}} | {:error, Exception.t} 34 | 35 | ## Queries 36 | 37 | @doc """ 38 | Receives a query and must return a SELECT query. 39 | """ 40 | defcallback all(Ecto.Query.t) :: String.t 41 | 42 | @doc """ 43 | Receives a query and values to update and must return an UPDATE query. 44 | """ 45 | defcallback update_all(Ecto.Query.t, values :: Keyword.t) :: String.t 46 | 47 | @doc """ 48 | Receives a query and must return a DELETE query. 49 | """ 50 | defcallback delete_all(Ecto.Query.t) :: String.t 51 | 52 | @doc """ 53 | Returns an INSERT for the given `fields` in `table` returning 54 | the given `returning`. 55 | """ 56 | defcallback insert(table :: String.t, fields :: [atom], returning :: [atom]) :: String.t 57 | 58 | @doc """ 59 | Returns an UPDATE for the given `fields` in `table` filtered by 60 | `filters` returning the given `returning`. 61 | """ 62 | defcallback update(table :: String.t, filters :: [atom], 63 | fields :: [atom], returning :: [atom]) :: String.t 64 | 65 | @doc """ 66 | Returns a DELETE for the given `fields` in `table` filtered by 67 | `filters` returning the given `returning`. 68 | """ 69 | defcallback delete(table :: String.t, filters :: [atom], returning :: [atom]) :: String.t 70 | 71 | ## DDL 72 | 73 | @doc """ 74 | Receives a DDL object and returns a query that checks its existence. 75 | """ 76 | defcallback ddl_exists(Ecto.Adapter.Migrations.ddl_object) :: String.t 77 | 78 | @doc """ 79 | Receives a DDL command and returns a query that executes it. 80 | """ 81 | defcallback execute_ddl(Ecto.Adapter.Migrations.command) :: String.t 82 | 83 | ## Transaction 84 | 85 | @doc """ 86 | Command to begin transaction. 87 | """ 88 | defcallback begin_transaction :: String.t 89 | 90 | @doc """ 91 | Command to rollback transaction. 92 | """ 93 | defcallback rollback :: String.t 94 | 95 | @doc """ 96 | Command to commit transaction. 97 | """ 98 | defcallback commit :: String.t 99 | 100 | @doc """ 101 | Command to emit savepoint. 102 | """ 103 | defcallback savepoint(savepoint :: String.t) :: String.t 104 | 105 | @doc """ 106 | Command to rollback to savepoint. 107 | """ 108 | defcallback rollback_to_savepoint(savepoint :: String.t) :: String.t 109 | end -------------------------------------------------------------------------------- /lib/ecto/adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapter do 2 | @moduledoc """ 3 | This module specifies the adapter API that an adapter is required to 4 | implement. 5 | """ 6 | 7 | use Behaviour 8 | 9 | @type t :: module 10 | 11 | @doc """ 12 | The callback invoked when the adapter is used. 13 | """ 14 | defmacrocallback __using__(opts :: Keyword.t) :: Macro.t 15 | 16 | @doc """ 17 | Starts any connection pooling or supervision and return `{:ok, pid}` 18 | or just `:ok` if nothing needs to be done. 19 | 20 | Returns `{:error, {:already_started, pid}}` if the repo already 21 | started or `{:error, term}` in case anything else goes wrong. 22 | """ 23 | defcallback start_link(repo :: Ecto.Repo.t, options :: Keyword.t) :: 24 | {:ok, pid} | :ok | {:error, {:already_started, pid}} | {:error, term} 25 | 26 | @doc """ 27 | Stops any connection pooling or supervision started with `start_link/1`. 28 | """ 29 | defcallback stop(repo :: Ecto.Repo.t) :: :ok 30 | 31 | @doc """ 32 | Fetches all results from the data store based on the given query. 33 | """ 34 | defcallback all(repo :: Ecto.Repo.t, query :: Ecto.Query.t, 35 | params :: map(), opts :: Keyword.t) :: [[term]] | no_return 36 | 37 | @doc """ 38 | Updates all entities matching the given query with the values given. The 39 | query shall only have `where` expressions and a single `from` expression. Returns 40 | the number of affected entities. 41 | """ 42 | defcallback update_all(repo :: Ecto.Repo.t, query :: Ecto.Query.t, 43 | filter :: Keyword.t, params :: map(), 44 | opts :: Keyword.t) :: integer | no_return 45 | 46 | @doc """ 47 | Deletes all entities matching the given query. 48 | 49 | The query shall only have `where` expressions and a `from` expression. 50 | Returns the number of affected entities. 51 | """ 52 | defcallback delete_all(repo :: Ecto.Repo.t, query :: Ecto.Query.t, 53 | params :: map(), opts :: Keyword.t) :: integer | no_return 54 | 55 | @doc """ 56 | Inserts a single new model in the data store. 57 | """ 58 | defcallback insert(repo :: Ecto.Repo.t, source :: binary, 59 | fields :: Keyword.t, returning :: [atom], 60 | opts :: Keyword.t) :: {:ok, tuple} | no_return 61 | 62 | @doc """ 63 | Updates a single model with the given filters. 64 | 65 | While `filter` can be any record column, it is expected that 66 | at least the primary key (or any other key that uniquely 67 | identifies an existing record) to be given as filter. Therefore, 68 | in case there is no record matching the given filters, 69 | `{:error, :stale}` is returned. 70 | """ 71 | defcallback update(repo :: Ecto.Repo.t, source :: binary, 72 | filter :: Keyword.t, fields :: Keyword.t, 73 | returning :: [atom], opts :: Keyword.t) :: 74 | {:ok, tuple} | {:error, :stale} | no_return 75 | 76 | @doc """ 77 | Deletes a sigle model with the given filters. 78 | 79 | While `filter` can be any record column, it is expected that 80 | at least the primary key (or any other key that uniquely 81 | identifies an existing record) to be given as filter. Therefore, 82 | in case there is no record matching the given filters, 83 | `{:error, :stale}` is returned. 84 | """ 85 | defcallback delete(repo :: Ecto.Repo.t, source :: binary, 86 | filter :: Keyword.t, opts :: Keyword.t) :: 87 | {:ok, tuple} | {:error, :stale} | no_return 88 | end 89 | -------------------------------------------------------------------------------- /lib/mix/ecto.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Ecto do 2 | # Conveniences for writing Mix.Tasks in Ecto. 3 | @moduledoc false 4 | 5 | @doc """ 6 | Parses the repository option from the given list. 7 | 8 | If no repo option is given, we get one from the environment. 9 | """ 10 | @spec parse_repo([term]) :: Ecto.Repo.t 11 | def parse_repo([key, value|_]) when key in ~w(--repo -r) do 12 | Module.concat([value]) 13 | end 14 | 15 | def parse_repo([_|t]) do 16 | parse_repo(t) 17 | end 18 | 19 | def parse_repo([]) do 20 | app = Mix.Project.config |> Keyword.fetch!(:app) 21 | 22 | case Application.get_env(app, :app_namespace, app) do 23 | ^app -> app |> to_string |> Mix.Utils.camelize 24 | mod -> mod |> inspect 25 | end |> Module.concat(Repo) 26 | end 27 | 28 | @doc """ 29 | Ensures the given module is a repository. 30 | """ 31 | @spec ensure_repo(module) :: Ecto.Repo.t | no_return 32 | def ensure_repo(repo) do 33 | case Code.ensure_compiled(repo) do 34 | {:module, _} -> 35 | if function_exported?(repo, :__repo__, 0) do 36 | repo 37 | else 38 | Mix.raise "module #{inspect repo} is not a Ecto.Repo. " <> 39 | "Please pass a proper repo with the -r option." 40 | end 41 | {:error, error} -> 42 | Mix.raise "could not load #{inspect repo}, error: #{inspect error}. " <> 43 | "Please pass a proper repo with the -r option." 44 | end 45 | end 46 | 47 | @doc """ 48 | Ensures the given repository is started and running. 49 | """ 50 | @spec ensure_started(Ecto.Repo.t) :: Ecto.Repo.t | no_return 51 | def ensure_started(repo) do 52 | case repo.start_link do 53 | :ok -> repo 54 | {:ok, _} -> repo 55 | {:error, {:already_started, _}} -> repo 56 | {:error, error} -> 57 | Mix.raise "could not start repo #{inspect repo}, error: #{inspect error}" 58 | end 59 | end 60 | 61 | @doc """ 62 | Gets the migrations path from a repository. 63 | """ 64 | @spec migrations_path(Ecto.Repo.t) :: String.t 65 | def migrations_path(repo) do 66 | Path.join(repo_priv(repo), "migrations") 67 | end 68 | 69 | @doc """ 70 | Returns the private repository path. 71 | """ 72 | def repo_priv(repo) do 73 | config = repo.config() 74 | 75 | Application.app_dir(Keyword.fetch!(config, :otp_app), 76 | config[:priv] || "priv/#{repo |> Module.split |> List.last |> Mix.Utils.underscore}") 77 | end 78 | 79 | @doc """ 80 | Asks if the user wants to open a file based on ECTO_EDITOR. 81 | """ 82 | @spec open?(binary) :: boolean 83 | def open?(file) do 84 | editor = System.get_env("ECTO_EDITOR") || "" 85 | if editor != "" do 86 | System.cmd(editor, [inspect(file)]) 87 | true 88 | else 89 | false 90 | end 91 | end 92 | 93 | @doc """ 94 | Gets a path relative to the application path. 95 | Raises on umbrella application. 96 | """ 97 | def no_umbrella!(task) do 98 | if Mix.Project.umbrella? do 99 | Mix.raise "cannot run task #{inspect task} from umbrella application" 100 | end 101 | end 102 | 103 | @doc """ 104 | Returns `true` if module implements behaviour. 105 | """ 106 | def ensure_implements(module, behaviour, message) do 107 | all = Keyword.take(module.__info__(:attributes), [:behaviour]) 108 | unless [behaviour] in Keyword.values(all) do 109 | Mix.raise "Expected #{inspect module} to implement #{inspect behaviour} " <> 110 | "in order to #{message}" 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/ecto/repo/assoc.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.Assoc do 2 | # The module invoked by repomodules 3 | # for association related functionality. 4 | @moduledoc false 5 | 6 | @doc """ 7 | Transforms a result set based on query assocs, loading 8 | the associations onto their parent model. 9 | """ 10 | @spec query([Ecto.Model.t], Ecto.Query.t) :: [Ecto.Model.t] 11 | def query(rows, query) 12 | 13 | def query([], _query), do: [] 14 | def query(rows, %{assocs: []}), do: rows 15 | 16 | def query(rows, %{assocs: assocs, sources: sources}) do 17 | # Pre-create rose tree of reflections and accumulator 18 | # dicts in the same structure as the fields tree 19 | refls = create_refls(0, assocs, sources) 20 | accs = create_accs(assocs) 21 | 22 | # Replace the dict in the accumulator by a list 23 | # We use it as a flag to store the substructs 24 | accs = put_elem(accs, 1, []) 25 | 26 | # Populate tree of dicts of associated entities from the result set 27 | {_keys, rows, sub_dicts} = Enum.reduce(rows, accs, fn row, acc -> 28 | merge(row, acc, 0) |> elem(0) 29 | end) 30 | 31 | # Retrieve and load the assocs from cached dictionaries recursively 32 | for {item, sub_structs} <- Enum.reverse(rows) do 33 | [load_assocs(item, sub_dicts, refls)|sub_structs] 34 | end 35 | end 36 | 37 | defp merge([struct|sub_structs], {keys, dict, sub_dicts}, parent_key) do 38 | if struct do 39 | child_key = Ecto.Model.primary_key(struct) || 40 | raise Ecto.MissingPrimaryKeyError, struct: struct 41 | end 42 | 43 | # Traverse sub_structs adding one by one to the tree. 44 | # Note we need to traverse even if we don't have a child_key 45 | # due to nested associations. 46 | {sub_dicts, sub_structs} = 47 | Enum.map_reduce sub_dicts, sub_structs, &merge(&2, &1, child_key) 48 | 49 | # Now if we have a struct and its parent key, we store the current 50 | # data unless we have already processed it. 51 | cache = {parent_key, child_key} 52 | 53 | if struct && parent_key && not HashSet.member?(keys, cache) do 54 | keys = HashSet.put(keys, cache) 55 | item = {child_key, struct} 56 | 57 | # If we have a list, we are at the root, 58 | # so we also store the sub structs 59 | dict = 60 | if is_list(dict) do 61 | [{item, sub_structs}|dict] 62 | else 63 | HashDict.update(dict, parent_key, [item], &[item|&1]) 64 | end 65 | end 66 | 67 | {{keys, dict, sub_dicts}, sub_structs} 68 | end 69 | 70 | defp load_assocs({child_key, struct}, sub_dicts, refls) do 71 | Enum.reduce :lists.zip(sub_dicts, refls), struct, fn 72 | {{_keys, dict, sub_dicts}, {refl, refls}}, acc -> 73 | loaded = 74 | dict 75 | |> HashDict.get(child_key, []) 76 | |> Enum.reverse() 77 | |> Enum.map(&load_assocs(&1, sub_dicts, refls)) 78 | 79 | if refl.cardinality == :one do 80 | loaded = List.first(loaded) 81 | end 82 | 83 | Map.put(acc, refl.field, loaded) 84 | end 85 | end 86 | 87 | defp create_refls(idx, fields, sources) do 88 | Enum.map(fields, fn {field, {child_idx, child_fields}} -> 89 | {_source, model} = elem(sources, idx) 90 | {model.__schema__(:association, field), 91 | create_refls(child_idx, child_fields, sources)} 92 | end) 93 | end 94 | 95 | defp create_accs(fields) do 96 | acc = Enum.map(fields, fn {_field, {_child_idx, child_fields}} -> 97 | create_accs(child_fields) 98 | end) 99 | 100 | {HashSet.new, HashDict.new, acc} 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /integration_test/cases/transaction.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Integration.TransactionTest do 2 | # We can keep this test async as long as it 3 | # is the only one access the transactions table 4 | use ExUnit.Case, async: true 5 | 6 | import Ecto.Query 7 | alias Ecto.Integration.PoolRepo 8 | alias Ecto.Integration.TestRepo 9 | 10 | defmodule UniqueError do 11 | defexception [:message] 12 | end 13 | 14 | setup do 15 | PoolRepo.delete_all "transactions" 16 | :ok 17 | end 18 | 19 | defmodule Trans do 20 | use Ecto.Model 21 | 22 | schema "transactions" do 23 | field :text, :string 24 | end 25 | end 26 | 27 | test "transaction returns value" do 28 | x = PoolRepo.transaction(fn -> 29 | PoolRepo.transaction(fn -> 30 | 42 31 | end) 32 | end) 33 | assert x == {:ok, {:ok, 42}} 34 | end 35 | 36 | test "transaction re-raises" do 37 | assert_raise UniqueError, fn -> 38 | PoolRepo.transaction(fn -> 39 | PoolRepo.transaction(fn -> 40 | raise UniqueError 41 | end) 42 | end) 43 | end 44 | end 45 | 46 | test "transaction commits" do 47 | PoolRepo.transaction(fn -> 48 | e = PoolRepo.insert(%Trans{text: "1"}) 49 | assert [^e] = PoolRepo.all(Trans) 50 | assert [] = TestRepo.all(Trans) 51 | end) 52 | 53 | assert [%Trans{text: "1"}] = TestRepo.all(Trans) 54 | end 55 | 56 | test "transaction rolls back" do 57 | try do 58 | PoolRepo.transaction(fn -> 59 | e = PoolRepo.insert(%Trans{text: "2"}) 60 | assert [^e] = PoolRepo.all(Trans) 61 | assert [] = TestRepo.all(Trans) 62 | raise UniqueError 63 | end) 64 | rescue 65 | UniqueError -> :ok 66 | end 67 | 68 | assert [] = TestRepo.all(Trans) 69 | end 70 | 71 | test "nested transaction partial roll back" do 72 | PoolRepo.transaction(fn -> 73 | e1 = PoolRepo.insert(%Trans{text: "3"}) 74 | assert [^e1] = PoolRepo.all(Trans) 75 | 76 | try do 77 | PoolRepo.transaction(fn -> 78 | e2 = PoolRepo.insert(%Trans{text: "4"}) 79 | assert [^e1, ^e2] = PoolRepo.all(from(t in Trans, order_by: t.text)) 80 | raise UniqueError 81 | end) 82 | rescue 83 | UniqueError -> :ok 84 | end 85 | 86 | e3 = PoolRepo.insert(%Trans{text: "5"}) 87 | assert [^e1, ^e3] = PoolRepo.all(from(t in Trans, order_by: t.text)) 88 | assert [] = TestRepo.all(Trans) 89 | end) 90 | 91 | assert [%Trans{text: "3"}, %Trans{text: "5"}] = TestRepo.all(from(t in Trans, order_by: t.text)) 92 | end 93 | 94 | test "manual rollback doesnt bubble up" do 95 | x = PoolRepo.transaction(fn -> 96 | e = PoolRepo.insert(%Trans{text: "6"}) 97 | assert [^e] = PoolRepo.all(Trans) 98 | PoolRepo.rollback(:oops) 99 | end) 100 | 101 | assert x == {:error, :oops} 102 | assert [] = TestRepo.all(Trans) 103 | end 104 | 105 | test "transactions are not shared in repo" do 106 | pid = self 107 | 108 | new_pid = spawn_link fn -> 109 | PoolRepo.transaction(fn -> 110 | e = PoolRepo.insert(%Trans{text: "7"}) 111 | assert [^e] = PoolRepo.all(Trans) 112 | send(pid, :in_transaction) 113 | receive do 114 | :commit -> :ok 115 | after 116 | 5000 -> raise "timeout" 117 | end 118 | end) 119 | send(pid, :commited) 120 | end 121 | 122 | receive do 123 | :in_transaction -> :ok 124 | after 125 | 5000 -> raise "timeout" 126 | end 127 | assert [] = PoolRepo.all(Trans) 128 | 129 | send(new_pid, :commit) 130 | receive do 131 | :commited -> :ok 132 | after 133 | 5000 -> raise "timeout" 134 | end 135 | 136 | assert [%Trans{text: "7"}] = PoolRepo.all(Trans) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecto 2 | 3 | [![Build Status](https://travis-ci.org/elixir-lang/ecto.svg?branch=master)](https://travis-ci.org/elixir-lang/ecto) 4 | [![Inline docs](http://inch-ci.org/github/elixir-lang/ecto.svg?branch=master&style=flat)](http://inch-ci.org/github/elixir-lang/ecto) 5 | 6 | Ecto is a domain specific language for writing queries and interacting with databases in Elixir. Here is an example: 7 | 8 | ```elixir 9 | # In your config/config.exs file 10 | config :my_app, Repo, 11 | database: "ecto_simple", 12 | username: "postgres", 13 | password: "postgres", 14 | hostname: "localhost" 15 | 16 | # In your application code 17 | defmodule Repo do 18 | use Ecto.Repo, 19 | otp_app: :my_app, 20 | adapter: Ecto.Adapters.Postgres 21 | end 22 | 23 | defmodule Weather do 24 | use Ecto.Model 25 | 26 | schema "weather" do 27 | field :city # Defaults to type :string 28 | field :temp_lo, :integer 29 | field :temp_hi, :integer 30 | field :prcp, :float, default: 0.0 31 | end 32 | end 33 | 34 | defmodule Simple do 35 | import Ecto.Query 36 | 37 | def sample_query do 38 | query = from w in Weather, 39 | where: w.prcp > 0 or is_nil(w.prcp), 40 | select: w 41 | Repo.all(query) 42 | end 43 | end 44 | ``` 45 | 46 | See the [online documentation](http://hexdocs.pm/ecto) or [run the sample application](https://github.com/elixir-lang/ecto/tree/master/examples/simple) for more information. 47 | 48 | ## Usage 49 | 50 | Add Ecto as a dependency in your `mix.exs` file. If you are using PostgreSQL, you will also need the library that Ecto's PostgreSQL adapter is using. 51 | 52 | ```elixir 53 | defp deps do 54 | [{:postgrex, ">= 0.0.0"}, 55 | {:ecto, "~> 0.5"}] 56 | end 57 | ``` 58 | 59 | You should also update your applications list to include both projects: 60 | 61 | ```elixir 62 | def application do 63 | [applications: [:postgrex, :ecto]] 64 | end 65 | ``` 66 | 67 | After you are done, run `mix deps.get` in your shell to fetch the dependencies. 68 | 69 | ## Supported databases 70 | 71 | The following databases are supported: 72 | 73 | Database | Elixir driver 74 | :---------------------- | :---------------------- 75 | PostgreSQL | [postgrex](http://github.com/ericmj/postgrex) 76 | 77 | We are currently looking for contributions to add support for other SQL databases and folks interested in exploring non-relational databases too. 78 | 79 | ## Important links 80 | 81 | * [Documentation](http://hexdocs.pm/ecto) 82 | * [Mailing list](https://groups.google.com/forum/#!forum/elixir-ecto) 83 | * [Examples](https://github.com/elixir-lang/ecto/tree/master/examples) 84 | 85 | ## Contributing 86 | 87 | Ecto is on the bleeding edge of Elixir so the latest master build is most likely needed, see [Elixir's README](https://github.com/elixir-lang/elixir) on how to build from source. 88 | 89 | To contribute you need to compile Ecto from source and test it: 90 | 91 | ``` 92 | $ git clone https://github.com/elixir-lang/ecto.git 93 | $ cd ecto 94 | $ mix test 95 | ``` 96 | 97 | Besides the unit tests above, it is recommended to run the adapter integration tests too: 98 | 99 | ``` 100 | # Run only PostgreSQL tests 101 | MIX_ENV=pg mix test 102 | 103 | # Run all tests (unit and all adapters) 104 | mix test.all 105 | ``` 106 | 107 | ## License 108 | 109 | Copyright 2012 Plataformatec 110 | 111 | Licensed under the Apache License, Version 2.0 (the "License"); 112 | you may not use this file except in compliance with the License. 113 | You may obtain a copy of the License at 114 | 115 | http://www.apache.org/licenses/LICENSE-2.0 116 | 117 | Unless required by applicable law or agreed to in writing, software 118 | distributed under the License is distributed on an "AS IS" BASIS, 119 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 120 | See the License for the specific language governing permissions and 121 | limitations under the License. 122 | -------------------------------------------------------------------------------- /lib/ecto/adapters/postgres.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapters.Postgres do 2 | @moduledoc """ 3 | Adapter module for PostgreSQL. 4 | 5 | It uses `postgrex` for communicating to the database 6 | and manages a connection pool with `poolboy`. 7 | 8 | ## Features 9 | 10 | * Full query support (including joins, preloads and associations) 11 | * Support for transactions 12 | * Support for data migrations 13 | * Support for ecto.create and ecto.drop operations 14 | * Support for transactional tests via `Ecto.Adapters.SQL` 15 | 16 | ## Options 17 | 18 | Postgres options split in different categories described 19 | below. All options should be given via the repository 20 | configuration. 21 | 22 | ### Connection options 23 | 24 | * `:hostname` - Server hostname 25 | * `:port` - Server port (default: 5432) 26 | * `:username` - Username 27 | * `:password` - User password 28 | * `:parameters` - Keyword list of connection parameters 29 | * `:ssl` - Set to true if ssl should be used (default: false) 30 | * `:ssl_opts` - A list of ssl options, see Erlang's `ssl` docs 31 | 32 | ### Pool options 33 | 34 | * `:size` - The number of connections to keep in the pool 35 | * `:max_overflow` - The maximum overflow of connections (see poolboy docs) 36 | * `:lazy` - If false all connections will be started immediately on Repo startup (default: true) 37 | 38 | ### Storage options 39 | 40 | * `:encoding` - the database encoding (default: "UTF8") 41 | * `:template` - the template to create the database from 42 | * `:lc_collate` - the collation order 43 | * `:lc_ctype` - the character classification 44 | 45 | """ 46 | 47 | use Ecto.Adapters.SQL, :postgrex 48 | @behaviour Ecto.Adapter.Storage 49 | 50 | ## Storage API 51 | 52 | @doc false 53 | def storage_up(opts) do 54 | database = Keyword.fetch!(opts, :database) 55 | encoding = Keyword.get(opts, :encoding, "UTF8") 56 | 57 | extra = "" 58 | 59 | if template = Keyword.get(opts, :template) do 60 | extra = extra <> " TEMPLATE=#{template}" 61 | end 62 | 63 | if lc_collate = Keyword.get(opts, :lc_collate) do 64 | extra = extra <> " LC_COLLATE='#{lc_collate}'" 65 | end 66 | 67 | if lc_ctype = Keyword.get(opts, :lc_ctype) do 68 | extra = extra <> " LC_CTYPE='#{lc_ctype}'" 69 | end 70 | 71 | {output, status} = 72 | run_with_psql opts, "CREATE DATABASE " <> database <> 73 | " ENCODING='#{encoding}'" <> extra 74 | 75 | cond do 76 | status == 0 -> :ok 77 | String.contains?(output, "already exists") -> {:error, :already_up} 78 | true -> {:error, output} 79 | end 80 | end 81 | 82 | @doc false 83 | def storage_down(opts) do 84 | {output, status} = run_with_psql(opts, "DROP DATABASE #{opts[:database]}") 85 | 86 | cond do 87 | status == 0 -> :ok 88 | String.contains?(output, "does not exist") -> {:error, :already_down} 89 | true -> {:error, output} 90 | end 91 | end 92 | 93 | defp run_with_psql(database, sql_command) do 94 | unless System.find_executable("psql") do 95 | raise "could not find executable `psql` in path, " <> 96 | "please guarantee it is available before running ecto commands" 97 | end 98 | 99 | env = 100 | if password = database[:password] do 101 | [{"PGPASSWORD", password}] 102 | else 103 | [] 104 | end 105 | 106 | args = [] 107 | 108 | if username = database[:username] do 109 | args = ["-U", username|args] 110 | end 111 | 112 | if port = database[:port] do 113 | args = ["-p", to_string(port)|args] 114 | end 115 | 116 | args = args ++ ["--quiet", "--host", database[:hostname], "-d", "template1", "-c", sql_command] 117 | System.cmd("psql", args, env: env, stderr_to_stdout: true) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/ecto/adapters/sql/worker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Adapters.SQL.WorkerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ecto.Adapters.SQL.Worker 5 | 6 | defmodule Connection do 7 | def connect(_opts) do 8 | Agent.start_link(fn -> [] end) 9 | end 10 | 11 | def disconnect(conn) do 12 | Agent.stop(conn) 13 | end 14 | 15 | def query(conn, "sleep", [value], opts) do 16 | Agent.get(conn, fn _ -> 17 | :timer.sleep(value) 18 | {:ok, %{}} 19 | end, opts[:timeout]) 20 | end 21 | 22 | def query(conn, query, [], opts) do 23 | Agent.update(conn, &[query|&1], opts[:timeout]) 24 | {:ok, %{}} 25 | end 26 | 27 | def begin_transaction, do: "BEGIN" 28 | def rollback, do: "ROLLBACK" 29 | def commit, do: "COMMIT" 30 | def savepoint(savepoint), do: "SAVEPOINT " <> savepoint 31 | def rollback_to_savepoint(savepoint), do: "ROLLBACK TO SAVEPOINT " <> savepoint 32 | end 33 | 34 | @opts [timeout: :infinity] 35 | 36 | test "worker starts without an active connection" do 37 | {:ok, worker} = Worker.start_link({Connection, []}) 38 | 39 | assert Process.alive?(worker) 40 | refute :sys.get_state(worker).conn 41 | end 42 | 43 | test "worker starts with an active connection" do 44 | {:ok, worker} = Worker.start_link({Connection, lazy: false}) 45 | 46 | assert Process.alive?(worker) 47 | assert :sys.get_state(worker).conn 48 | end 49 | 50 | test "worker survives, connection stops if caller dies" do 51 | {:ok, worker} = Worker.start({Connection, lazy: false}) 52 | conn = :sys.get_state(worker).conn 53 | conn_mon = Process.monitor(conn) 54 | worker_mon = Process.monitor(worker) 55 | 56 | spawn_link(fn -> 57 | Worker.link_me(worker, :infinity) 58 | end) 59 | 60 | assert_receive {:DOWN, ^conn_mon, :process, ^conn, _}, 1000 61 | refute_received {:DOWN, ^worker_mon, :process, ^worker, _} 62 | refute :sys.get_state(worker).conn 63 | end 64 | 65 | test "worker survives, caller dies if connection dies" do 66 | {:ok, worker} = Worker.start({Connection, lazy: false}) 67 | conn = :sys.get_state(worker).conn 68 | parent = self() 69 | 70 | caller = spawn_link(fn -> 71 | Worker.link_me(worker, :infinity) 72 | Worker.begin!(worker, @opts) 73 | send parent, :go_on 74 | :timer.sleep(:infinity) 75 | end) 76 | 77 | # Wait until caller is linked 78 | assert_receive :go_on, :infinity 79 | Process.unlink(caller) 80 | 81 | conn_mon = Process.monitor(conn) 82 | caller_mon = Process.monitor(caller) 83 | worker_mon = Process.monitor(worker) 84 | Process.exit(conn, :shutdown) 85 | 86 | assert_receive {:DOWN, ^conn_mon, :process, ^conn, _}, 1000 87 | assert_receive {:DOWN, ^caller_mon, :process, ^caller, _}, 1000 88 | refute_received {:DOWN, ^worker_mon, :process, ^worker, _} 89 | refute :sys.get_state(worker).conn 90 | end 91 | 92 | test "worker correctly manages transactions" do 93 | {:ok, worker} = Worker.start({Connection, lazy: false}) 94 | 95 | Worker.begin!(worker, @opts) 96 | Worker.begin!(worker, @opts) 97 | Worker.rollback!(worker, @opts) 98 | Worker.commit!(worker, @opts) 99 | 100 | assert commands(worker) == 101 | ["BEGIN", "SAVEPOINT ecto_1", "ROLLBACK TO SAVEPOINT ecto_1", "COMMIT"] 102 | 103 | Worker.begin!(worker, @opts) 104 | Worker.rollback!(worker, @opts) 105 | 106 | assert commands(worker) == ["BEGIN", "ROLLBACK"] 107 | end 108 | 109 | test "provides test transactions" do 110 | {:ok, worker} = Worker.start({Connection, lazy: false}) 111 | 112 | # Check for idempotent commands 113 | Worker.restart_test_transaction!(worker, @opts) 114 | Worker.rollback_test_transaction!(worker, @opts) 115 | Worker.begin_test_transaction!(worker, @opts) 116 | 117 | Worker.begin_test_transaction!(worker, @opts) 118 | Worker.restart_test_transaction!(worker, @opts) 119 | Worker.rollback_test_transaction!(worker, @opts) 120 | 121 | assert commands(worker) == ["BEGIN", "SAVEPOINT ecto_sandbox", 122 | "ROLLBACK TO SAVEPOINT ecto_sandbox", "ROLLBACK"] 123 | end 124 | 125 | defp commands(worker) do 126 | conn = :sys.get_state(worker).conn 127 | Agent.get_and_update(conn, fn commands -> {Enum.reverse(commands), []} end) 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/ecto/model.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Model do 2 | @moduledoc """ 3 | Provides convenience functions for defining and working 4 | with models. 5 | 6 | ## Using 7 | 8 | When used, `Ecto.Model` works as an "umbrella" module that adds 9 | common functionality to your module: 10 | 11 | * `use Ecto.Schema` - provides the API necessary to define schemas 12 | * `import Ecto.Changeset` - functions for building and manipulating changesets 13 | * `import Ecto.Model` - functions for working with models and their associations 14 | * `import Ecto.Query` - functions for generating and manipulating queries 15 | 16 | Plus all the modules existing in `Ecto.Model.*` are brought in 17 | too: 18 | 19 | * `use Ecto.Model.Callbacks` - provides lifecycle callbacks 20 | * `use Ecto.Model.Timestamps` - automatically set `inserted_at` and 21 | `updated_at` fields declared via `Ecto.Schema.timestamps/1` 22 | 23 | However, you can avoid using `Ecto.Model` altogether in favor 24 | of cherry picking any of the functionality above. 25 | 26 | ## Importing 27 | 28 | You may want to import this module in contexts where you are 29 | working with different models. For example, in a web application, 30 | you may want to import this module into your plugs to provide 31 | conveniences for building and accessing model information. 32 | """ 33 | 34 | @doc false 35 | defmacro __using__(_opts) do 36 | quote do 37 | use Ecto.Schema 38 | import Ecto.Changeset 39 | import Ecto.Query 40 | 41 | import Ecto.Model 42 | use Ecto.Model.Timestamps 43 | use Ecto.Model.Callbacks 44 | end 45 | end 46 | 47 | @type t :: %{__struct__: atom} 48 | 49 | @doc """ 50 | Returns the model primary key value. 51 | 52 | Raises `Ecto.NoPrimaryKeyError` if model has no primary key field. 53 | """ 54 | @spec primary_key(t) :: any 55 | def primary_key(struct) do 56 | Map.fetch!(struct, primary_key_field(struct)) 57 | end 58 | 59 | defp primary_key_field(%{__struct__: model}) do 60 | model.__schema__(:primary_key) || raise Ecto.NoPrimaryKeyError, model: model 61 | end 62 | 63 | @doc """ 64 | Builds a structs from the given `assoc` in `model`. 65 | 66 | ## Examples 67 | 68 | If the relationship is a `has_one` or `has_many` and 69 | the key is set in the given model, the key will automatically 70 | be set in the built association: 71 | 72 | iex> post = Repo.get(Post, 13) 73 | %Post{id: 13} 74 | iex> build(post, :comment) 75 | %Comment{id: nil, post_id: 13} 76 | 77 | Note though it doesn't happen with belongs to cases, as the 78 | key is often the primary key and such is usually generated 79 | dynamically: 80 | 81 | iex> comment = Repo.get(Post, 13) 82 | %Comment{id: 13, post_id: 25} 83 | iex> build(comment, :post) 84 | %Post{id: nil} 85 | """ 86 | def build(%{__struct__: model} = struct, assoc) do 87 | assoc = Ecto.Associations.association_from_model!(model, assoc) 88 | assoc.__struct__.build(assoc, struct) 89 | end 90 | 91 | @doc """ 92 | Builds a query for the association in the given model or models. 93 | 94 | ## Examples 95 | 96 | In the example below, we get all comments associated to the given 97 | post: 98 | 99 | post = Repo.get Post, 1 100 | Repo.all assoc(post, :comments) 101 | 102 | `assoc/2` can also receive a list of posts, as long as the posts are 103 | not empty: 104 | 105 | posts = Repo.all from p in Post, where: is_nil(p.published_at) 106 | Repo.all assoc(posts, :comments) 107 | 108 | """ 109 | def assoc(model_or_models, assoc) do 110 | structs = List.wrap(model_or_models) 111 | 112 | if structs == [] do 113 | raise ArgumentError, "cannot retrieve association #{inspect assoc} for empty list" 114 | end 115 | 116 | model = hd(structs).__struct__ 117 | assoc = %{owner_key: owner_key} = 118 | Ecto.Associations.association_from_model!(model, assoc) 119 | 120 | values = 121 | for struct <- structs, 122 | assert_struct!(model, struct), 123 | key = Map.fetch!(struct, owner_key), 124 | do: key 125 | 126 | assoc.__struct__.assoc_query(assoc, values) 127 | end 128 | 129 | defp assert_struct!(model, %{__struct__: struct}) do 130 | if struct != model do 131 | raise ArgumentError, "expected an homogeneous list containing the same struct, " <> 132 | "got: #{inspect model} and #{inspect struct}" 133 | else 134 | true 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/order_by.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.OrderBy do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | 6 | @doc """ 7 | Escapes an order by query. 8 | 9 | The query is escaped to a list of `{direction, expression}` 10 | pairs at runtime. Escaping also validates direction is one of 11 | `:asc` or `:desc`. 12 | 13 | ## Examples 14 | 15 | iex> escape(quote do [x.x, desc: 13] end, [x: 0]) 16 | {[asc: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]}, 17 | desc: 13], 18 | %{}} 19 | 20 | """ 21 | @spec escape(Macro.t, Keyword.t) :: Macro.t 22 | def escape({:^, _, [expr]}, _vars) do 23 | {quote(do: Ecto.Query.Builder.OrderBy.order_by!(unquote(expr))), %{}} 24 | end 25 | 26 | def escape(expr, vars) do 27 | List.wrap(expr) 28 | |> Enum.map_reduce(%{}, &do_escape(&1, &2, vars)) 29 | end 30 | 31 | defp do_escape({dir, {:^, _, [expr]}}, params, _vars) do 32 | {{quoted_dir!(dir), quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(expr)))}, params} 33 | end 34 | 35 | defp do_escape({:^, _, [expr]}, params, _vars) do 36 | {{:asc, quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(expr)))}, params} 37 | end 38 | 39 | defp do_escape({dir, field}, params, _vars) when is_atom(field) do 40 | {{quoted_dir!(dir), Macro.escape(to_field(field))}, params} 41 | end 42 | 43 | defp do_escape(field, params, _vars) when is_atom(field) do 44 | {{:asc, Macro.escape(to_field(field))}, params} 45 | end 46 | 47 | defp do_escape({dir, expr}, params, vars) do 48 | {ast, params} = Builder.escape(expr, :any, params, vars) 49 | {{quoted_dir!(dir), ast}, params} 50 | end 51 | 52 | defp do_escape(expr, params, vars) do 53 | {ast, params} = Builder.escape(expr, :any, params, vars) 54 | {{:asc, ast}, params} 55 | end 56 | 57 | @doc """ 58 | Checks the variable is a quoted direction at compilation time or 59 | delegate the check to runtime for interpolation. 60 | """ 61 | def quoted_dir!({:^, _, [expr]}), 62 | do: quote(do: Ecto.Query.Builder.OrderBy.dir!(unquote(expr))) 63 | def quoted_dir!(dir) when dir in [:asc, :desc], 64 | do: dir 65 | def quoted_dir!(other), 66 | do: Builder.error!("expected :asc, :desc or interpolated value in `order_by`, got: `#{inspect other}`") 67 | 68 | @doc """ 69 | Called by at runtime to verify the direction. 70 | """ 71 | def dir!(dir) when dir in [:asc, :desc], 72 | do: dir 73 | def dir!(other), 74 | do: Builder.error!("expected :asc or :desc in `order_by`, got: `#{inspect other}`") 75 | 76 | @doc """ 77 | Called at runtime to verify a field. 78 | """ 79 | def field!(field) when is_atom(field), 80 | do: to_field(field) 81 | def field!(other), 82 | do: Builder.error!("expected a field as an atom in `order_by`, got: `#{inspect other}`") 83 | 84 | @doc """ 85 | Called at runtime to verify order_by. 86 | """ 87 | def order_by!(order_by) do 88 | Enum.map List.wrap(order_by), fn 89 | {dir, field} when dir in [:asc, :desc] and is_atom(field) -> 90 | {dir, to_field(field)} 91 | field when is_atom(field) -> 92 | {:asc, to_field(field)} 93 | _ -> 94 | Builder.error!("expected a list or keyword list of fields in `order_by`, got: `#{inspect order_by}`") 95 | end 96 | end 97 | 98 | defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []} 99 | 100 | @doc """ 101 | Builds a quoted expression. 102 | 103 | The quoted expression should evaluate to a query at runtime. 104 | If possible, it does all calculations at compile time to avoid 105 | runtime work. 106 | """ 107 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 108 | def build(query, binding, expr, env) do 109 | binding = Builder.escape_binding(binding) 110 | {expr, params} = escape(expr, binding) 111 | params = Builder.escape_params(params) 112 | 113 | order_by = quote do: %Ecto.Query.QueryExpr{ 114 | expr: unquote(expr), 115 | params: unquote(params), 116 | file: unquote(env.file), 117 | line: unquote(env.line)} 118 | Builder.apply_query(query, __MODULE__, [order_by], env) 119 | end 120 | 121 | @doc """ 122 | The callback applied by `build/4` to build the query. 123 | """ 124 | @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 125 | def apply(query, expr) do 126 | query = Ecto.Queryable.to_query(query) 127 | %{query | order_bys: query.order_bys ++ [expr]} 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /integration_test/cases/migration.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test/support/file_helpers.exs", __DIR__ 2 | 3 | defmodule Ecto.Integration.MigrationTest do 4 | use Ecto.Integration.Case 5 | 6 | import Support.FileHelpers 7 | import Ecto.Migrator, only: [migrated_versions: 1] 8 | 9 | defmodule GoodMigration do 10 | use Ecto.Migration 11 | 12 | def up do 13 | table = table(:migrations_test) 14 | 15 | assert exists? table 16 | drop table 17 | refute exists? table 18 | 19 | create table do 20 | add :name, :text 21 | add :other, :text 22 | end 23 | 24 | alter table do 25 | modify :name, :string 26 | remove :other 27 | add :author, :string 28 | end 29 | 30 | index = index(:migrations_test, ["lower(author)"]) 31 | refute exists? index 32 | create index 33 | assert exists? index 34 | drop index 35 | refute exists? index 36 | end 37 | 38 | def down do 39 | drop table(:migrations_test) 40 | end 41 | end 42 | 43 | defmodule BadMigration do 44 | use Ecto.Migration 45 | 46 | def change do 47 | execute "CREATE WHAT" 48 | end 49 | end 50 | 51 | import Ecto.Migrator 52 | 53 | setup do 54 | Application.put_env(:elixir, :ansi_enabled, false) 55 | 56 | on_exit fn -> 57 | Application.delete_env(:elixir, :ansi_enabled) 58 | end 59 | end 60 | 61 | test "schema migration" do 62 | [migration] = TestRepo.all(Ecto.Migration.SchemaMigration) 63 | assert migration.version == 0 64 | assert migration.inserted_at 65 | end 66 | 67 | test "migrations up and down" do 68 | assert migrated_versions(TestRepo) == [0] 69 | assert up(TestRepo, 20080906120000, GoodMigration, log: false) == :ok 70 | 71 | assert migrated_versions(TestRepo) == [0, 20080906120000] 72 | assert up(TestRepo, 20080906120000, GoodMigration, log: false) == :already_up 73 | assert migrated_versions(TestRepo) == [0, 20080906120000] 74 | assert down(TestRepo, 20080906120001, GoodMigration, log: false) == :already_down 75 | assert migrated_versions(TestRepo) == [0, 20080906120000] 76 | assert down(TestRepo, 20080906120000, GoodMigration, log: false) == :ok 77 | assert migrated_versions(TestRepo) == [0] 78 | end 79 | 80 | test "bad migration" do 81 | assert catch_error(up(TestRepo, 20080906120000, BadMigration, log: false)) 82 | end 83 | 84 | test "run up to/step migration" do 85 | in_tmp fn path -> 86 | create_migration(47) 87 | create_migration(48) 88 | 89 | assert [47] = run(TestRepo, path, :up, step: 1, log: false) 90 | assert count_entries() == 1 91 | 92 | assert [48] = run(TestRepo, path, :up, to: 48, log: false) 93 | end 94 | end 95 | 96 | test "run down to/step migration" do 97 | in_tmp fn path -> 98 | migrations = [ 99 | create_migration(49), 100 | create_migration(50), 101 | ] 102 | 103 | assert [49, 50] = run(TestRepo, path, :up, all: true, log: false) 104 | purge migrations 105 | 106 | assert [50] = run(TestRepo, path, :down, step: 1, log: false) 107 | purge migrations 108 | 109 | assert count_entries() == 1 110 | assert [50] = run(TestRepo, path, :up, to: 50, log: false) 111 | end 112 | end 113 | 114 | test "runs all migrations" do 115 | in_tmp fn path -> 116 | migrations = [ 117 | create_migration(53), 118 | create_migration(54), 119 | ] 120 | 121 | assert [53, 54] = run(TestRepo, path, :up, all: true, log: false) 122 | assert [] = run(TestRepo, path, :up, all: true, log: false) 123 | purge migrations 124 | 125 | assert [54, 53] = run(TestRepo, path, :down, all: true, log: false) 126 | purge migrations 127 | 128 | assert count_entries() == 0 129 | assert [53, 54] = run(TestRepo, path, :up, all: true, log: false) 130 | end 131 | end 132 | 133 | defp count_entries() do 134 | import Ecto.Query, only: [from: 2] 135 | TestRepo.one! from p in "migrations_test", select: count(1) 136 | end 137 | 138 | defp create_migration(num) do 139 | module = Module.concat(__MODULE__, "Migration#{num}") 140 | 141 | File.write! "#{num}_migration.exs", """ 142 | defmodule #{module} do 143 | use Ecto.Migration 144 | 145 | def up do 146 | execute "INSERT INTO migrations_test (name) VALUES ('inserted')" 147 | end 148 | 149 | def down do 150 | execute "DELETE FROM migrations_test WHERE id IN (SELECT id FROM migrations_test LIMIT 1)" 151 | end 152 | end 153 | """ 154 | 155 | module 156 | end 157 | 158 | defp purge(modules) do 159 | Enum.each(List.wrap(modules), fn m -> 160 | :code.delete m 161 | :code.purge m 162 | end) 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/ecto/repo_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../support/mock_repo.exs", __DIR__ 2 | 3 | defmodule Ecto.RepoTest.MyModel do 4 | use Ecto.Model 5 | 6 | schema "my_model" do 7 | field :x, :string 8 | end 9 | end 10 | 11 | defmodule Ecto.RepoTest.MyModelNoPK do 12 | use Ecto.Model 13 | 14 | @primary_key false 15 | schema "my_model" do 16 | field :x, :string 17 | end 18 | end 19 | 20 | defmodule Ecto.RepoTest do 21 | use ExUnit.Case, async: true 22 | 23 | import Ecto.Query 24 | require Ecto.MockRepo, as: MockRepo 25 | 26 | alias Ecto.RepoTest.MyModel 27 | alias Ecto.RepoTest.MyModelNoPK 28 | 29 | test "needs model with primary key field" do 30 | model = %MyModelNoPK{x: "abc"} 31 | 32 | assert_raise Ecto.NoPrimaryKeyError, fn -> 33 | MockRepo.update(model) 34 | end 35 | 36 | assert_raise Ecto.NoPrimaryKeyError, fn -> 37 | MockRepo.delete(model) 38 | end 39 | 40 | assert_raise Ecto.NoPrimaryKeyError, fn -> 41 | MockRepo.get(MyModelNoPK, 123) 42 | end 43 | end 44 | 45 | test "works with primary key value" do 46 | model = %MyModel{id: 1, x: "abc"} 47 | MockRepo.update(model) 48 | MockRepo.delete(model) 49 | MockRepo.get(MyModel, 123) 50 | end 51 | 52 | test "fails without primary key value" do 53 | model = %MyModel{x: "abc"} 54 | 55 | assert_raise Ecto.MissingPrimaryKeyError, fn -> 56 | MockRepo.update(model) 57 | end 58 | 59 | assert_raise Ecto.MissingPrimaryKeyError, fn -> 60 | MockRepo.delete(model) 61 | end 62 | end 63 | 64 | test "validate model types" do 65 | model = %MyModel{x: 123} 66 | 67 | assert_raise Ecto.ChangeError, fn -> 68 | MockRepo.insert(model) 69 | end 70 | 71 | model = %MyModel{id: 1, x: 123} 72 | 73 | assert_raise Ecto.ChangeError, fn -> 74 | MockRepo.update(model) 75 | end 76 | end 77 | 78 | test "repo validates get" do 79 | MockRepo.get(MyModel, 123) 80 | 81 | message = ~r"value `:atom` in `where` cannot be cast to type :integer in query" 82 | assert_raise Ecto.CastError, message, fn -> 83 | MockRepo.get(MyModel, :atom) 84 | end 85 | 86 | message = ~r"expected a from expression with a model in query" 87 | assert_raise Ecto.QueryError, message, fn -> 88 | MockRepo.get(%Ecto.Query{}, :atom) 89 | end 90 | end 91 | 92 | test "repo validates update_all" do 93 | # Success 94 | MockRepo.update_all(e in MyModel, x: nil) 95 | MockRepo.update_all(e in MyModel, x: e.x) 96 | MockRepo.update_all(e in MyModel, x: "123") 97 | MockRepo.update_all(MyModel, x: "123") 98 | MockRepo.update_all("my_model", x: "123") 99 | 100 | query = from(e in MyModel, where: e.x == "123") 101 | MockRepo.update_all(query, x: "") 102 | 103 | # Failures 104 | message = "no fields given to `update_all`" 105 | assert_raise ArgumentError, message, fn -> 106 | MockRepo.update_all(from(e in MyModel, select: e), []) 107 | end 108 | 109 | assert_raise ArgumentError, "value `123` in `update_all` cannot be cast to type :string", fn -> 110 | MockRepo.update_all(p in MyModel, x: ^123) 111 | end 112 | 113 | message = ~r"only `where` expressions are allowed in query" 114 | assert_raise Ecto.QueryError, message, fn -> 115 | MockRepo.update_all(from(e in MyModel, order_by: e.x), x: "123") 116 | end 117 | 118 | message = "field `Ecto.RepoTest.MyModel.y` in `update_all` does not exist in the model source" 119 | assert_raise Ecto.ChangeError, message, fn -> 120 | MockRepo.update_all(p in MyModel, y: "123") 121 | end 122 | end 123 | 124 | test "repo validates delete_all" do 125 | # Success 126 | MockRepo.delete_all(MyModel) 127 | 128 | query = from(e in MyModel, where: e.x == "123") 129 | MockRepo.delete_all(query) 130 | 131 | # Failures 132 | assert_raise Ecto.QueryError, fn -> 133 | MockRepo.delete_all from(e in MyModel, select: e) 134 | end 135 | 136 | assert_raise Ecto.QueryError, fn -> 137 | MockRepo.delete_all from(e in MyModel, order_by: e.x) 138 | end 139 | end 140 | 141 | ## Changesets 142 | 143 | test "create and update accepts changesets" do 144 | valid = Ecto.Changeset.cast(%{}, %MyModel{id: 1}, [], []) 145 | MockRepo.insert(valid) 146 | MockRepo.update(valid) 147 | end 148 | 149 | test "create and update fail on invalid changeset" do 150 | invalid = %Ecto.Changeset{valid?: false, model: %MyModel{}} 151 | 152 | assert_raise ArgumentError, "cannot insert/update an invalid changeset", fn -> 153 | MockRepo.insert(invalid) 154 | end 155 | 156 | assert_raise ArgumentError, "cannot insert/update an invalid changeset", fn -> 157 | MockRepo.update(invalid) 158 | end 159 | end 160 | 161 | test "create and update fail on changeset without model" do 162 | invalid = %Ecto.Changeset{valid?: true, model: nil} 163 | 164 | assert_raise ArgumentError, "cannot insert/update a changeset without a model", fn -> 165 | MockRepo.insert(invalid) 166 | end 167 | 168 | assert_raise ArgumentError, "cannot insert/update a changeset without a model", fn -> 169 | MockRepo.update(invalid) 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/preload.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Preload do 2 | @moduledoc false 3 | alias Ecto.Query.Builder 4 | 5 | @doc """ 6 | Escapes a preload. 7 | 8 | A preload may be an atom, a list of atoms or a keyword list 9 | nested as a rose tree. 10 | 11 | iex> escape(:foo, []) 12 | {[:foo], []} 13 | 14 | iex> escape([foo: :bar], []) 15 | {[foo: [:bar]], []} 16 | 17 | iex> escape([foo: [:bar, bar: :bat]], []) 18 | {[foo: [:bar, bar: [:bat]]], []} 19 | 20 | iex> escape([foo: {:^, [], ["external"]}], []) 21 | {[foo: ["external"]], []} 22 | 23 | iex> escape([foo: [:bar, {:^, [], ["external"]}], baz: :bat], []) 24 | {[foo: [:bar, "external"], baz: [:bat]], []} 25 | 26 | iex> escape([foo: {:c, [], nil}], [c: 1]) 27 | {[], [foo: {1, []}]} 28 | 29 | iex> escape([foo: {{:c, [], nil}, bar: {:l, [], nil}}], [c: 1, l: 2]) 30 | {[], [foo: {1, [bar: {2, []}]}]} 31 | 32 | iex> escape([foo: {:c, [], nil}, bar: {:l, [], nil}], [c: 1, l: 2]) 33 | {[], [foo: {1, []}, bar: {2, []}]} 34 | 35 | iex> escape([foo: {{:c, [], nil}, :bar}], [c: 1]) 36 | ** (Ecto.Query.CompileError) cannot preload `:bar` inside join association preload 37 | 38 | iex> escape([foo: [bar: {:c, [], nil}]], [c: 1]) 39 | ** (Ecto.Query.CompileError) cannot preload join association `:bar` with binding `c` because parent preload is not a join association 40 | 41 | """ 42 | @spec escape(Macro.t, Keyword.t) :: {[Macro.t], [Macro.t]} | no_return 43 | def escape(preloads, vars) do 44 | {preloads, assocs} = escape(preloads, :both, [], [], vars) 45 | {Enum.reverse(preloads), Enum.reverse(assocs)} 46 | end 47 | 48 | defp escape(atom, mode, preloads, assocs, _vars) when is_atom(atom) do 49 | assert_preload!(mode, atom) 50 | {[atom|preloads], assocs} 51 | end 52 | 53 | defp escape(list, mode, preloads, assocs, vars) when is_list(list) do 54 | Enum.reduce list, {preloads, assocs}, fn item, acc -> 55 | escape_each(item, mode, acc, vars) 56 | end 57 | end 58 | 59 | defp escape({:^, _, [inner]} = expr, mode, preloads, assocs, _vars) do 60 | assert_preload!(mode, expr) 61 | {[inner|preloads], assocs} 62 | end 63 | 64 | defp escape(other, _mode, _preloads, _assocs, _vars) do 65 | Builder.error! "`#{Macro.to_string other}` is not a valid preload expression. " <> 66 | "preload expects an atom, a (nested) list of atoms or a (nested) " <> 67 | "keyword list with a binding, atoms or lists as values. " <> 68 | "Use ^ if you want to interpolate a value" 69 | end 70 | 71 | defp escape_each({atom, {var, _, context}}, mode, {preloads, assocs}, vars) 72 | when is_atom(atom) and is_atom(context) do 73 | assert_assoc!(mode, atom, var) 74 | idx = Builder.find_var!(var, vars) 75 | {preloads, [{atom, {idx, []}}|assocs]} 76 | end 77 | 78 | defp escape_each({atom, {{var, _, context}, list}}, mode, {preloads, assocs}, vars) 79 | when is_atom(atom) and is_atom(context) do 80 | assert_assoc!(mode, atom, var) 81 | idx = Builder.find_var!(var, vars) 82 | {[], inner_assocs} = escape(list, :assoc, [], [], vars) 83 | {preloads, 84 | [{atom, {idx, Enum.reverse(inner_assocs)}}|assocs]} 85 | end 86 | 87 | defp escape_each({atom, list}, mode, {preloads, assocs}, vars) when is_atom(atom) do 88 | assert_preload!(mode, {atom, list}) 89 | {inner_preloads, []} = escape(list, :preload, [], [], vars) 90 | {[{atom, Enum.reverse(inner_preloads)}|preloads], assocs} 91 | end 92 | 93 | defp escape_each(other, mode, {preloads, assocs}, vars) do 94 | escape(other, mode, preloads, assocs, vars) 95 | end 96 | 97 | defp assert_assoc!(mode, _atom, _var) when mode in [:both, :assoc], do: :ok 98 | defp assert_assoc!(_mode, atom, var) do 99 | Builder.error! "cannot preload join association `#{inspect atom}` with binding `#{var}` " <> 100 | "because parent preload is not a join association" 101 | end 102 | 103 | defp assert_preload!(mode, _term) when mode in [:both, :preload], do: :ok 104 | defp assert_preload!(_mode, term) do 105 | Builder.error! "cannot preload `#{Macro.to_string(term)}` inside join association preload" 106 | end 107 | 108 | @doc """ 109 | Applies the preloaded value into the query. 110 | 111 | The quoted expression should evaluate to a query at runtime. 112 | If possible, it does all calculations at compile time to avoid 113 | runtime work. 114 | """ 115 | @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 116 | def build(query, binding, expr, env) do 117 | binding = Builder.escape_binding(binding) 118 | {preloads, assocs} = escape(expr, binding) 119 | Builder.apply_query(query, __MODULE__, [Enum.reverse(preloads), Enum.reverse(assocs)], env) 120 | end 121 | 122 | @doc """ 123 | The callback applied by `build/4` to build the query. 124 | """ 125 | @spec apply(Ecto.Queryable.t, term, term) :: Ecto.Query.t 126 | def apply(query, preloads, assocs) do 127 | query = Ecto.Queryable.to_query(query) 128 | %{query | preloads: query.preloads ++ preloads, assocs: query.assocs ++ assocs} 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /test/ecto/model/callbacks_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../support/mock_repo.exs", __DIR__ 2 | alias Ecto.MockRepo 3 | 4 | defmodule Ecto.Model.CallbacksTest do 5 | use ExUnit.Case, async: true 6 | 7 | defmodule SomeCallback do 8 | use Ecto.Model 9 | 10 | schema "some_callback" do 11 | field :x, :string, default: "" 12 | end 13 | 14 | before_delete __MODULE__, :add_to_x 15 | before_delete __MODULE__, :add_to_x, [%{str: "2"}] 16 | before_delete :add_to_x 17 | before_delete :add_to_x, [%{str: "2"}] 18 | 19 | before_update :bad_callback 20 | 21 | def add_to_x(changeset, %{str: str} \\ %{str: "1"}) do 22 | update_in changeset.model.x, &(&1 <> "," <> str) 23 | end 24 | 25 | defp bad_callback(_changeset) do 26 | nil 27 | end 28 | end 29 | 30 | test "defines functions for callbacks" do 31 | assert function_exported?(SomeCallback, :before_delete, 1) 32 | end 33 | 34 | test "doesn't define callbacks for not-registered events" do 35 | refute function_exported?(SomeCallback, :after_delete, 1) 36 | end 37 | 38 | test "applies callbacks" do 39 | changeset = %Ecto.Changeset{model: %SomeCallback{x: "x"}} 40 | 41 | assert Ecto.Model.Callbacks.__apply__(SomeCallback, :before_delete, changeset) == 42 | %Ecto.Changeset{model: %SomeCallback{x: "x,1,2,1,2"}} 43 | end 44 | 45 | test "raises on bad callbacks" do 46 | msg = "expected `before_update` callbacks to return a Ecto.Changeset, got: nil" 47 | assert_raise ArgumentError, msg, fn -> 48 | Ecto.Model.Callbacks.__apply__(SomeCallback, :before_update, %Ecto.Changeset{}) 49 | end 50 | end 51 | 52 | ## Repo integration 53 | 54 | defmodule AllCallback do 55 | use Ecto.Model 56 | 57 | schema "all_callback" do 58 | field :x, :string, default: "" 59 | field :y, :string, default: "" 60 | field :z, :string, default: "" 61 | field :before, :any, virtual: true 62 | field :after, :any, virtual: true 63 | field :xyz, :string, virtual: true 64 | end 65 | 66 | before_insert __MODULE__, :changeset_before 67 | after_insert __MODULE__, :changeset_after 68 | before_update __MODULE__, :changeset_before 69 | after_update __MODULE__, :changeset_after 70 | before_delete __MODULE__, :changeset_before 71 | after_delete __MODULE__, :changeset_after 72 | after_load __MODULE__, :changeset_load 73 | 74 | def changeset_before(%{repo: MockRepo} = changeset) do 75 | put_in(changeset.model.before, changeset.changes) 76 | |> delete_change(:z) 77 | end 78 | 79 | def changeset_after(%{repo: MockRepo} = changeset) do 80 | put_in(changeset.model.after, changeset.changes) 81 | end 82 | 83 | def changeset_load(model) do 84 | Map.put(model, :xyz, model.x <> model.y <> model.z) 85 | end 86 | end 87 | 88 | test "wraps operations into transactions if callback present" do 89 | model = %SomeCallback{x: "x"} 90 | MockRepo.insert model 91 | refute_received {:transaction, _fun} 92 | 93 | model = %AllCallback{x: "x"} 94 | MockRepo.insert model 95 | assert_received {:transaction, _fun} 96 | end 97 | 98 | test "before_insert and after_insert with model" do 99 | model = %AllCallback{x: "x"} 100 | model = MockRepo.insert model 101 | assert model.before == %{x: "x", y: "", z: ""} 102 | assert model.after == %{x: "x", y: ""} 103 | 104 | model = %AllCallback{id: 1, x: "x"} 105 | model = MockRepo.insert model 106 | assert model.before == %{id: 1, x: "x", y: "", z: ""} 107 | assert model.after == %{id: 1, x: "x", y: ""} 108 | end 109 | 110 | test "before_update and after_update with model" do 111 | model = %AllCallback{id: 1, x: "x"} 112 | model = MockRepo.update model 113 | assert model.before == %{id: 1, x: "x", y: "", z: ""} 114 | assert model.after == %{id: 1, x: "x", y: ""} 115 | end 116 | 117 | test "before_delete and after_delete with model" do 118 | model = %AllCallback{id: 1, x: "x"} 119 | model = MockRepo.delete model 120 | assert model.before == %{} 121 | assert model.after == %{} 122 | end 123 | 124 | test "before_insert and after_insert with changeset" do 125 | changeset = Ecto.Changeset.cast(%{"y" => "y", "z" => "z"}, 126 | %AllCallback{x: "x", y: "z"}, ~w(y z), ~w()) 127 | model = MockRepo.insert changeset 128 | assert model.before == %{x: "x", y: "y", z: "z"} 129 | assert model.after == %{x: "x", y: "y"} 130 | assert model.x == "x" 131 | assert model.y == "y" 132 | assert model.z == "" 133 | end 134 | 135 | test "before_update and after_update with changeset" do 136 | changeset = Ecto.Changeset.cast(%{"y" => "y", "z" => "z"}, 137 | %AllCallback{id: 1, x: "x", y: "z"}, ~w(y z), ~w()) 138 | model = MockRepo.update changeset 139 | assert model.before == %{y: "y", z: "z"} 140 | assert model.after == %{y: "y"} 141 | assert model.x == "x" 142 | assert model.y == "y" 143 | assert model.z == "" 144 | end 145 | 146 | test "after_load with model" do 147 | model = %AllCallback{id: 1, x: "x", y: "y", z: "z"} 148 | model = MockRepo.insert model 149 | assert model.xyz == "xyz" 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/ecto/migration/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.Runner do 2 | # A GenServer responsible for running migrations 3 | # in either `:forward` or `:backward` directions. 4 | @moduledoc false 5 | 6 | use GenServer 7 | require Logger 8 | 9 | alias Ecto.Migration.Table 10 | alias Ecto.Migration.Index 11 | 12 | @opts [timeout: :infinity, log: false] 13 | 14 | @doc """ 15 | Runs the given migration. 16 | """ 17 | def run(repo, module, direction, operation, opts) do 18 | level = Keyword.get(opts, :log, :info) 19 | start_link(repo, direction, level) 20 | 21 | log(level, "== Running #{inspect module}.#{operation}/0 #{direction}") 22 | {time, _} = :timer.tc(module, operation, []) 23 | log(level, "== Migrated in #{inspect(div(time, 10000) / 10)}s") 24 | 25 | stop() 26 | end 27 | 28 | @doc """ 29 | Starts the runner for the specified repo. 30 | """ 31 | def start_link(repo, direction, level) do 32 | Agent.start_link(fn -> 33 | %{direction: direction, repo: repo, 34 | command: nil, subcommands: [], level: level} 35 | end, name: __MODULE__) 36 | end 37 | 38 | @doc """ 39 | Stops the runner. 40 | """ 41 | def stop() do 42 | Agent.stop(__MODULE__) 43 | end 44 | 45 | @doc """ 46 | Executes command tuples or strings. 47 | 48 | Ecto.MigrationError will be raised when the server 49 | is in `:backward` direction and `command` is irreversible. 50 | """ 51 | def execute(command) do 52 | {repo, direction, level} = repo_and_direction_and_level() 53 | execute_in_direction(repo, direction, level, command) 54 | end 55 | 56 | @doc """ 57 | Starts a command. 58 | """ 59 | def start_command(command) do 60 | Agent.update __MODULE__, &put_in(&1.command, command) 61 | end 62 | 63 | @doc """ 64 | Executes and clears current command. Must call `start_command/1` first. 65 | """ 66 | def end_command do 67 | command = 68 | Agent.get_and_update __MODULE__, fn state -> 69 | {operation, object} = state.command 70 | {{operation, object, Enum.reverse(state.subcommands)}, 71 | %{state | command: nil, subcommands: []}} 72 | end 73 | execute(command) 74 | end 75 | 76 | @doc """ 77 | Adds a subcommand to the current command. Must call `start_command/1` first. 78 | """ 79 | def subcommand(subcommand) do 80 | reply = 81 | Agent.get_and_update(__MODULE__, fn 82 | %{command: nil} = state -> 83 | {:error, state} 84 | state -> 85 | {:ok, update_in(state.subcommands, &[subcommand|&1])} 86 | end) 87 | 88 | case reply do 89 | :ok -> 90 | :ok 91 | :error -> 92 | raise Ecto.MigrationError, message: "cannot execute command outside of block" 93 | end 94 | end 95 | 96 | @doc """ 97 | Checks if a table or index exists. 98 | """ 99 | def exists?(object) do 100 | {repo, direction, _level} = repo_and_direction_and_level() 101 | exists = repo.adapter.ddl_exists?(repo, object, @opts) 102 | if direction == :forward, do: exists, else: !exists 103 | end 104 | 105 | ## Helpers 106 | 107 | defp repo_and_direction_and_level do 108 | Agent.get(__MODULE__, fn %{repo: repo, direction: direction, level: level} -> 109 | {repo, direction, level} 110 | end) 111 | end 112 | 113 | defp execute_in_direction(repo, :forward, level, command) do 114 | log_ddl(level, command) 115 | repo.adapter.execute_ddl(repo, command, @opts) 116 | end 117 | 118 | defp execute_in_direction(repo, :backward, level, command) do 119 | reversed = reverse(command) 120 | 121 | if reversed do 122 | log_ddl(level, reversed) 123 | repo.adapter.execute_ddl(repo, reversed, @opts) 124 | else 125 | raise Ecto.MigrationError, message: "cannot reverse migration command: #{inspect command}" 126 | end 127 | end 128 | 129 | defp reverse([]), do: [] 130 | defp reverse([h|t]) do 131 | if reversed = reverse(h) do 132 | [reversed|reverse(t)] 133 | end 134 | end 135 | 136 | defp reverse({:create, %Index{}=index}), do: {:drop, index} 137 | defp reverse({:drop, %Index{}=index}), do: {:create, index} 138 | defp reverse({:create, %Table{}=table, _columns}), do: {:drop, table} 139 | defp reverse({:add, name, _type, _opts}), do: {:remove, name} 140 | defp reverse({:alter, %Table{}=table, changes}) do 141 | if reversed = reverse(changes) do 142 | {:alter, table, reversed} 143 | end 144 | end 145 | 146 | defp reverse(_), do: false 147 | 148 | ## Logging 149 | 150 | defp log_ddl(level, ddl) when is_binary(ddl), 151 | do: log(level, "execute #{inspect ddl}") 152 | 153 | defp log_ddl(level, {:create, %Table{} = table, _}), 154 | do: log(level, "create table #{table.name}") 155 | defp log_ddl(level, {:alter, %Table{} = table, _}), 156 | do: log(level, "alter table #{table.name}") 157 | defp log_ddl(level, {:drop, %Table{} = table}), 158 | do: log(level, "drop table #{table.name}") 159 | 160 | defp log_ddl(level, {:create, %Index{} = index}), 161 | do: log(level, "create index #{index.name}") 162 | defp log_ddl(level, {:drop, %Index{} = index}), 163 | do: log(level, "drop index #{index.name}") 164 | 165 | defp log(false, _msg), do: :ok 166 | defp log(level, msg), do: Logger.log(level, msg) 167 | end 168 | -------------------------------------------------------------------------------- /lib/ecto/query/builder/join.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.Builder.Join do 2 | @moduledoc false 3 | 4 | alias Ecto.Query.Builder 5 | alias Ecto.Query.JoinExpr 6 | 7 | @doc """ 8 | Escapes a join expression (not including the `on` expression). 9 | 10 | It returns a tuple containing the binds, the on expression (if available) 11 | and the association expression. 12 | 13 | ## Examples 14 | 15 | iex> escape(quote(do: x in "foo"), []) 16 | {:x, {"foo", nil}, nil} 17 | 18 | iex> escape(quote(do: "foo"), []) 19 | {:_, {"foo", nil}, nil} 20 | 21 | iex> escape(quote(do: x in Sample), []) 22 | {:x, {nil, {:__aliases__, [alias: false], [:Sample]}}, nil} 23 | 24 | iex> escape(quote(do: c in assoc(p, :comments)), [p: 0]) 25 | {:c, nil, {0, :comments}} 26 | 27 | """ 28 | @spec escape(Macro.t, Keyword.t) :: {[atom], Macro.t | nil, Macro.t | nil} 29 | def escape({:in, _, [{var, _, context}, expr]}, vars) 30 | when is_atom(var) and is_atom(context) do 31 | {_, expr, assoc} = escape(expr, vars) 32 | {var, expr, assoc} 33 | end 34 | 35 | def escape({:__aliases__, _, _} = module, _vars) do 36 | {:_, {nil, module}, nil} 37 | end 38 | 39 | def escape(string, _vars) when is_binary(string) do 40 | {:_, {string, nil}, nil} 41 | end 42 | 43 | def escape({:assoc, _, [{var, _, context}, field]}, vars) 44 | when is_atom(var) and is_atom(context) do 45 | var = Builder.find_var!(var, vars) 46 | field = Builder.quoted_field!(field) 47 | {:_, nil, {var, field}} 48 | end 49 | 50 | def escape({:^, _, [expr]}, _vars) do 51 | {:_, quote(do: Ecto.Query.Builder.Join.join!(unquote(expr))), nil} 52 | end 53 | 54 | def escape(join, _vars) do 55 | Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression" 56 | end 57 | 58 | @doc """ 59 | Called at runtime to check dynamic joins. 60 | """ 61 | def join!(expr) when is_atom(expr), 62 | do: {nil, expr} 63 | def join!(expr) when is_binary(expr), 64 | do: {expr, nil} 65 | def join!(expr), 66 | do: Builder.error!("expected join to be a string or atom, got: `#{inspect expr}`") 67 | 68 | @doc """ 69 | Builds a quoted expression. 70 | 71 | The quoted expression should evaluate to a query at runtime. 72 | If possible, it does all calculations at compile time to avoid 73 | runtime work. 74 | """ 75 | @spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, Macro.Env.t) :: {Macro.t, Keyword.t, non_neg_integer | nil} 76 | def build(query, qual, binding, expr, on, count_bind, env) do 77 | binding = Builder.escape_binding(binding) 78 | {join_bind, join_expr, join_assoc} = escape(expr, binding) 79 | 80 | qual = validate_qual(qual) 81 | validate_bind(join_bind, binding) 82 | 83 | if join_bind != :_ and !count_bind do 84 | # If count_bind is not an integer, make it a variable. 85 | # The variable is the getter/setter storage. 86 | count_bind = quote(do: count_bind) 87 | count_setter = quote(do: unquote(count_bind) = Builder.count_binds(query)) 88 | end 89 | 90 | if on && join_assoc do 91 | Builder.error! "cannot specify `on` on `#{qual}_join` when using association join, " <> 92 | "add extra clauses with `where` instead" 93 | end 94 | 95 | binding = binding ++ [{join_bind, count_bind}] 96 | join_on = escape_on(on || true, binding, env) 97 | 98 | join = 99 | quote do 100 | %JoinExpr{qual: unquote(qual), source: unquote(join_expr), 101 | on: unquote(join_on), assoc: unquote(join_assoc), 102 | file: unquote(env.file), line: unquote(env.line)} 103 | end 104 | 105 | if is_integer(count_bind) do 106 | count_bind = count_bind + 1 107 | quoted = Builder.apply_query(query, __MODULE__, [join], env) 108 | else 109 | count_bind = quote(do: unquote(count_bind) + 1) 110 | quoted = 111 | quote do 112 | query = Ecto.Queryable.to_query(unquote(query)) 113 | unquote(count_setter) 114 | %{query | joins: query.joins ++ [unquote(join)]} 115 | end 116 | end 117 | 118 | {quoted, binding, count_bind} 119 | end 120 | 121 | def apply(query, expr) do 122 | query = Ecto.Queryable.to_query(query) 123 | %{query | joins: query.joins ++ [expr]} 124 | end 125 | 126 | defp escape_on(on, binding, env) do 127 | {on, params} = Builder.escape(on, :boolean, %{}, binding) 128 | params = Builder.escape_params(params) 129 | 130 | quote do: %Ecto.Query.QueryExpr{ 131 | expr: unquote(on), 132 | params: unquote(params), 133 | line: unquote(env.line), 134 | file: unquote(env.file)} 135 | end 136 | 137 | defp validate_qual(qual) when is_atom(qual) do 138 | qual!(qual) 139 | end 140 | 141 | defp validate_qual(qual) do 142 | quote(do: Ecto.Query.Builder.Join.qual!(unquote(qual))) 143 | end 144 | 145 | defp validate_bind(bind, all) do 146 | if bind != :_ and bind in all do 147 | Builder.error! "variable `#{bind}` is already defined in query" 148 | end 149 | end 150 | 151 | @qualifiers [:inner, :left, :right, :full] 152 | 153 | @doc """ 154 | Called at runtime to check dynamic qualifier. 155 | """ 156 | def qual!(qual) when qual in @qualifiers, do: qual 157 | def qual!(qual) do 158 | Builder.error! "invalid join qualifier `#{inspect qual}`, accepted qualifiers are: " <> 159 | Enum.map_join(@qualifiers, ", ", &"`#{inspect &1}`") 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/ecto/query/builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Query.BuilderTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Ecto.Query.Builder 5 | doctest Ecto.Query.Builder 6 | 7 | defp escape(quoted, vars) do 8 | escape(quoted, :any, %{}, vars) 9 | end 10 | 11 | test "escape" do 12 | assert {Macro.escape(quote do &0.y end), %{}} == 13 | escape(quote do x.y end, [x: 0]) 14 | 15 | assert {Macro.escape(quote do &0.y == &0.z end), %{}} == 16 | escape(quote do x.y == x.z end, [x: 0]) 17 | 18 | assert {Macro.escape(quote do &0.y == &1.z end), %{}} == 19 | escape(quote do x.y == y.z end, [x: 0, y: 1]) 20 | 21 | assert {Macro.escape(quote do avg(0) end), %{}} == 22 | escape(quote do avg(0) end, []) 23 | 24 | assert {Macro.escape(quote do fragment("date_add(", &0.created_at, ", ", ^0, ")") end), %{0 => {0, :any}}} == 25 | escape(quote do fragment("date_add(?, ?)", p.created_at, ^0) end, [p: 0]) 26 | 27 | assert {quote(do: ~s"123"), %{}} == 28 | escape(quote do ~s"123" end, []) 29 | 30 | assert {{:%, [], [Ecto.Query.Tagged, {:%{}, [], [value: "abc", type: :uuid, tag: :uuid]}]}, %{}} == 31 | escape(quote do uuid("abc") end, []) 32 | 33 | assert quote(do: &0.z) == 34 | escape(quote do field(x, :z) end, [x: 0]) |> elem(0) |> Code.eval_quoted([], __ENV__) |> elem(0) 35 | end 36 | 37 | test "escape type checks" do 38 | assert_raise Ecto.Query.CompileError, ~r"It returns a value of type :boolean but a value of type :integer is expected", fn -> 39 | escape(quote(do: ^1 == ^2), :integer, %{}, []) 40 | end 41 | 42 | assert_raise Ecto.Query.CompileError, ~r"It returns a value of type :boolean but a value of type :integer is expected", fn -> 43 | escape(quote(do: 1 > 2), :integer, %{}, []) 44 | end 45 | end 46 | 47 | test "escape raise" do 48 | assert_raise Ecto.Query.CompileError, ~r"variable `x` is not a valid query expression", fn -> 49 | escape(quote(do: x), []) 50 | end 51 | 52 | assert_raise Ecto.Query.CompileError, ~r"`:atom` is not a valid query expression", fn -> 53 | escape(quote(do: :atom), []) 54 | end 55 | 56 | assert_raise Ecto.Query.CompileError, ~r"`unknown\(1, 2\)` is not a valid query expression", fn -> 57 | escape(quote(do: unknown(1, 2)), []) 58 | end 59 | 60 | assert_raise Ecto.Query.CompileError, ~r"unbound variable", fn -> 61 | escape(quote(do: x.y), []) 62 | end 63 | 64 | assert_raise Ecto.Query.CompileError, ~r"unbound variable", fn -> 65 | escape(quote(do: x.y == 1), []) 66 | end 67 | 68 | assert_raise Ecto.Query.CompileError, ~r"expected literal atom or interpolated value", fn -> 69 | escape(quote(do: field(x, 123)), [x: 0]) |> elem(0) |> Code.eval_quoted([], __ENV__) 70 | end 71 | end 72 | 73 | test "doesn't escape interpolation" do 74 | assert {Macro.escape(quote(do: ^0)), %{0 => {quote(do: 1 == 2), :any}}} == 75 | escape(quote(do: ^(1 == 2)), []) 76 | 77 | assert {Macro.escape(quote(do: ^0)), %{0 => {quote(do: [] ++ []), :any}}} == 78 | escape(quote(do: ^([] ++ [])), []) 79 | 80 | assert {Macro.escape(quote(do: ^0 == ^1)), %{0 => {1, :any}, 1 => {2, :any}}} == 81 | escape(quote(do: ^1 == ^2), []) 82 | end 83 | 84 | defp params(quoted, type, vars \\ []) do 85 | escape(quoted, type, %{}, vars) |> elem(1) 86 | end 87 | 88 | test "infers the type for parameter" do 89 | assert params(quote(do: ^1 == 2), :any) == 90 | %{0 => {1, :integer}} 91 | 92 | assert params(quote(do: 2 == ^1), :any) == 93 | %{0 => {1, :integer}} 94 | 95 | assert params(quote(do: ^1 == ^2), :any) == 96 | %{0 => {1, :any}, 1 => {2, :any}} 97 | 98 | assert params(quote(do: ^1 == p.title), :any, [p: 0]) == 99 | %{0 => {1, {0, :title}}} 100 | 101 | assert params(quote(do: ^1 and true), :any) == 102 | %{0 => {1, :boolean}} 103 | 104 | assert params(quote(do: ^1), :boolean) == 105 | %{0 => {1, :boolean}} 106 | end 107 | 108 | test "returns the type for quoted query expression" do 109 | assert quoted_type({:<<>>, [], [1, 2, 3]}, []) == :binary 110 | assert quoted_type({:type, [], ["foo", :hello]}, []) == :hello 111 | 112 | assert quoted_type(1, []) == :integer 113 | assert quoted_type(1.0, []) == :float 114 | assert quoted_type("foo", []) == :string 115 | assert quoted_type(true, []) == :boolean 116 | assert quoted_type(false, []) == :boolean 117 | 118 | assert quoted_type([1, 2, 3], []) == {:array, :integer} 119 | assert quoted_type([1, 2.0, 3], []) == {:array, :any} 120 | 121 | assert quoted_type({:sigil_w, [], ["foo", []]}, []) == {:array, :string} 122 | assert quoted_type({:sigil_s, [], ["foo", []]}, []) == :string 123 | 124 | assert quoted_type({:==, [], [1, 2]}, []) == :boolean 125 | assert quoted_type({:like, [], [1, 2]}, []) == :boolean 126 | assert quoted_type({:and, [], [1, 2]}, []) == :boolean 127 | assert quoted_type({:or, [], [1, 2]}, []) == :boolean 128 | assert quoted_type({:not, [], [1]}, []) == :boolean 129 | assert quoted_type({:avg, [], [1]}, []) == :any 130 | 131 | assert quoted_type({{:., [], [{:p, [], Elixir}, :title]}, [], []}, [p: 0]) == {0, :title} 132 | assert quoted_type({:field, [], [{:p, [], Elixir}, :title]}, [p: 0]) == {0, :title} 133 | assert quoted_type({:field, [], [{:p, [], Elixir}, {:^, [], [:title]}]}, [p: 0]) == {0, :title} 134 | 135 | assert quoted_type({:unknown, [], []}, []) == :any 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/ecto/query/inspect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Inspect.Post do 2 | use Ecto.Model 3 | 4 | schema "posts" do 5 | has_many :comments, Inspect.Comment 6 | has_one :post, Inspect.Post 7 | end 8 | end 9 | 10 | defmodule Inspect.Comment do 11 | use Ecto.Model 12 | 13 | schema "comments" do 14 | end 15 | end 16 | 17 | defmodule Ecto.Query.InspectTest do 18 | use ExUnit.Case, async: true 19 | import Ecto.Query 20 | 21 | alias Inspect.Post 22 | alias Inspect.Comment 23 | 24 | test "from" do 25 | assert i(from(Post, [])) == 26 | ~s{from p in Inspect.Post} 27 | 28 | assert i(from(x in Post, [])) == 29 | ~s{from p in Inspect.Post} 30 | 31 | assert i(from(x in "posts", [])) == 32 | ~s{from p in "posts"} 33 | end 34 | 35 | test "join" do 36 | assert i(from(x in Post, join: y in Comment, on: x.id == y.id)) == 37 | ~s{from p in Inspect.Post, join: c in Inspect.Comment, on: p.id == c.id} 38 | 39 | assert i(from(x in Post, join: y in assoc(x, :comments))) == 40 | ~s{from p in Inspect.Post, join: c in assoc(p, :comments)} 41 | 42 | assert i(from(x in Post, join: y in assoc(x, :post), join: z in assoc(y, :post))) == 43 | ~s{from p0 in Inspect.Post, join: p1 in assoc(p0, :post), join: p2 in assoc(p1, :post)} 44 | 45 | assert i(from(x in Post, left_join: y in assoc(x, :comments))) == 46 | ~s{from p in Inspect.Post, left_join: c in assoc(p, :comments)} 47 | end 48 | 49 | test "where" do 50 | assert i(from(x in Post, where: x.foo == x.bar, where: true)) == 51 | ~s{from p in Inspect.Post, where: p.foo == p.bar, where: true} 52 | end 53 | 54 | test "group by" do 55 | assert i(from(x in Post, group_by: [x.foo, x.bar], group_by: x.foobar)) == 56 | ~s{from p in Inspect.Post, group_by: [p.foo, p.bar], group_by: [p.foobar]} 57 | end 58 | 59 | test "having" do 60 | assert i(from(x in Post, having: x.foo == x.bar, having: true)) == 61 | ~s{from p in Inspect.Post, having: p.foo == p.bar, having: true} 62 | end 63 | 64 | test "order by" do 65 | assert i(from(x in Post, order_by: [asc: x.foo, desc: x.bar], order_by: x.foobar)) == 66 | ~s{from p in Inspect.Post, order_by: [asc: p.foo, desc: p.bar], order_by: [asc: p.foobar]} 67 | end 68 | 69 | test "limit" do 70 | assert i(from(x in Post, limit: 123)) == 71 | ~s{from p in Inspect.Post, limit: 123} 72 | end 73 | 74 | test "offset" do 75 | assert i(from(x in Post, offset: 123)) == 76 | ~s{from p in Inspect.Post, offset: 123} 77 | end 78 | 79 | test "lock" do 80 | assert i(from(x in Post, lock: true)) == 81 | ~s{from p in Inspect.Post, lock: true} 82 | 83 | assert i(from(x in Post, lock: "FOOBAR")) == 84 | ~s{from p in Inspect.Post, lock: "FOOBAR"} 85 | end 86 | 87 | test "preload" do 88 | assert i(from(x in Post, preload: :comments)) == 89 | ~s"from p in Inspect.Post, preload: [:comments]" 90 | 91 | assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: y])) == 92 | ~s"from p in Inspect.Post, join: c in assoc(p, :comments), preload: [comments: c]" 93 | 94 | assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: {y, post: x}])) == 95 | ~s"from p in Inspect.Post, join: c in assoc(p, :comments), preload: [comments: {c, [post: p]}]" 96 | end 97 | 98 | test "fragments" do 99 | value = "foobar" 100 | assert i(from(x in Post, where: fragment("downcase(?) == ?", x.id, ^value))) == 101 | ~s{from p in Inspect.Post, where: fragment("downcase(?) == ?", p.id, ^"foobar")} 102 | end 103 | 104 | test "inspect all" do 105 | string = """ 106 | from p in Inspect.Post, join: c in assoc(p, :comments), where: true, 107 | group_by: [p.id], having: true, order_by: [asc: p.id], limit: 1, 108 | offset: 1, lock: true, distinct: [1], select: 1, preload: [:likes], preload: [comments: c] 109 | """ 110 | |> String.rstrip 111 | |> String.replace("\n", " ") 112 | 113 | assert i(from(x in Post, join: y in assoc(x, :comments), where: true, group_by: x.id, 114 | having: true, order_by: x.id, limit: 1, offset: 1, 115 | lock: true, select: 1, distinct: 1, preload: [:likes, comments: y])) == string 116 | end 117 | 118 | test "to_string all" do 119 | string = """ 120 | from p in Inspect.Post, 121 | join: c in assoc(p, :comments), 122 | where: true, 123 | group_by: [p.id], 124 | having: true, 125 | order_by: [asc: p.id], 126 | limit: 1, 127 | offset: 1, 128 | lock: true, 129 | distinct: [1], 130 | select: 1, 131 | preload: [:likes], 132 | preload: [comments: c] 133 | """ 134 | |> String.rstrip 135 | 136 | assert Inspect.Ecto.Query.to_string( 137 | from(x in Post, join: y in assoc(x, :comments), where: true, group_by: x.id, 138 | having: true, order_by: x.id, limit: 1, offset: 1, 139 | lock: true, distinct: 1, select: 1, preload: [:likes, comments: y]) 140 | ) == string 141 | end 142 | 143 | test "container values" do 144 | assert i(from(Post, select: {<<1, 2, 3>>, uuid(<<0>>), [0]})) == 145 | "from p in Inspect.Post, select: {<<1, 2, 3>>, uuid(<<0>>), [0]}" 146 | 147 | foo = <<1, 2, 3>> 148 | assert i(from(Post, select: type(^foo, :uuid))) == 149 | "from p in Inspect.Post, select: type(^<<1, 2, 3>>, :uuid)" 150 | end 151 | 152 | test "params" do 153 | assert i(from(x in Post, where: ^123 > ^(1 * 3))) == 154 | ~s{from p in Inspect.Post, where: ^123 > ^3} 155 | end 156 | 157 | test "params after prepare" do 158 | query = from(x in Post, where: ^123 > ^(1 * 3)) 159 | {query, _params} = Ecto.Query.Planner.prepare(query, %{}) 160 | assert i(query) == ~s{from p in Inspect.Post, where: ^... > ^...} 161 | end 162 | 163 | def i(query) do 164 | assert "#Ecto.Query<" <> rest = inspect query 165 | size = byte_size(rest) 166 | assert ">" = :binary.part(rest, size-1, 1) 167 | :binary.part(rest, 0, size-1) 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /test/ecto/migration_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../support/mock_repo.exs", __DIR__ 2 | 3 | defmodule Ecto.MigrationTest do 4 | # Although this test uses the Ecto.Migration.Runner which 5 | # is global state, we can run it async as long as this is 6 | # the only test case that uses the Runner in async mode. 7 | use ExUnit.Case, async: true 8 | 9 | use Ecto.Migration 10 | 11 | alias Ecto.MockRepo 12 | alias Ecto.Migration.Table 13 | alias Ecto.Migration.Index 14 | alias Ecto.Migration.Reference 15 | 16 | setup meta do 17 | {:ok, _} = Ecto.Migration.Runner.start_link(MockRepo, meta[:direction] || :forward, false) 18 | 19 | on_exit fn -> 20 | try do 21 | Ecto.Migration.Runner.stop() 22 | catch 23 | :exit, _ -> :ok 24 | end 25 | end 26 | 27 | :ok 28 | end 29 | 30 | test "defines __migration__ function" do 31 | assert function_exported?(__MODULE__, :__migration__, 0) 32 | end 33 | 34 | test "creates a table" do 35 | assert table(:posts) == %Table{name: :posts, primary_key: true} 36 | assert table(:posts, primary_key: false) == %Table{name: :posts, primary_key: false} 37 | end 38 | 39 | test "creates an index" do 40 | assert index(:posts, [:title]) == 41 | %Index{table: :posts, unique: false, name: :posts_title_index, columns: [:title]} 42 | assert index(:posts, ["lower(title)"]) == 43 | %Index{table: :posts, unique: false, name: :posts_lower_title_index, columns: ["lower(title)"]} 44 | assert index(:posts, [:title], name: :foo, unique: true) == 45 | %Index{table: :posts, unique: true, name: :foo, columns: [:title]} 46 | end 47 | 48 | test "creates a reference" do 49 | assert references(:posts) == 50 | %Reference{table: :posts, column: :id, type: :integer} 51 | assert references(:posts, type: :uuid, column: :other) == 52 | %Reference{table: :posts, column: :other, type: :uuid} 53 | end 54 | 55 | ## Forward 56 | @moduletag direction: :forward 57 | 58 | test "forward: executes the given SQL" do 59 | execute "HELLO, IS IT ME YOU ARE LOOKING FOR?" 60 | assert last_command() == "HELLO, IS IT ME YOU ARE LOOKING FOR?" 61 | end 62 | 63 | test "forward: table exists?" do 64 | assert exists?(table(:hello)) 65 | assert %Table{name: :hello} = last_exists() 66 | end 67 | 68 | test "forward: index exists?" do 69 | assert exists?(index(:hello, [:world])) 70 | assert %Index{table: :hello} = last_exists() 71 | end 72 | 73 | test "forward: creates a table" do 74 | create table = table(:posts) do 75 | add :title 76 | add :cost, :decimal, precision: 3 77 | add :author_id, references(:authors) 78 | timestamps 79 | end 80 | 81 | assert last_command() == 82 | {:create, table, 83 | [{:add, :id, :serial, [primary_key: true]}, 84 | {:add, :title, :string, []}, 85 | {:add, :cost, :decimal, [precision: 3]}, 86 | {:add, :author_id, %Reference{table: :authors}, []}, 87 | {:add, :inserted_at, :datetime, [null: false]}, 88 | {:add, :updated_at, :datetime, [null: false]}]} 89 | 90 | create table = table(:posts, primary_key: false, timestamps: false) do 91 | add :title 92 | end 93 | 94 | assert last_command() == 95 | {:create, table, 96 | [{:add, :title, :string, []}]} 97 | end 98 | 99 | test "forward: alters a table" do 100 | alter table(:posts) do 101 | add :summary, :text 102 | modify :title, :text 103 | remove :views 104 | end 105 | 106 | assert last_command() == 107 | {:alter, %Table{name: :posts}, 108 | [{:add, :summary, :text, []}, 109 | {:modify, :title, :text, []}, 110 | {:remove, :views}]} 111 | end 112 | 113 | test "forward: drops a table" do 114 | drop table(:posts) 115 | assert {:drop, %Table{}} = last_command() 116 | end 117 | 118 | test "forward: creates an index" do 119 | create index(:posts, [:title]) 120 | assert {:create, %Index{}} = last_command() 121 | end 122 | 123 | test "forward: drops an index" do 124 | drop index(:posts, [:title]) 125 | assert {:drop, %Index{}} = last_command() 126 | end 127 | 128 | ## Reverse 129 | @moduletag direction: :backward 130 | 131 | test "backward: fails when executing SQL" do 132 | assert_raise Ecto.MigrationError, ~r/cannot reverse migration command/, fn -> 133 | execute "HELLO, IS IT ME YOU ARE LOOKING FOR?" 134 | end 135 | end 136 | 137 | test "backward: table exists?" do 138 | refute exists?(table(:hello)) 139 | assert %Table{name: :hello} = last_exists() 140 | end 141 | 142 | test "backward: index exists?" do 143 | refute exists?(index(:hello, [:world])) 144 | assert %Index{table: :hello} = last_exists() 145 | end 146 | 147 | test "backward: creates a table" do 148 | create table(:posts) do 149 | add :title 150 | add :cost, :decimal, precision: 3 151 | end 152 | 153 | assert last_command() == 154 | {:drop, %Ecto.Migration.Table{name: :posts, primary_key: true}} 155 | end 156 | 157 | test "backward: alters a table" do 158 | alter table(:posts) do 159 | add :summary, :text 160 | end 161 | 162 | assert last_command() == 163 | {:alter, %Table{name: :posts}, 164 | [{:remove, :summary}]} 165 | 166 | assert_raise Ecto.MigrationError, ~r/cannot reverse migration command/, fn -> 167 | alter table(:posts) do 168 | remove :summary 169 | end 170 | end 171 | end 172 | 173 | test "backward: drops a table" do 174 | assert_raise Ecto.MigrationError, ~r/cannot reverse migration command/, fn -> 175 | drop table(:posts) 176 | end 177 | end 178 | 179 | test "backward: creates an index" do 180 | create index(:posts, [:title]) 181 | assert {:drop, %Index{}} = last_command() 182 | end 183 | 184 | test "backward: drops an index" do 185 | drop index(:posts, [:title]) 186 | assert {:create, %Index{}} = last_command() 187 | end 188 | 189 | defp last_exists(), do: Process.get(:last_exists) 190 | defp last_command(), do: Process.get(:last_command) 191 | end 192 | -------------------------------------------------------------------------------- /lib/ecto/repo/model.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.Model do 2 | # The module invoked by user defined repos 3 | # for model related functionality. 4 | @moduledoc false 5 | 6 | alias Ecto.Query.Planner 7 | alias Ecto.Model.Callbacks 8 | 9 | @doc """ 10 | Implementation for `Ecto.Repo.insert/2`. 11 | """ 12 | def insert(repo, adapter, %Ecto.Changeset{} = changeset, opts) when is_list(opts) do 13 | struct = struct_from_changeset!(changeset) 14 | model = struct.__struct__ 15 | fields = model.__schema__(:fields) 16 | source = model.__schema__(:source) 17 | return = handle_returning(adapter, model) 18 | 19 | # On insert, we always merge the whole struct into the 20 | # changeset as changes, except the primary key if it is nil. 21 | changeset = %{changeset | repo: repo} 22 | changeset = merge_into_changeset(model, struct, fields, changeset) 23 | 24 | with_transactions_if_callbacks repo, adapter, model, opts, 25 | ~w(before_insert after_insert)a, fn -> 26 | changeset = Callbacks.__apply__(model, :before_insert, changeset) 27 | changes = validate_changes(:insert, model, fields, changeset) 28 | 29 | {:ok, values} = adapter.insert(repo, source, changes, return, opts) 30 | 31 | changeset = load_into_changeset(changeset, model, return, values) 32 | Callbacks.__apply__(model, :after_insert, changeset).model 33 | end 34 | end 35 | 36 | def insert(repo, adapter, %{__struct__: _} = struct, opts) do 37 | insert(repo, adapter, %Ecto.Changeset{model: struct, valid?: true}, opts) 38 | end 39 | 40 | @doc """ 41 | Implementation for `Ecto.Repo.update/2`. 42 | """ 43 | def update(repo, adapter, %Ecto.Changeset{} = changeset, opts) when is_list(opts) do 44 | struct = struct_from_changeset!(changeset) 45 | model = struct.__struct__ 46 | fields = model.__schema__(:fields) 47 | source = model.__schema__(:source) 48 | return = handle_returning(adapter, model) 49 | 50 | # Differently from insert, update does not copy the struct 51 | # fields into the changeset. All changes must be in the 52 | # changeset before hand. 53 | changeset = %{changeset | repo: repo} 54 | 55 | with_transactions_if_callbacks repo, adapter, model, opts, 56 | ~w(before_update after_update)a, fn -> 57 | changeset = Callbacks.__apply__(model, :before_update, changeset) 58 | changes = validate_changes(:update, model, fields, changeset) 59 | 60 | pk_filter = Planner.fields(:update, model, pk_filter(model, struct)) 61 | 62 | if changes == [] do 63 | changes = pk_filter 64 | end 65 | 66 | {:ok, values} = adapter.update(repo, source, pk_filter, changes, return, opts) 67 | 68 | changeset = load_into_changeset(changeset, model, return, values) 69 | Callbacks.__apply__(model, :after_update, changeset).model 70 | end 71 | end 72 | 73 | def update(repo, adapter, %{__struct__: model} = struct, opts) do 74 | changes = Map.take(struct, model.__schema__(:fields)) 75 | changeset = %Ecto.Changeset{model: struct, valid?: true, changes: changes} 76 | update(repo, adapter, changeset, opts) 77 | end 78 | 79 | @doc """ 80 | Implementation for `Ecto.Repo.delete/2`. 81 | """ 82 | def delete(repo, adapter, %Ecto.Changeset{} = changeset, opts) when is_list(opts) do 83 | struct = struct_from_changeset!(changeset) 84 | model = struct.__struct__ 85 | source = model.__schema__(:source) 86 | 87 | # There are no field changes on delete 88 | changeset = %{changeset | repo: repo} 89 | 90 | with_transactions_if_callbacks repo, adapter, model, opts, 91 | ~w(before_delete after_delete)a, fn -> 92 | changeset = Callbacks.__apply__(model, :before_delete, changeset) 93 | 94 | pk_filter = Planner.fields(:delete, model, pk_filter(model, struct)) 95 | {:ok, _} = adapter.delete(repo, source, pk_filter, opts) 96 | 97 | Callbacks.__apply__(model, :after_delete, changeset).model 98 | |> Map.put(:__state__, :deleted) 99 | end 100 | end 101 | 102 | def delete(repo, adapter, %{__struct__: _} = struct, opts) do 103 | delete(repo, adapter, %Ecto.Changeset{model: struct, valid?: true}, opts) 104 | end 105 | 106 | ## Helpers 107 | 108 | def handle_returning(Ecto.Adapters.MySQL, _model), 109 | do: [] 110 | def handle_returning(_adapter, model), 111 | do: model.__schema__(:read_after_writes) 112 | 113 | defp struct_from_changeset!(%{valid?: false}), 114 | do: raise(ArgumentError, "cannot insert/update an invalid changeset") 115 | defp struct_from_changeset!(%{model: nil}), 116 | do: raise(ArgumentError, "cannot insert/update a changeset without a model") 117 | defp struct_from_changeset!(%{model: struct}), 118 | do: struct 119 | 120 | defp load_into_changeset(%{changes: changes} = changeset, model, return, values) do 121 | update_in changeset.model, 122 | &model.__schema__(:load, struct(&1, changes), return, values) 123 | end 124 | 125 | defp merge_into_changeset(model, struct, fields, changeset) do 126 | changes = Map.take(struct, fields) 127 | pk_field = model.__schema__(:primary_key) 128 | 129 | # If we have a primary key field but it is nil, 130 | # we should not include it in the list of changes. 131 | if pk_field && !Ecto.Model.primary_key(struct) do 132 | changes = Map.delete(changes, pk_field) 133 | end 134 | 135 | update_in changeset.changes, &Map.merge(changes, &1) 136 | end 137 | 138 | defp validate_changes(kind, model, fields, changeset) do 139 | Planner.fields(kind, model, Map.take(changeset.changes, fields)) 140 | end 141 | 142 | defp pk_filter(model, struct) do 143 | pk_field = model.__schema__(:primary_key) 144 | pk_value = Ecto.Model.primary_key(struct) || 145 | raise Ecto.MissingPrimaryKeyError, struct: struct 146 | [{pk_field, pk_value}] 147 | end 148 | 149 | defp with_transactions_if_callbacks(repo, adapter, model, opts, callbacks, fun) do 150 | if Enum.any?(callbacks, &function_exported?(model, &1, 1)) do 151 | {:ok, value} = adapter.transaction(repo, opts, fun) 152 | value 153 | else 154 | fun.() 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/ecto/repo/preloader.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.Preloader do 2 | # The module invoked by user defined repos 3 | # for preload related functionality. 4 | @moduledoc false 5 | 6 | require Ecto.Query 7 | 8 | @doc """ 9 | Transforms a result set based on query preloads, loading 10 | the associations onto their parent model. 11 | """ 12 | @spec query([list], Ecto.Repo.t, Ecto.Query.t, fun) :: [list] 13 | def query([], _repo, _query, _fun), do: [] 14 | def query(rows, _repo, %{preloads: []}, fun), do: Enum.map(rows, fun) 15 | 16 | def query(rows, repo, query, fun) do 17 | rows 18 | |> extract 19 | |> do_preload(repo, query.preloads, query.assocs) 20 | |> unextract(rows, fun) 21 | end 22 | 23 | defp extract([[nil|_]|t2]), do: extract(t2) 24 | defp extract([[h|_]|t2]), do: [h|extract(t2)] 25 | defp extract([]), do: [] 26 | 27 | defp unextract(structs, [[nil|_]=h2|t2], fun), do: [fun.(h2)|unextract(structs, t2, fun)] 28 | defp unextract([h1|structs], [[_|t1]|t2], fun), do: [fun.([h1|t1])|unextract(structs, t2, fun)] 29 | defp unextract([], [], _fun), do: [] 30 | 31 | @doc """ 32 | Implementation for `Ecto.Repo.preload/2`. 33 | """ 34 | @spec preload(models, atom, atom | list) :: models when models: [Ecto.Model.t] | Ecto.Model.t 35 | def preload(structs, repo, preloads) when is_list(structs) do 36 | do_preload(structs, repo, preloads, nil) 37 | end 38 | 39 | def preload(struct, repo, preloads) when is_map(struct) do 40 | do_preload([struct], repo, preloads, nil) |> hd() 41 | end 42 | 43 | ## Implementation 44 | 45 | defp do_preload(structs, repo, preloads, assocs) do 46 | preloads = normalize(preloads, assocs, preloads) 47 | do_preload(structs, repo, preloads) 48 | end 49 | 50 | defp do_preload(structs, _repo, []), do: structs 51 | defp do_preload([], _repo, _preloads), do: [] 52 | 53 | defp do_preload(structs, repo, preloads) do 54 | preloads = expand(hd(structs).__struct__, preloads, []) 55 | 56 | entries = 57 | Enum.map preloads, fn 58 | {preload, {:assoc, assoc, assoc_key}, sub_preloads} -> 59 | query = Ecto.Model.assoc(structs, preload) 60 | card = assoc.cardinality 61 | 62 | if card == :many do 63 | query = Ecto.Query.from q in query, order_by: field(q, ^assoc_key) 64 | end 65 | 66 | loaded = do_preload(repo.all(query), repo, sub_preloads) 67 | {:assoc, assoc, into_dict(card, assoc_key, loaded)} 68 | 69 | {_, {:through, _, _} = info, []} -> 70 | info 71 | end 72 | 73 | for struct <- structs do 74 | Enum.reduce entries, struct, fn 75 | {:assoc, assoc, dict}, acc -> load_assoc(acc, assoc, dict) 76 | {:through, assoc, through}, acc -> load_through(acc, assoc, through) 77 | end 78 | end 79 | end 80 | 81 | ## Load preloaded data 82 | 83 | defp load_assoc(struct, assoc, dict) do 84 | key = Map.fetch!(struct, assoc.owner_key) 85 | 86 | loaded = 87 | cond do 88 | value = HashDict.get(dict, key) -> value 89 | assoc.cardinality == :many -> [] 90 | true -> nil 91 | end 92 | 93 | Map.put(struct, assoc.field, loaded) 94 | end 95 | 96 | defp load_through(struct, assoc, [h|t]) do 97 | initial = struct |> Map.fetch!(h) |> List.wrap 98 | loaded = Enum.reduce(t, initial, &recur_through/2) 99 | 100 | if assoc.cardinality == :one do 101 | loaded = List.first(loaded) 102 | end 103 | 104 | Map.put(struct, assoc.field, loaded) 105 | end 106 | 107 | defp recur_through(assoc, structs) do 108 | Enum.reduce(structs, {[], HashSet.new}, fn struct, acc -> 109 | children = struct |> Map.fetch!(assoc) |> List.wrap 110 | 111 | Enum.reduce children, acc, fn child, {fresh, set} -> 112 | pk = Ecto.Model.primary_key(child) || 113 | raise Ecto.MissingPrimaryKeyError, struct: child 114 | 115 | if HashSet.member?(set, pk) do 116 | {fresh, set} 117 | else 118 | {[child|fresh], HashSet.put(set, pk)} 119 | end 120 | end 121 | end) |> elem(0) |> Enum.reverse() 122 | end 123 | 124 | ## Stores preloaded data 125 | 126 | defp into_dict(:one, key, structs) do 127 | Enum.reduce structs, HashDict.new, fn x, acc -> 128 | HashDict.put(acc, Map.fetch!(x, key), x) 129 | end 130 | end 131 | 132 | defp into_dict(:many, key, structs) do 133 | many_into_dict(structs, key, HashDict.new) 134 | end 135 | 136 | defp many_into_dict([], _key, dict) do 137 | dict 138 | end 139 | 140 | defp many_into_dict([h|t], key, dict) do 141 | current = Map.fetch!(h, key) 142 | {t1, t2} = Enum.split_while(t, &(Map.fetch!(&1, key) == current)) 143 | many_into_dict(t2, key, HashDict.put(dict, current, [h|t1])) 144 | end 145 | 146 | ## Normalizer 147 | 148 | def normalize(preload, assocs, original) do 149 | normalize_each(List.wrap(preload), [], assocs, original) 150 | end 151 | 152 | defp normalize_each({atom, list}, acc, assocs, original) when is_atom(atom) do 153 | no_assoc!(assocs, atom) 154 | [{atom, normalize_each(List.wrap(list), [], assocs, original)}|acc] 155 | end 156 | 157 | defp normalize_each(atom, acc, assocs, _original) when is_atom(atom) do 158 | no_assoc!(assocs, atom) 159 | [{atom, []}|acc] 160 | end 161 | 162 | defp normalize_each(list, acc, assocs, original) when is_list(list) do 163 | Enum.reduce(list, acc, &normalize_each(&1, &2, assocs, original)) 164 | end 165 | 166 | defp normalize_each(other, _, _assocs, original) do 167 | raise ArgumentError, "invalid preload `#{inspect other}` in `#{inspect original}`. " <> 168 | "preload expects an atom, a (nested) keyword or a (nested) list of atoms" 169 | end 170 | 171 | defp no_assoc!(nil, _atom), do: nil 172 | defp no_assoc!(assocs, atom) do 173 | if assocs[atom] do 174 | raise ArgumentError, "cannot preload association `#{inspect atom}` because " <> 175 | "it has already been loaded with join association" 176 | end 177 | end 178 | 179 | ## Expand 180 | 181 | def expand(model, preloads, acc) do 182 | Enum.reduce(preloads, acc, fn {preload, sub_preloads}, acc -> 183 | case List.keyfind(acc, preload, 0) do 184 | {^preload, info, extra_preloads} -> 185 | List.keyreplace(acc, preload, 0, {preload, info, sub_preloads ++ extra_preloads}) 186 | nil -> 187 | assoc = Ecto.Associations.association_from_model!(model, preload) 188 | info = assoc.__struct__.preload_info(assoc) 189 | 190 | case info do 191 | {:assoc, _, _} -> 192 | [{preload, info, sub_preloads}|acc] 193 | {:through, _, through} -> 194 | through = through |> Enum.reverse |> Enum.reduce(sub_preloads, &[{&1, &2}]) 195 | List.keystore(expand(model, through, acc), preload, 0, {preload, info, []}) 196 | end 197 | end 198 | end) 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/ecto/repo/queryable.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Repo.Queryable do 2 | # The module invoked by user defined repos 3 | # for query related functionality. 4 | @moduledoc false 5 | 6 | alias Ecto.Queryable 7 | alias Ecto.Query.Builder 8 | alias Ecto.Query.Planner 9 | 10 | require Ecto.Query 11 | 12 | @doc """ 13 | Implementation for `Ecto.Repo.all/2` 14 | """ 15 | def all(repo, adapter, queryable, opts) when is_list(opts) do 16 | {query, params} = 17 | Queryable.to_query(queryable) 18 | |> Planner.query(%{}) 19 | 20 | adapter.all(repo, query, params, opts) 21 | |> Ecto.Repo.Assoc.query(query) 22 | |> Ecto.Repo.Preloader.query(repo, query, to_select(query.select)) 23 | end 24 | 25 | @doc """ 26 | Implementation for `Ecto.Repo.get/3` 27 | """ 28 | def get(repo, adapter, queryable, id, opts) do 29 | one(repo, adapter, query_for_get(queryable, id), opts) 30 | end 31 | 32 | @doc """ 33 | Implementation for `Ecto.Repo.get!/3` 34 | """ 35 | def get!(repo, adapter, queryable, id, opts) do 36 | one!(repo, adapter, query_for_get(queryable, id), opts) 37 | end 38 | 39 | @doc """ 40 | Implementation for `Ecto.Repo.one/2` 41 | """ 42 | def one(repo, adapter, queryable, opts) do 43 | case all(repo, adapter, queryable, opts) do 44 | [one] -> one 45 | [] -> nil 46 | other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other) 47 | end 48 | end 49 | 50 | @doc """ 51 | Implementation for `Ecto.Repo.one!/2` 52 | """ 53 | def one!(repo, adapter, queryable, opts) do 54 | case all(repo, adapter, queryable, opts) do 55 | [one] -> one 56 | [] -> raise Ecto.NoResultsError, queryable: queryable 57 | other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other) 58 | end 59 | end 60 | 61 | @doc """ 62 | Implementation for `Ecto.Repo.update_all/3` 63 | """ 64 | def update_all(repo, adapter, queryable, values, opts) do 65 | {binds, expr} = Ecto.Query.Builder.From.escape(queryable) 66 | 67 | {updates, params} = 68 | Enum.map_reduce(values, %{}, fn {field, expr}, params -> 69 | {expr, params} = Builder.escape(expr, {0, field}, params, binds) 70 | {{field, expr}, params} 71 | end) 72 | 73 | params = Builder.escape_params(params) 74 | 75 | quote do 76 | Ecto.Repo.Queryable.update_all(unquote(repo), unquote(adapter), 77 | unquote(expr), unquote(updates), unquote(params), unquote(opts)) 78 | end 79 | end 80 | 81 | @doc """ 82 | Runtime callback for `Ecto.Repo.update_all/3` 83 | """ 84 | def update_all(repo, adapter, queryable, updates, params, opts) when is_list(opts) do 85 | query = Queryable.to_query(queryable) 86 | 87 | if updates == [] do 88 | message = "no fields given to `update_all`" 89 | raise ArgumentError, message 90 | end 91 | 92 | # If we have a model in the query, let's use it for casting. 93 | case query.from do 94 | {_source, model} when model != nil -> 95 | # Check all fields are valid but don't use dump as we'll cast below. 96 | _ = Planner.fields(:update_all, model, updates, fn _type, value -> {:ok, value} end) 97 | 98 | # Properly cast parameters. 99 | params = Enum.into params, %{}, fn 100 | {k, {v, {0, field}}} -> 101 | type = model.__schema__(:field, field) 102 | {k, cast_and_dump(:update_all, type, v)} 103 | {k, {v, type}} -> 104 | {k, cast_and_dump(:update_all, type, v)} 105 | end 106 | _ -> 107 | :ok 108 | end 109 | 110 | {query, params} = 111 | Queryable.to_query(queryable) 112 | |> Planner.query(params, only_where: true) 113 | adapter.update_all(repo, query, updates, params, opts) 114 | end 115 | 116 | @doc """ 117 | Implementation for `Ecto.Repo.delete_all/2` 118 | """ 119 | def delete_all(repo, adapter, queryable, opts) when is_list(opts) do 120 | {query, params} = 121 | Queryable.to_query(queryable) 122 | |> Planner.query(%{}, only_where: true) 123 | adapter.delete_all(repo, query, params, opts) 124 | end 125 | 126 | ## Helpers 127 | 128 | defp to_select(select) do 129 | expr = select.expr 130 | # The planner always put the from as the first 131 | # entry in the query, avoiding fetching it multiple 132 | # times even if it appears multiple times in the query. 133 | # So we always need to handle it specially. 134 | from? = match?([{:&, _, [0]}|_], select.fields) 135 | &to_select(&1, expr, from?) 136 | end 137 | 138 | defp to_select(row, expr, true), 139 | do: transform_row(expr, hd(row), tl(row)) |> elem(0) 140 | defp to_select(row, expr, false), 141 | do: transform_row(expr, nil, row) |> elem(0) 142 | 143 | defp transform_row({:{}, _, list}, from, values) do 144 | {result, values} = transform_row(list, from, values) 145 | {List.to_tuple(result), values} 146 | end 147 | 148 | defp transform_row({left, right}, from, values) do 149 | {[left, right], values} = transform_row([left, right], from, values) 150 | {{left, right}, values} 151 | end 152 | 153 | defp transform_row(list, from, values) when is_list(list) do 154 | Enum.map_reduce(list, values, &transform_row(&1, from, &2)) 155 | end 156 | 157 | defp transform_row(%Ecto.Query.Tagged{tag: tag}, _from, values) do 158 | [value|values] = values 159 | {Ecto.Type.load!(tag, value), values} 160 | end 161 | 162 | defp transform_row({:&, _, [0]}, from, values) do 163 | {from, values} 164 | end 165 | 166 | defp transform_row({{:., _, [{:&, _, [_]}, _]}, meta, []}, _from, values) do 167 | [value|values] = values 168 | 169 | if tag = Keyword.get(meta, :ecto_tag) do 170 | {Ecto.Type.load!(tag, value), values} 171 | else 172 | {value, values} 173 | end 174 | end 175 | 176 | defp transform_row(_, _from, values) do 177 | [value|values] = values 178 | {value, values} 179 | end 180 | 181 | defp query_for_get(queryable, id) do 182 | query = Queryable.to_query(queryable) 183 | model = assert_model!(query) 184 | primary_key = primary_key_field!(model) 185 | Ecto.Query.from(x in query, where: field(x, ^primary_key) == ^id) 186 | end 187 | 188 | defp assert_model!(query) do 189 | case query.from do 190 | {_source, model} when model != nil -> 191 | model 192 | _ -> 193 | raise Ecto.QueryError, 194 | query: query, 195 | message: "expected a from expression with a model" 196 | end 197 | end 198 | 199 | defp cast_and_dump(kind, type, v) do 200 | case Ecto.Type.cast(type, v) do 201 | {:ok, v} -> 202 | Ecto.Type.dump!(type, v) 203 | :error -> 204 | raise ArgumentError, 205 | "value `#{inspect v}` in `#{kind}` cannot be cast to type #{inspect type}" 206 | end 207 | end 208 | 209 | defp primary_key_field!(model) when is_atom(model) do 210 | model.__schema__(:primary_key) || 211 | raise Ecto.NoPrimaryKeyError, model: model 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /lib/ecto/migrator.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migrator do 2 | @moduledoc """ 3 | This module provides the migration API. 4 | 5 | ## Example 6 | 7 | defmodule MyApp.MigrationExample do 8 | use Ecto.Migration 9 | 10 | def up do 11 | execute "CREATE TABLE users(id serial PRIMARY_KEY, username text)" 12 | end 13 | 14 | def down do 15 | execute "DROP TABLE users" 16 | end 17 | end 18 | 19 | Ecto.Migrator.up(Repo, 20080906120000, MyApp.MigrationExample) 20 | 21 | """ 22 | 23 | alias Ecto.Migration.Runner 24 | alias Ecto.Migration.SchemaMigration 25 | 26 | @doc """ 27 | Gets all migrated versions. 28 | 29 | This function ensures the migration table exists 30 | if no table has been defined yet. 31 | """ 32 | @spec migrated_versions(Ecto.Repo.t) :: [integer] 33 | def migrated_versions(repo) do 34 | SchemaMigration.ensure_schema_migrations_table!(repo) 35 | SchemaMigration.migrated_versions(repo) 36 | end 37 | 38 | @doc """ 39 | Runs an up migration on the given repository. 40 | 41 | ## Options 42 | 43 | * `:log` - the level to use for logging. 44 | Can be any of `Logger.level/0` values or `false`. 45 | """ 46 | @spec up(Ecto.Repo.t, integer, Module.t, Keyword.t) :: :ok | :already_up | no_return 47 | def up(repo, version, module, opts \\ []) do 48 | versions = migrated_versions(repo) 49 | 50 | if version in versions do 51 | :already_up 52 | else 53 | do_up(repo, version, module, opts) 54 | :ok 55 | end 56 | end 57 | 58 | defp do_up(repo, version, module, opts) do 59 | repo.transaction [log: false], fn -> 60 | attempt(repo, module, :forward, :up, opts) 61 | || attempt(repo, module, :forward, :change, opts) 62 | || raise Ecto.MigrationError, 63 | message: "#{inspect module} does not implement a `up/0` or `change/0` function" 64 | SchemaMigration.up(repo, version) 65 | end 66 | end 67 | 68 | @doc """ 69 | Runs a down migration on the given repository. 70 | 71 | ## Options 72 | 73 | * `:log` - the level to use for logging. 74 | Can be any of `Logger.level/0` values or `false`. 75 | 76 | """ 77 | @spec down(Ecto.Repo.t, integer, Module.t) :: :ok | :already_down | no_return 78 | def down(repo, version, module, opts \\ []) do 79 | versions = migrated_versions(repo) 80 | 81 | if version in versions do 82 | do_down(repo, version, module, opts) 83 | :ok 84 | else 85 | :already_down 86 | end 87 | end 88 | 89 | defp do_down(repo, version, module, opts) do 90 | repo.transaction [log: false], fn -> 91 | attempt(repo, module, :forward, :down, opts) 92 | || attempt(repo, module, :backward, :change, opts) 93 | || raise Ecto.MigrationError, 94 | message: "#{inspect module} does not implement a `down/0` or `change/0` function" 95 | SchemaMigration.down(repo, version) 96 | end 97 | end 98 | 99 | defp attempt(repo, module, direction, operation, opts) do 100 | if Code.ensure_loaded?(module) and 101 | function_exported?(module, operation, 0) do 102 | Runner.run(repo, module, direction, operation, opts) 103 | :ok 104 | end 105 | end 106 | 107 | @doc """ 108 | Apply migrations in a directory to a repository with given strategy. 109 | 110 | A strategy must be given as an option. 111 | 112 | ## Options 113 | 114 | * `:all` - runs all available if `true` 115 | * `:step` - runs the specific number of migrations 116 | * `:to` - runs all until the supplied version is reached 117 | * `:log` - the level to use for logging. 118 | Can be any of `Logger.level/0` values or `false`. 119 | 120 | """ 121 | @spec run(Ecto.Repo.t, binary, atom, Keyword.t) :: [integer] 122 | def run(repo, directory, direction, opts) do 123 | versions = migrated_versions(repo) 124 | 125 | cond do 126 | opts[:all] -> 127 | run_all(repo, versions, directory, direction, opts) 128 | to = opts[:to] -> 129 | run_to(repo, versions, directory, direction, to, opts) 130 | step = opts[:step] -> 131 | run_step(repo, versions, directory, direction, step, opts) 132 | true -> 133 | raise ArgumentError, message: "expected one of :all, :to, or :step strategies" 134 | end 135 | end 136 | 137 | defp run_to(repo, versions, directory, direction, target, opts) do 138 | within_target_version? = fn 139 | {version, _}, target, :up -> 140 | version <= target 141 | {version, _}, target, :down -> 142 | version >= target 143 | end 144 | 145 | pending_in_direction(versions, directory, direction) 146 | |> Enum.take_while(&(within_target_version?.(&1, target, direction))) 147 | |> migrate(direction, repo, opts) 148 | end 149 | 150 | defp run_step(repo, versions, directory, direction, count, opts) do 151 | pending_in_direction(versions, directory, direction) 152 | |> Enum.take(count) 153 | |> migrate(direction, repo, opts) 154 | end 155 | 156 | defp run_all(repo, versions, directory, direction, opts) do 157 | pending_in_direction(versions, directory, direction) 158 | |> migrate(direction, repo, opts) 159 | end 160 | 161 | defp pending_in_direction(versions, directory, :up) do 162 | migrations_for(directory) 163 | |> Enum.filter(fn {version, _file} -> not (version in versions) end) 164 | end 165 | 166 | defp pending_in_direction(versions, directory, :down) do 167 | migrations_for(directory) 168 | |> Enum.filter(fn {version, _file} -> version in versions end) 169 | |> Enum.reverse 170 | end 171 | 172 | defp migrations_for(directory) do 173 | Path.join(directory, "*") 174 | |> Path.wildcard 175 | |> Enum.filter(&Regex.match?(~r"\d+_.+\.exs$", &1)) 176 | |> attach_versions 177 | end 178 | 179 | defp attach_versions(files) do 180 | Enum.map(files, fn(file) -> 181 | {integer, _} = Integer.parse(Path.basename(file)) 182 | {integer, file} 183 | end) 184 | end 185 | 186 | defp migrate(migrations, direction, repo, opts) do 187 | ensure_no_duplication(migrations) 188 | 189 | Enum.map migrations, fn {version, file} -> 190 | {mod, _bin} = 191 | Enum.find(Code.load_file(file), fn {mod, _bin} -> 192 | function_exported?(mod, :__migration__, 0) 193 | end) || raise_no_migration_in_file(file) 194 | 195 | case direction do 196 | :up -> do_up(repo, version, mod, opts) 197 | :down -> do_down(repo, version, mod, opts) 198 | end 199 | 200 | version 201 | end 202 | end 203 | 204 | defp ensure_no_duplication([{version, _} | t]) do 205 | if List.keyfind(t, version, 0) do 206 | raise Ecto.MigrationError, message: "migrations can't be executed, version #{version} is duplicated" 207 | else 208 | ensure_no_duplication(t) 209 | end 210 | end 211 | 212 | defp ensure_no_duplication([]), do: :ok 213 | 214 | defp raise_no_migration_in_file(file) do 215 | raise Ecto.MigrationError, message: "file #{Path.relative_to_cwd(file)} does not contain any Ecto.Migration" 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/ecto/query/inspect.ex: -------------------------------------------------------------------------------- 1 | defimpl Inspect, for: Ecto.Query do 2 | import Inspect.Algebra 3 | alias Ecto.Query.JoinExpr 4 | 5 | @doc false 6 | def inspect(query, opts) do 7 | list = Enum.map(to_list(query), fn 8 | {key, string} -> 9 | concat(Atom.to_string(key) <> ": ", string) 10 | string -> 11 | string 12 | end) 13 | 14 | surround_many("#Ecto.Query<", list, ">", opts, fn str, _ -> str end) 15 | end 16 | 17 | @doc false 18 | def to_string(query) do 19 | Enum.map_join(to_list(query), ",\n ", fn 20 | {key, string} -> 21 | Atom.to_string(key) <> ": " <> string 22 | string -> 23 | string 24 | end) 25 | end 26 | 27 | defp to_list(query) do 28 | names = 29 | query 30 | |> collect_sources 31 | |> generate_letters 32 | |> generate_names 33 | |> List.to_tuple 34 | 35 | from = bound_from(query.from, elem(names, 0)) 36 | joins = joins(query.joins, names) 37 | preloads = preloads(query.preloads) 38 | assocs = assocs(query.assocs, names) 39 | 40 | wheres = kw_exprs(:where, query.wheres, names) 41 | group_bys = kw_exprs(:group_by, query.group_bys, names) 42 | havings = kw_exprs(:having, query.havings, names) 43 | order_bys = kw_exprs(:order_by, query.order_bys, names) 44 | distincts = kw_exprs(:distinct, query.distincts, names) 45 | 46 | lock = kw_inspect(:lock, query.lock) 47 | limit = kw_expr(:limit, query.limit, names) 48 | offset = kw_expr(:offset, query.offset, names) 49 | select = kw_expr(:select, query.select, names) 50 | 51 | Enum.concat [from, joins, wheres, group_bys, havings, order_bys, 52 | limit, offset, lock, distincts, select, preloads, assocs] 53 | end 54 | 55 | defp bound_from(from, name), do: ["from #{name} in #{unbound_from from}"] 56 | 57 | defp unbound_from({source, nil}), do: inspect source 58 | defp unbound_from({_source, model}), do: inspect model 59 | defp unbound_from(nil), do: "query" 60 | 61 | defp joins(joins, names) do 62 | joins 63 | |> Enum.with_index 64 | |> Enum.flat_map(fn {expr, ix} -> join(expr, elem(names, expr.ix || ix + 1), names) end) 65 | end 66 | 67 | defp join(%JoinExpr{qual: qual, assoc: {ix, right}}, name, names) do 68 | string = "#{name} in assoc(#{elem(names, ix)}, #{inspect right})" 69 | [{join_qual(qual), string}] 70 | end 71 | 72 | defp join(%JoinExpr{qual: qual, source: {source, model}, on: on}, name, names) do 73 | string = "#{name} in #{inspect model || source}" 74 | [{join_qual(qual), string}, on: expr(on, names)] 75 | end 76 | 77 | defp preloads([]), do: [] 78 | defp preloads(preloads), do: [preload: inspect(preloads)] 79 | 80 | defp assocs([], _names), do: [] 81 | defp assocs(assocs, names), do: [preload: expr(assocs(assocs), names, %{})] 82 | 83 | defp assocs(assocs) do 84 | Enum.map assocs, fn 85 | {field, {idx, []}} -> 86 | {field, {:&, [], [idx]}} 87 | {field, {idx, children}} -> 88 | {field, {{:&, [], [idx]}, assocs(children)}} 89 | end 90 | end 91 | 92 | defp kw_exprs(key, exprs, names) do 93 | Enum.map exprs, &{key, expr(&1, names)} 94 | end 95 | 96 | defp kw_expr(_key, nil, _names), do: [] 97 | defp kw_expr(key, expr, names), do: [{key, expr(expr, names)}] 98 | 99 | defp kw_inspect(_key, nil), do: [] 100 | defp kw_inspect(key, val), do: [{key, inspect(val)}] 101 | 102 | defp expr(%{expr: expr, params: params}, names) do 103 | expr(expr, names, params) 104 | end 105 | 106 | defp expr(expr, names, params) do 107 | Macro.to_string(expr, &expr_to_string(&1, &2, names, params)) 108 | end 109 | 110 | defp expr_to_string({:fragment, _, parts}, _, names, params) do 111 | "fragment(" <> unmerge_fragments(parts, "", [], names, params) <> ")" 112 | end 113 | 114 | # Convert variables to proper names 115 | defp expr_to_string({:&, _, [ix]}, _, names, _) do 116 | elem(names, ix) 117 | end 118 | 119 | # Inject the interpolated value 120 | # 121 | # In case the query had its parameters removed, 122 | # we use ... to express the interpolated code. 123 | defp expr_to_string({:^, _, [ix]}, _, _, params) do 124 | escaped = 125 | case Map.get(params || %{}, ix) do 126 | {value, _type} -> Macro.escape(value) 127 | _ -> {:..., [], nil} 128 | end 129 | Macro.to_string {:^, [], [escaped]} 130 | end 131 | 132 | # Strip trailing () 133 | defp expr_to_string({{:., _, [_, _]}, _, []}, string, _, _) do 134 | size = byte_size(string) 135 | :binary.part(string, 0, size - 2) 136 | end 137 | 138 | # Tagged values 139 | defp expr_to_string(%Ecto.Query.Tagged{value: value, tag: :binary}, _, _names, _params) when is_binary(value) do 140 | inspect value 141 | end 142 | 143 | defp expr_to_string(%Ecto.Query.Tagged{value: value, tag: :uuid}, _, names, params) when is_binary(value) do 144 | {:uuid, [], [value]} |> expr(names, params) 145 | end 146 | 147 | defp expr_to_string(%Ecto.Query.Tagged{value: value, tag: type}, _, names, params) do 148 | {:type, [], [value, type]} |> expr(names, params) 149 | end 150 | 151 | defp expr_to_string(_expr, string, _, _) do 152 | string 153 | end 154 | 155 | defp unmerge_fragments([s, v|t], frag, args, names, params) do 156 | unmerge_fragments(t, frag <> s <> "?", [expr(v, names, params)|args], names, params) 157 | end 158 | 159 | defp unmerge_fragments([s], frag, args, _names, _params) do 160 | Enum.join [inspect(frag <> s)|Enum.reverse(args)], ", " 161 | end 162 | 163 | defp join_qual(:inner), do: :join 164 | defp join_qual(:left), do: :left_join 165 | defp join_qual(:right), do: :right_join 166 | defp join_qual(:outer), do: :outer_join 167 | 168 | defp collect_sources(query) do 169 | from_sources(query.from) ++ join_sources(query.joins) 170 | end 171 | 172 | defp from_sources({source, model}), do: [model || source] 173 | defp from_sources(nil), do: ["query"] 174 | 175 | defp join_sources(joins) do 176 | Enum.map(joins, fn 177 | %JoinExpr{assoc: {_var, assoc}} -> 178 | assoc 179 | %JoinExpr{source: {source, model}} -> 180 | model || source 181 | end) 182 | end 183 | 184 | defp generate_letters(sources) do 185 | Enum.map(sources, fn source -> 186 | source 187 | |> Kernel.to_string 188 | |> normalize_source 189 | |> String.first 190 | |> String.downcase 191 | end) 192 | end 193 | 194 | defp generate_names(letters) do 195 | generate_names(Enum.reverse(letters), [], []) 196 | end 197 | 198 | defp generate_names([letter|rest], acc, found) do 199 | index = Enum.count(rest, & &1 == letter) 200 | 201 | cond do 202 | index > 0 -> 203 | generate_names(rest, ["#{letter}#{index}"|acc], [letter|found]) 204 | letter in found -> 205 | generate_names(rest, ["#{letter}0"|acc], [letter|found]) 206 | true -> 207 | generate_names(rest, [letter|acc], found) 208 | end 209 | end 210 | 211 | defp generate_names([], acc, _found) do 212 | acc 213 | end 214 | 215 | defp normalize_source("Elixir." <> _ = source), 216 | do: source |> Module.split |> List.last 217 | defp normalize_source(source), 218 | do: source 219 | end 220 | --------------------------------------------------------------------------------