update
This commit is contained in:
36
phoenix/deps/ecto_sql/.formatter.exs
Normal file
36
phoenix/deps/ecto_sql/.formatter.exs
Normal file
@@ -0,0 +1,36 @@
|
||||
locals_without_parens = [
|
||||
add: 2,
|
||||
add: 3,
|
||||
add_if_not_exists: 2,
|
||||
add_if_not_exists: 3,
|
||||
alter: 2,
|
||||
create: 1,
|
||||
create: 2,
|
||||
create_if_not_exists: 1,
|
||||
create_if_not_exists: 2,
|
||||
drop: 1,
|
||||
drop: 2,
|
||||
drop_if_exists: 1,
|
||||
drop_if_exists: 2,
|
||||
execute: 1,
|
||||
execute: 2,
|
||||
modify: 2,
|
||||
modify: 3,
|
||||
remove: 1,
|
||||
remove: 2,
|
||||
remove: 3,
|
||||
remove_if_exists: 1,
|
||||
remove_if_exists: 2,
|
||||
rename: 2,
|
||||
rename: 3,
|
||||
timestamps: 1
|
||||
]
|
||||
|
||||
[
|
||||
import_deps: [:ecto],
|
||||
locals_without_parens: locals_without_parens,
|
||||
export: [
|
||||
locals_without_parens: locals_without_parens
|
||||
],
|
||||
inputs: ["{lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
BIN
phoenix/deps/ecto_sql/.hex
Normal file
BIN
phoenix/deps/ecto_sql/.hex
Normal file
Binary file not shown.
579
phoenix/deps/ecto_sql/CHANGELOG.md
Normal file
579
phoenix/deps/ecto_sql/CHANGELOG.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# Changelog for v3.x
|
||||
|
||||
## v3.13.5 (2026-03-03)
|
||||
|
||||
* [postgrex] Map `:restrict_violation` to `:foreign_key` constraint (required by PostgreSQL 18)
|
||||
|
||||
## v3.13.4 (2025-12-27)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mysql] Do not crash `mix ecto.load` with large dumped databases
|
||||
|
||||
## v3.13.3 (2025-12-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sql] Tag generated functions as `:generated`
|
||||
* [sql] Add `:wrap_in_transaction` option to explain
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mysql] Fix `structure_load/2` for MySQL 9.4+
|
||||
|
||||
## v3.13.2 (2025-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sandbox] Allow passing through opts in `Ecto.Adapters.SQL.Sandbox.allow/4` calls
|
||||
* [sql] Add support for `ON DELETE SET DEFAULT`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [postgres] Fix nested array generated time columns
|
||||
|
||||
## v3.13.1 (2025-06-20)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [postgres] Fix nested array generated columns
|
||||
|
||||
## v3.13.0 (2025-06-18)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Ecto.Migration] Add support for index directions
|
||||
* [sql] Support `:log_stacktrace_mfa` for filtering or modifying stacktrace-derived info in query logs
|
||||
* [mysql] Support arrays using JSON for MariaDB
|
||||
* [mysql] Allow to specify `:prepare` per operation
|
||||
* [postgres] Add support for collations in Postgres
|
||||
* [postgres] Allow source fields in `json_extract_path`
|
||||
|
||||
## v3.12.1 (2024-10-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sql] Support `:pool_count` option
|
||||
|
||||
## v3.12.0 (2024-08-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Ecto.Migration] Add `Ecto.Migration.remove_if_exists/1`
|
||||
* [Ecto.Migrator] Warn for migration files that end in `.ex`
|
||||
* [sql] Support for subqueries in order_bys and group_bys
|
||||
* [mysql] Add check constraints for MySQL
|
||||
* [postgres] Add native bitstring support to Postgres
|
||||
* [postgres] Add support for `:duration` type
|
||||
* [postgres] Add `:plan` explain option for Postgres
|
||||
* [tds] Allow passing `%Tds.Parameter` structs as params for named parameter usage in `query`
|
||||
|
||||
### Bug fix
|
||||
|
||||
* [mysql] Type cast of integers in MySQL should use signed integers
|
||||
|
||||
## v3.11.3 (2024-06-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Relax `myxql` dependency
|
||||
|
||||
## v3.11.2 (2024-05-18)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [postgres] Relax `postgrex` dependency
|
||||
|
||||
## v3.11.1 (2023-12-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Ecto.Migration] Add `:generated` option to columns
|
||||
* [Ecto.Migration] Add index storage parameters (via :options) for Postgres
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [Ecto.Migration] Support `:prefix` on index rename
|
||||
* [Ecto.Migrator] Stop runner if migration fails
|
||||
|
||||
## v3.11.0 (2023-11-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mix ecto.migrate] Add `--log-level` to ecto.migrate
|
||||
* [mix ecto.rollback] Add `--log-level` to ecto.rollback
|
||||
* [sql] Support fragment splicing
|
||||
* [sql] Support data-modifying CTEs
|
||||
* [sql] Add source to insert_ll, insert, update, and delete telemetry events
|
||||
* [tds] Include `exec` before stored procedure for TDS (for earlier SQLServer versions)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mix ecto.migrate] Read existing dynamic repo in migrations
|
||||
* [mix ecto.migrate] Don't add primary key on remove migration
|
||||
|
||||
## v3.10.2 (2023-08-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Handle `from: {reference, opts}` in FK migrations
|
||||
* [mysql] Support MariaDB versioned tables
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Don't add comment to removed columns
|
||||
* [migrations] Ensure module is loaded before checking for migration
|
||||
* [mysql] Fix for casting boolean values in MySQL
|
||||
|
||||
## v3.10.1 (2023-04-11)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [postgres] Allow Postgrex v0.17.x
|
||||
|
||||
## v3.10.0 (2023-04-10)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [Ecto.Migrator] Allow running the migrator in your supervision tree
|
||||
* [Ecto.Migrator] Allow renaming an index
|
||||
* [Ecto.Migrator] Add `execute_file/1` and `execute_file/2`
|
||||
* [mix ecto.dump] Support dumping multiple prefixes on PostgreSQL and MySQL
|
||||
* [mysql] Improve constraint matching support on alternative implementations
|
||||
* [postgres] Allow `CASCADE` when dropping a constraint on postgres
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mix ecto.load] Suppress query logs in mix ecto.load when quiet flag is given
|
||||
|
||||
## v3.9.2 (2022-12-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrator] Raise if target version in `to`/`exclusive_to` is not an integer
|
||||
* [mysql] Add support for cross lateral joins
|
||||
* [postgres] Add support for cross lateral joins
|
||||
* [postgres] Add support for materialized CTEs
|
||||
* [telemetry] Send `cast_params` metadata to telemetry events
|
||||
|
||||
## v3.9.1 (2022-11-18)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Support `:format` option on `explain`
|
||||
* [postgres] Permit outer joins when using `update_all`
|
||||
* [sql] Add support for `ONLY` in index creation
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mysql] Ensure locks are quoted
|
||||
* [mysql] Do not crash on `mix ecto.drop` when the database is unreachable
|
||||
* [postgres] Fix empty array compare in PostgreSQL
|
||||
* [sql] Allow function sources whose name begins with 'select'
|
||||
|
||||
## v3.9.0 (2022-09-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Support `primary_key` configuration options in `table`
|
||||
* [migrations] Add `:nulls_distinct` option for unique indexes
|
||||
* [postgres] Support the use of advisory locks for migrations
|
||||
* [sql] Add `dump_cmd` to `postgrex` and `myxql` adapters
|
||||
* [sql] Log human-readable UUIDs by using pre-dumped query parameters
|
||||
* [sql] Support select aliases from `selected_as/1` and `selected_as/2`
|
||||
* [telemetry] Emit `schema_migration: true` under `telemetry_options`
|
||||
|
||||
## v3.8.3 (2022-06-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sql] Implement `literal/1` support in fragments
|
||||
|
||||
## v3.8.2 (2022-05-18)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [postgres] Fix possible breaking change on `json_extract_path` for boolean values introduced in v3.8.0
|
||||
* [sql] Colorize stacktrace and use `:` before printing line number
|
||||
|
||||
## v3.8.1 (2022-04-29)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mysql] Raise on a subquery with parameter on MySQL join
|
||||
* [sql] Do not invoke dynamic repositories in direct `Ecto.Adapters.SQL` operations
|
||||
|
||||
## v3.8.0 (2022-04-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Support `--to-exclusive` in `mix ecto.migrate` and `mix ecto.rollback`
|
||||
* [mysql] Add `:comment` support on MySQL migrations
|
||||
* [postgres] Support `:prepare` option per operation
|
||||
* [postgres] Optimize `json_extract_path` comparisons in PostgreSQL
|
||||
* [sql] Optionally log last known call, publish stacktrace in telemetry
|
||||
* [telemetry] Include `:repo` option in telemetry events
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [sql] Ensure `:timeout` option is respected in `Ecto.Adapters.SQL.explain/3`
|
||||
|
||||
## v3.7.2 (2022-01-23)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [adapters] Support latest `myxql` and `postgrex`
|
||||
|
||||
## v3.7.1 (2021-10-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Add `:cascade` option to `drop`
|
||||
* [migrations] Support `--prefix` in `mix ecto.migrations`
|
||||
* [migrations] Add `--log-migrator-sql` and `--log-migrations-sql`
|
||||
* [mysql] Cache more insert/update queries and allow `:cache_statement` to be set
|
||||
* [mssql] Support more recent tds versions
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Consider the database prefix when locking tables
|
||||
|
||||
## v3.7.0 (2021-08-19)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Support lateral joins
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [sql] Fix CTE subqueries not finding parent bindings
|
||||
|
||||
## v3.6.2 (2021-05-28)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migration] Improve error message on invalid migration type
|
||||
* [postgres] Avoid duplicate order_by with distinct
|
||||
* [sql] Implement new checked_out? callback required by latest Ecto
|
||||
|
||||
## v3.6.1 (2021-04-12)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Ensure migration_source option is respected in PostgreSQL adapter
|
||||
|
||||
## v3.6.0 (2021-04-03)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Fix a bug where the migration lock would not apply on the first migration (when the schema migrations table is empty). This fix changes how migration tables are locked, therefore let us know of any possible regressions in your workflow
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Allow generating migrations from within umbrella app
|
||||
* [postgres] Add `:format` option to PostgreSQL explain
|
||||
* [postgres] Support `:socket_dir` connection option when using `mix ecto.load` or `mix ecto.dump`
|
||||
* [sandbox] Support locally registered processes in `allow/3`
|
||||
* [storage] Do not fail `storage_up` if the user has access to an already-created database
|
||||
* [tds] Support for `:inner_lateral` and `:left_lateral`
|
||||
|
||||
## v3.5.4 (2020-01-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Support defaults for JSON columns
|
||||
* [postgres] Allow Postgrex v1.0
|
||||
|
||||
## v3.5.3 (2020-10-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Pass `:schema_migration` option to repo operations for `prepare_query` checks
|
||||
* [psql] Support `:force_drop` configuration to force a DB to be dropped
|
||||
|
||||
## v3.5.2 (2020-10-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Support `:with` option in `references` for composite foreign keys
|
||||
* [migrations] Support `:match` option in `references`
|
||||
* [tds] Support TDS 3-part and 4-part prefixes
|
||||
|
||||
## v3.5.1 (2020-10-12)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [tds] Support explain plan for the TDS adapter
|
||||
|
||||
### Bug fix
|
||||
|
||||
* [migrations] Reload all migrations once the lock is free to avoid running the same migration more than once
|
||||
* [query] Support nested subqueries
|
||||
|
||||
## v3.5.0 (2020-10-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Add option to skip schema migrations table checks
|
||||
* [migrations] Add `:migration_repo` configuration to allow a different repository to host the schema migrations
|
||||
* [migrations] Support `validate: false` on references and constraints
|
||||
* [migrations] Accept `:migration_primary_key` as false and add `:migration_foreign_key` repo config
|
||||
* [postgres] Support for `:identity` key types in Postgres 10 or later
|
||||
* [postgres] Use IF NOT EXIST when creating index with `create_if_not_exists`, this requires PG 9.5+ or later
|
||||
* [repo] Support `Repo.explain(:all | :update_all | :delete_all, query)` for Ecto adapters
|
||||
* [sandbox] Allow for dynamic repos to be checked out in sandbox
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Flush migration commands before executing `before_commit` callback
|
||||
* [migrations] Do not swallow errors when migration lock is disabled
|
||||
|
||||
## v3.4.5 (2020-07-05)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [ecto] Fix warnings on Elixir v1.11
|
||||
* [migrations] Migration prefix should have higher preference than `default_options`
|
||||
|
||||
## v3.4.4 (2020-05-19)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sandbox] Add `Ecto.Adapters.SQL.start_owner!/2` and `Ecto.Adapters.SQL.stop_owner/1`
|
||||
* [myxql] Decode BIT columns when using MyXQL and `:boolean` type
|
||||
* [migrations] Use one line per migration in the schema dump
|
||||
|
||||
## v3.4.3 (2020-04-27)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [ecto] Support `as` and `parent_as` from Ecto v3.4.3+
|
||||
* [ecto] Support `x in subquery(query)` from Ecto v3.4.3+
|
||||
|
||||
## v3.4.2 (2020-04-02)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [myxql] A binary with size should be a varbinary
|
||||
* [mssql] A binary without size should be a varbinary(max)
|
||||
|
||||
## v3.4.1 (2020-03-25)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [myxql] Assume the reference does not change in MyXQL and prepare for v0.4.0
|
||||
|
||||
## v3.4.0 (2020-03-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [adapters] Support Ecto's v3.4 `json_extract_path/2`
|
||||
* [migrations] Support multiple migration paths to be given with `--migration-path`
|
||||
* [mssql] Add built-in support to MSSQL via the TDS adapter
|
||||
* [repo] Support custom options on telemetry
|
||||
|
||||
## v3.3.4 (2020-02-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [adapters] Support fragments in locks
|
||||
* [migration] Add `:include` option to support covering indexes
|
||||
|
||||
## v3.3.3 (2020-01-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [myxql] Allow not setting the encoding when creating a database
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [myxql] Removing prefixed table name from constraints on latest MySQL versions
|
||||
* [sql] Fix precedence of `is_nil` when inside a comparison operator
|
||||
|
||||
## v3.3.2 (2019-12-15)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [adapters] Start StorageSupervisor before using it
|
||||
|
||||
## v3.3.1 (2019-12-15)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [adapters] Do not leak PIDs on storage commands
|
||||
* [migrations] Use :migration_primary_key in create/1
|
||||
|
||||
## v3.3.0 (2019-12-11)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [ecto] Upgrade and support Ecto v3.3
|
||||
* [repo] Include `:idle_time` on telemetry measurements
|
||||
* [migration] Support anonymous functions in `Ecto.Migration.execute/2`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migration] Ensure that flush() will raise on rollback if called from `change/0`
|
||||
|
||||
## v3.2.2 (2019-11-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Support myxql v0.3
|
||||
|
||||
## v3.2.1 (2019-11-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migration] Support anonymous functions in execute
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [mix ecto.create] Change default charset in MyXQL to utf8mb4
|
||||
|
||||
## v3.2.0 (2019-09-07)
|
||||
|
||||
This new version requires Elixir v1.6+. Note also the previously soft-deprecated `Ecto.Adapters.MySQL` has been removed in favor of `Ecto.Adapters.MyXQL`. We announced the intent to remove `Ecto.Adapters.MySQL` back in v3.0 and `Ecto.Adapters.MyXQL` has been tested since then and ready for prime time since v3.1.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sql] Use `get_dynamic_repo` on SQL-specific functions
|
||||
* [sql] Respect `Ecto.Type.embed_as/2` choice when loading/dumping embeds (Ecto 3.2+ compat)
|
||||
* [sql] Support CTE expressions (Ecto 3.2+ compat)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [sql] Fix generated "COMMENT ON INDEX" for PostgreSQL
|
||||
|
||||
## v3.1.6 (2019-06-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [sql] Set `cache_statement` for `insert_all`
|
||||
|
||||
## v3.1.5 (2019-06-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migration] Add `@disable_migration_lock` to be better handle concurrent indexes
|
||||
* [mysql] Set `cache_statement` for inserts
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [mysql] Deprecate Ecto.Adapters.MySQL
|
||||
|
||||
## v3.1.4 (2019-05-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrator] Print warning message if concurrent indexes are used with migration lock
|
||||
|
||||
## v3.1.3 (2019-05-19)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrator] Add `--migrations-path` to ecto.migrate/ecto.rollback/ecto.migrations Mix tasks
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrator] Make sure an unboxed run is performed when running migrations with the ownership pool
|
||||
|
||||
## v3.1.2 (2019-05-11)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrator] Add `Ecto.Migrator.with_repo/2` to start repo and apps
|
||||
* [mix] Add `--skip-if-loaded` for `ecto.load`
|
||||
* [sql] Add `Ecto.Adapters.SQL.table_exists?/2`
|
||||
|
||||
## v3.1.1 (2019-04-16)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [repo] Fix backwards incompatible change in Telemetry metadata
|
||||
|
||||
## v3.1.0 (2019-04-02)
|
||||
|
||||
v3.1 requires Elixir v1.5+.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [mysql] Introduce Ecto.Adapters.MyXQL as an alternative library for MySQL
|
||||
* [migrations] Run all migrations in subdirectories
|
||||
* [repo] Update to Telemetry v0.4.0 (note the measurements value differ from previous versions)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [sandbox] Respect `:ownership_timeout` repo configuration on SQL Sandbox
|
||||
* [migrations] Commit and relock after every migration to avoid leaving the DB in an inconsistent state under certain failures
|
||||
|
||||
### Backwards incompatible changes
|
||||
|
||||
* [migrations] If you are creating indexes concurrently, you need to disable the migration lock: `config :app, App.Repo, migration_lock: nil`. This will migrations behave the same way as they did in Ecto 2.0.
|
||||
|
||||
## v3.0.5 (2019-02-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [repo] Add `:repo` and `:type` keys to telemetry events
|
||||
* [migrations] Add `:add_if_not_exists` and `:remove_if_exists` to columns in migrations
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Load all migrations before running them
|
||||
* [sandbox] Include `:queue_target` and `:queue_interval` in SQL Sandbox checkout
|
||||
|
||||
## v3.0.4 (2018-12-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [repo] Bump telemetry dependency
|
||||
* [migrations] Perform strict argument parsing in `ecto.migrate`, `ecto.rollback`, `ecto.load` and `ecto.dump`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Do not log migration versions query
|
||||
|
||||
### Deprecations
|
||||
|
||||
* [repo] `Telemetry.attach/5` and `Telemetry.attach_many/5` are deprecated in favor of `:telemetry.attach/5` and `:telemetry.attach_many/5`
|
||||
|
||||
## v3.0.3 (2018-11-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migration] Support `after_begin` and `before_commit` migration callbacks
|
||||
* [migration] Add `:prefix` option to `references/2`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migration] Do not start a transaction for migrated versions if there is no `:migration_lock`
|
||||
* [migration] Fix removing an reference column inside alter table
|
||||
* [migration] Warn on removed `:pool_timeout` option
|
||||
|
||||
## v3.0.2 (2018-11-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [query] Support `Ecto.Query` in `insert_all` values
|
||||
* [migration] Add `Ecto.Migration.repo/0`
|
||||
|
||||
## v3.0.1 (2018-11-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [migrations] Support `drop_if_exists` for constraints
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* [migrations] Only commit migration transaction if migration can be inserted into the DB
|
||||
* [migrations] Do not run migrations from `_build` when using Mix
|
||||
* [migrations] Improve errors when checking in already committed sandboxes
|
||||
* [mysql] Do not pass nil for `--user` to mysqldump
|
||||
* [package] Require Ecto 3.0.2 with bug fixes
|
||||
* [package] Require Mariaex 0.9.1 which fixes a bug when used with Ecto 3.0.2
|
||||
* [sandbox] Raise when using sandbox on non-sandbox pools
|
||||
|
||||
## v3.0.0 (2018-10-29)
|
||||
|
||||
* Initial release
|
||||
73
phoenix/deps/ecto_sql/README.md
Normal file
73
phoenix/deps/ecto_sql/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
Ecto SQL
|
||||
=========
|
||||
|
||||
[](https://github.com/elixir-ecto/ecto_sql/actions)
|
||||
|
||||
Ecto SQL ([documentation](https://hexdocs.pm/ecto_sql)) provides building blocks for writing SQL adapters for Ecto. It features:
|
||||
|
||||
* The Ecto.Adapters.SQL module as an entry point for all SQL-based adapters
|
||||
* Default implementations for Postgres (Ecto.Adapters.Postgres), MySQL (Ecto.Adapters.MyXQL), and MSSQL (Ecto.Adapters.Tds)
|
||||
* A test sandbox (Ecto.Adapters.SQL.Sandbox) that concurrently runs database tests inside transactions
|
||||
* Support for database migrations via Mix tasks
|
||||
|
||||
To learn more about getting started, [see the Ecto repository](https://github.com/elixir-ecto/ecto).
|
||||
|
||||
## Running tests
|
||||
|
||||
Clone the repo and fetch its dependencies:
|
||||
|
||||
$ git clone https://github.com/elixir-ecto/ecto_sql.git
|
||||
$ cd ecto_sql
|
||||
$ mix deps.get
|
||||
$ mix test
|
||||
|
||||
In case you are modifying Ecto and EctoSQL at the same time, you can configure EctoSQL to use an Ecto version from your machine by running:
|
||||
|
||||
$ ECTO_PATH=../ecto mix test.all
|
||||
|
||||
### Running integration tests
|
||||
|
||||
The command above will run unit tests. EctoSQL also has a suite of integration tests for its built-in adapters: `pg`, `myxql` and `tds`. If you are changing logic specific to a database, we recommend running its respective integration test suite as well. Doing so requires you to have the database available locally. MySQL and PostgreSQL can be installed directly on most systems. For MSSQL, you may need to run it as a Docker image:
|
||||
|
||||
docker run -d -p 1433:1433 --name mssql -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=some!Password' mcr.microsoft.com/mssql/server:2017-latest
|
||||
|
||||
Once the database is running, you can run tests against a specific Ecto adapter by using the `ECTO_ADAPTER` environment variable:
|
||||
|
||||
$ ECTO_ADAPTER=pg mix test
|
||||
|
||||
You may also run `mix test.all` to run the unit tests and all integration tests. You can also use a local Ecto checkout if desired:
|
||||
|
||||
$ ECTO_PATH=../ecto mix test.all
|
||||
|
||||
### Running containerized tests
|
||||
|
||||
It is also possible to run the integration tests under a containerized environment using [earthly](https://earthly.dev/get-earthly). You will also need Docker installed on your system. Then you can run:
|
||||
|
||||
$ earthly -P +all
|
||||
|
||||
You can also use this to interactively debug any failing integration tests using the corresponding commands:
|
||||
|
||||
$ earthly -P -i --build-arg ELIXIR_BASE=1.8.2-erlang-20.3.8.26-alpine-3.11.6 --build-arg MYSQL=5.7 +integration-test-mysql
|
||||
$ earthly -P -i --build-arg ELIXIR_BASE=1.8.2-erlang-20.3.8.26-alpine-3.11.6 --build-arg MSSQL=2019 +integration-test-mssql
|
||||
$ earthly -P -i --build-arg ELIXIR_BASE=1.8.2-erlang-20.3.8.26-alpine-3.11.6 --build-arg POSTGRES=11.11 +integration-test-postgres
|
||||
|
||||
Then once you enter the containerized shell, you can inspect the underlying databases with the respective commands:
|
||||
|
||||
PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres -d postgres ecto_test
|
||||
MYSQL_PASSWORD=root mysql -h 127.0.0.1 -uroot -proot ecto_test
|
||||
sqlcmd -U sa -P 'some!Password'
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2012 Plataformatec \
|
||||
Copyright (c) 2020 Dashbit
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
74
phoenix/deps/ecto_sql/hex_metadata.config
Normal file
74
phoenix/deps/ecto_sql/hex_metadata.config
Normal file
@@ -0,0 +1,74 @@
|
||||
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-ecto/ecto_sql">>}]}.
|
||||
{<<"name">>,<<"ecto_sql">>}.
|
||||
{<<"version">>,<<"3.13.5">>}.
|
||||
{<<"description">>,<<"SQL-based adapters for Ecto and database migrations">>}.
|
||||
{<<"elixir">>,<<"~> 1.14">>}.
|
||||
{<<"files">>,
|
||||
[<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>,
|
||||
<<"lib">>,<<"lib/ecto">>,<<"lib/ecto/adapter">>,
|
||||
<<"lib/ecto/adapter/migration.ex">>,<<"lib/ecto/adapter/structure.ex">>,
|
||||
<<"lib/ecto/adapters">>,<<"lib/ecto/adapters/tds.ex">>,
|
||||
<<"lib/ecto/adapters/myxql">>,<<"lib/ecto/adapters/myxql/connection.ex">>,
|
||||
<<"lib/ecto/adapters/tds">>,<<"lib/ecto/adapters/tds/types.ex">>,
|
||||
<<"lib/ecto/adapters/tds/connection.ex">>,<<"lib/ecto/adapters/sql.ex">>,
|
||||
<<"lib/ecto/adapters/postgres">>,
|
||||
<<"lib/ecto/adapters/postgres/connection.ex">>,
|
||||
<<"lib/ecto/adapters/myxql.ex">>,<<"lib/ecto/adapters/postgres.ex">>,
|
||||
<<"lib/ecto/adapters/sql">>,<<"lib/ecto/adapters/sql/stream.ex">>,
|
||||
<<"lib/ecto/adapters/sql/sandbox.ex">>,
|
||||
<<"lib/ecto/adapters/sql/connection.ex">>,
|
||||
<<"lib/ecto/adapters/sql/application.ex">>,<<"lib/ecto/migration.ex">>,
|
||||
<<"lib/ecto/migrator.ex">>,<<"lib/ecto/migration">>,
|
||||
<<"lib/ecto/migration/runner.ex">>,
|
||||
<<"lib/ecto/migration/schema_migration.ex">>,<<"lib/mix">>,
|
||||
<<"lib/mix/tasks">>,<<"lib/mix/tasks/ecto.migrations.ex">>,
|
||||
<<"lib/mix/tasks/ecto.dump.ex">>,<<"lib/mix/tasks/ecto.rollback.ex">>,
|
||||
<<"lib/mix/tasks/ecto.migrate.ex">>,
|
||||
<<"lib/mix/tasks/ecto.gen.migration.ex">>,<<"lib/mix/tasks/ecto.load.ex">>,
|
||||
<<"lib/mix/ecto_sql.ex">>,<<"integration_test/sql">>,
|
||||
<<"integration_test/sql/migrator.exs">>,
|
||||
<<"integration_test/sql/query_many.exs">>,
|
||||
<<"integration_test/sql/lock.exs">>,
|
||||
<<"integration_test/sql/transaction.exs">>,
|
||||
<<"integration_test/sql/alter.exs">>,<<"integration_test/sql/stream.exs">>,
|
||||
<<"integration_test/sql/sql.exs">>,<<"integration_test/sql/sandbox.exs">>,
|
||||
<<"integration_test/sql/subquery.exs">>,
|
||||
<<"integration_test/sql/logging.exs">>,
|
||||
<<"integration_test/sql/migration.exs">>,<<"integration_test/support">>,
|
||||
<<"integration_test/support/file_helpers.exs">>,
|
||||
<<"integration_test/support/migration.exs">>,
|
||||
<<"integration_test/support/repo.exs">>]}.
|
||||
{<<"app">>,<<"ecto_sql">>}.
|
||||
{<<"licenses">>,[<<"Apache-2.0">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"ecto">>},
|
||||
{<<"app">>,<<"ecto">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 3.13.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"telemetry">>},
|
||||
{<<"app">>,<<"telemetry">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 0.4.0 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"db_connection">>},
|
||||
{<<"app">>,<<"db_connection">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 2.5 or ~> 2.4.1">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"postgrex">>},
|
||||
{<<"app">>,<<"postgrex">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 0.19 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"myxql">>},
|
||||
{<<"app">>,<<"myxql">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 0.7">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"tds">>},
|
||||
{<<"app">>,<<"tds">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 2.1.1 or ~> 2.2">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
90
phoenix/deps/ecto_sql/integration_test/sql/alter.exs
Normal file
90
phoenix/deps/ecto_sql/integration_test/sql/alter.exs
Normal file
@@ -0,0 +1,90 @@
|
||||
defmodule Ecto.Integration.AlterTest do
|
||||
use Ecto.Integration.Case, async: false
|
||||
|
||||
alias Ecto.Integration.PoolRepo
|
||||
|
||||
defmodule AlterMigrationOne do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:alter_col_type) do
|
||||
add :value, :integer
|
||||
end
|
||||
|
||||
execute "INSERT INTO alter_col_type (value) VALUES (1)"
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:alter_col_type)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterMigrationTwo do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:alter_col_type) do
|
||||
modify :value, :numeric
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:alter_col_type) do
|
||||
modify :value, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
import Ecto.Query, only: [from: 1, from: 2]
|
||||
|
||||
defp run(direction, repo, module) do
|
||||
Ecto.Migration.Runner.run(repo, repo.config(), 1, module, :forward, direction, direction, log: false)
|
||||
end
|
||||
|
||||
test "reset cache on returning query after alter column type" do
|
||||
values = from v in "alter_col_type", select: v.value
|
||||
|
||||
assert :ok == run(:up, PoolRepo, AlterMigrationOne)
|
||||
assert PoolRepo.all(values) == [1]
|
||||
|
||||
assert :ok == run(:up, PoolRepo, AlterMigrationTwo)
|
||||
[%Decimal{}] = PoolRepo.all(values)
|
||||
|
||||
PoolRepo.transaction(fn() ->
|
||||
assert [%Decimal{}] = PoolRepo.all(values)
|
||||
assert :ok == run(:down, PoolRepo, AlterMigrationTwo)
|
||||
|
||||
# Optionally fail once with database error when
|
||||
# already prepared on connection (and clear cache)
|
||||
try do
|
||||
PoolRepo.all(values, [mode: :savepoint])
|
||||
rescue
|
||||
_ ->
|
||||
assert PoolRepo.all(values) == [1]
|
||||
else
|
||||
result ->
|
||||
assert result == [1]
|
||||
end
|
||||
end)
|
||||
after
|
||||
assert :ok == run(:down, PoolRepo, AlterMigrationOne)
|
||||
end
|
||||
|
||||
test "reset cache on parameterized query after alter column type" do
|
||||
values = from v in "alter_col_type"
|
||||
|
||||
assert :ok == run(:up, PoolRepo, AlterMigrationOne)
|
||||
assert PoolRepo.update_all(values, [set: [value: 2]]) == {1, nil}
|
||||
|
||||
assert :ok == run(:up, PoolRepo, AlterMigrationTwo)
|
||||
assert PoolRepo.update_all(values, [set: [value: 3]]) == {1, nil}
|
||||
|
||||
PoolRepo.transaction(fn() ->
|
||||
assert PoolRepo.update_all(values, [set: [value: Decimal.new(5)]]) == {1, nil}
|
||||
assert :ok == run(:down, PoolRepo, AlterMigrationTwo)
|
||||
assert PoolRepo.update_all(values, [set: [value: 6]]) == {1, nil}
|
||||
end)
|
||||
after
|
||||
assert :ok == run(:down, PoolRepo, AlterMigrationOne)
|
||||
end
|
||||
end
|
||||
59
phoenix/deps/ecto_sql/integration_test/sql/lock.exs
Normal file
59
phoenix/deps/ecto_sql/integration_test/sql/lock.exs
Normal file
@@ -0,0 +1,59 @@
|
||||
defmodule Ecto.Integration.LockTest do
|
||||
# We can keep this test async as long as it
|
||||
# is the only one accessing the lock_test table.
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Ecto.Query
|
||||
alias Ecto.Integration.PoolRepo
|
||||
|
||||
defmodule LockCounter do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "lock_counters" do
|
||||
field :count, :integer
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
PoolRepo.delete_all(LockCounter)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "lock for update" do
|
||||
%{id: id} = PoolRepo.insert!(%LockCounter{count: 1})
|
||||
pid = self()
|
||||
|
||||
lock_for_update =
|
||||
Application.get_env(:ecto_sql, :lock_for_update) ||
|
||||
raise ":lock_for_update not set in :ecto application"
|
||||
|
||||
# Here we are manually inserting the lock in the query
|
||||
# to test multiple adapters. Never do this in actual
|
||||
# application code: it is not safe and not public.
|
||||
query = from(lc in LockCounter, where: lc.id == ^id)
|
||||
query = %{query | lock: lock_for_update}
|
||||
|
||||
{:ok, new_pid} =
|
||||
Task.start_link fn ->
|
||||
assert_receive :select_for_update, 5000
|
||||
|
||||
PoolRepo.transaction(fn ->
|
||||
[post] = PoolRepo.all(query) # this should block until the other trans. commit
|
||||
post |> Ecto.Changeset.change(count: post.count + 1) |> PoolRepo.update!
|
||||
end)
|
||||
|
||||
send pid, :updated
|
||||
end
|
||||
|
||||
PoolRepo.transaction(fn ->
|
||||
[post] = PoolRepo.all(query) # select and lock the row
|
||||
send new_pid, :select_for_update # signal second process to begin a transaction
|
||||
post |> Ecto.Changeset.change(count: post.count + 1) |> PoolRepo.update!
|
||||
end)
|
||||
|
||||
assert_receive :updated, 5000
|
||||
|
||||
# Final count will be 3 if SELECT ... FOR UPDATE worked and 2 otherwise
|
||||
assert [%LockCounter{count: 3}] = PoolRepo.all(LockCounter)
|
||||
end
|
||||
end
|
||||
857
phoenix/deps/ecto_sql/integration_test/sql/logging.exs
Normal file
857
phoenix/deps/ecto_sql/integration_test/sql/logging.exs
Normal file
@@ -0,0 +1,857 @@
|
||||
defmodule Ecto.Integration.LoggingTest do
|
||||
use Ecto.Integration.Case, async: true
|
||||
|
||||
alias Ecto.Integration.TestRepo
|
||||
alias Ecto.Integration.PoolRepo
|
||||
alias Ecto.Integration.{Post, Logging, ArrayLogging}
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
describe "telemetry" do
|
||||
test "dispatches event" do
|
||||
log = fn event_name, measurements, metadata ->
|
||||
assert Enum.at(event_name, -1) == :query
|
||||
assert %{result: {:ok, _res}} = metadata
|
||||
|
||||
assert measurements.total_time ==
|
||||
measurements.query_time + measurements.decode_time + measurements.queue_time
|
||||
|
||||
assert measurements.idle_time
|
||||
send(self(), :logged)
|
||||
end
|
||||
|
||||
Process.put(:telemetry, log)
|
||||
_ = PoolRepo.all(Post)
|
||||
assert_received :logged
|
||||
end
|
||||
|
||||
test "dispatches event with stacktrace" do
|
||||
log = fn _event_name, _measurements, metadata ->
|
||||
assert %{stacktrace: [_ | _]} = metadata
|
||||
send(self(), :logged)
|
||||
end
|
||||
|
||||
Process.put(:telemetry, log)
|
||||
_ = PoolRepo.all(Post, stacktrace: true)
|
||||
assert_received :logged
|
||||
end
|
||||
|
||||
test "dispatches event with custom options" do
|
||||
log = fn event_name, _measurements, metadata ->
|
||||
assert Enum.at(event_name, -1) == :query
|
||||
assert metadata.options == [:custom_metadata]
|
||||
send(self(), :logged)
|
||||
end
|
||||
|
||||
Process.put(:telemetry, log)
|
||||
_ = PoolRepo.all(Post, telemetry_options: [:custom_metadata])
|
||||
assert_received :logged
|
||||
end
|
||||
|
||||
test "dispatches under another event name" do
|
||||
log = fn [:custom], measurements, metadata ->
|
||||
assert %{result: {:ok, _res}} = metadata
|
||||
|
||||
assert measurements.total_time ==
|
||||
measurements.query_time + measurements.decode_time + measurements.queue_time
|
||||
|
||||
assert measurements.idle_time
|
||||
send(self(), :logged)
|
||||
end
|
||||
|
||||
Process.put(:telemetry, log)
|
||||
_ = PoolRepo.all(Post, telemetry_event: [:custom])
|
||||
assert_received :logged
|
||||
end
|
||||
|
||||
test "is not dispatched with no event name" do
|
||||
Process.put(:telemetry, fn _, _ -> raise "never called" end)
|
||||
_ = TestRepo.all(Post, telemetry_event: nil)
|
||||
refute_received :logged
|
||||
end
|
||||
|
||||
test "cast params" do
|
||||
uuid_module =
|
||||
if TestRepo.__adapter__() == Ecto.Adapters.Tds do
|
||||
Tds.Ecto.UUID
|
||||
else
|
||||
Ecto.UUID
|
||||
end
|
||||
|
||||
uuid = uuid_module.generate()
|
||||
dumped_uuid = uuid_module.dump!(uuid)
|
||||
|
||||
log = fn _event_name, _measurements, metadata ->
|
||||
assert [dumped_uuid] == metadata.params
|
||||
assert [uuid] == metadata.cast_params
|
||||
send(self(), :logged)
|
||||
end
|
||||
|
||||
Process.put(:telemetry, log)
|
||||
TestRepo.all(from l in Logging, where: l.uuid == ^uuid)
|
||||
assert_received :logged
|
||||
end
|
||||
end
|
||||
|
||||
describe "logs" do
|
||||
@stacktrace_opts [stacktrace: true, log: :error]
|
||||
|
||||
defp stacktrace_entry(line) do
|
||||
~r/↳ anonymous fn\/0 in Ecto.Integration.LoggingTest.\"test logs includes stacktraces\"\/1, at: .*integration_test\/sql\/logging.exs:#{line - 3}/
|
||||
end
|
||||
|
||||
test "when some measurements are nil" do
|
||||
assert capture_log(fn -> TestRepo.query("BEG", [], log: :error) end) =~
|
||||
"[error]"
|
||||
end
|
||||
|
||||
test "includes stacktraces" do
|
||||
assert capture_log(fn ->
|
||||
TestRepo.all(Post, @stacktrace_opts)
|
||||
|
||||
:ok
|
||||
end) =~ stacktrace_entry(__ENV__.line)
|
||||
|
||||
assert capture_log(fn ->
|
||||
TestRepo.insert(%Post{}, @stacktrace_opts)
|
||||
|
||||
:ok
|
||||
end) =~ stacktrace_entry(__ENV__.line)
|
||||
|
||||
assert capture_log(fn ->
|
||||
# Test cascading options
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.insert(:post, %Post{})
|
||||
|> TestRepo.transaction(@stacktrace_opts)
|
||||
|
||||
:ok
|
||||
end) =~ stacktrace_entry(__ENV__.line)
|
||||
|
||||
assert capture_log(fn ->
|
||||
# In theory we should point to the call _inside_ run
|
||||
# but all multi calls point to the transaction starting point.
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.run(:all, fn _, _ -> {:ok, TestRepo.all(Post, @stacktrace_opts)} end)
|
||||
|> TestRepo.transaction()
|
||||
|
||||
:ok
|
||||
end) =~ stacktrace_entry(__ENV__.line)
|
||||
|
||||
out = capture_log(fn ->
|
||||
TestRepo.all(Post, Keyword.put(@stacktrace_opts, :log_stacktrace_mfa, {Ecto.Adapters.SQL, :first_non_ecto_stacktrace, [2]}))
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert out =~ stacktrace_entry(__ENV__.line - 2)
|
||||
|
||||
# We are a bit liberal with what we expect as we don't want to tie to internal ExUnit code
|
||||
assert out =~ ~r/ ExUnit.CaptureLog.*/
|
||||
end
|
||||
|
||||
test "with custom log level" do
|
||||
assert capture_log(fn -> TestRepo.insert!(%Post{title: "1"}, log: :error) end) =~
|
||||
"[error]"
|
||||
|
||||
# We cannot assert on the result because it depends on the suite log level
|
||||
capture_log(fn ->
|
||||
TestRepo.insert!(%Post{title: "1"}, log: true)
|
||||
end)
|
||||
|
||||
# But this assertion is always true
|
||||
assert capture_log(fn ->
|
||||
TestRepo.insert!(%Post{title: "1"}, log: false)
|
||||
end) == ""
|
||||
end
|
||||
|
||||
test "with a log: true override when logging is disabled" do
|
||||
refute capture_log(fn ->
|
||||
TestRepo.insert!(%Post{title: "1"}, log: true)
|
||||
end) =~ "an exception was raised logging"
|
||||
end
|
||||
|
||||
test "with unspecified :log option when logging is disabled" do
|
||||
refute capture_log(fn ->
|
||||
TestRepo.insert!(%Post{title: "1"})
|
||||
end) =~ "an exception was raised logging"
|
||||
end
|
||||
end
|
||||
|
||||
describe "parameter logging" do
|
||||
@describetag :parameter_logging
|
||||
|
||||
@uuid_regex ~r/[0-9a-f]{2}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
|
||||
@naive_datetime_regex ~r/~N\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\]/
|
||||
|
||||
test "for insert_all with query" do
|
||||
# Source query
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
source_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^int and l.uuid == ^uuid,
|
||||
select: %{uuid: l.uuid, int: l.int}
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, source_query, log: :info)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# Query parameters
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
end
|
||||
|
||||
@tag :insert_select
|
||||
test "for insert_all with entries" do
|
||||
# Row 1
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
|
||||
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
row1 = [
|
||||
int: int,
|
||||
uuid: uuid_query,
|
||||
inserted_at: datetime,
|
||||
updated_at: datetime
|
||||
]
|
||||
|
||||
# Row 2
|
||||
int2 = 2
|
||||
uuid2 = Ecto.UUID.generate()
|
||||
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
|
||||
datetime2 = NaiveDateTime.add(datetime, 1)
|
||||
|
||||
row2 = [
|
||||
int: int_query,
|
||||
uuid: uuid2,
|
||||
inserted_at: datetime2,
|
||||
updated_at: datetime2
|
||||
]
|
||||
|
||||
# Extract the parameters from the log:
|
||||
# 1. Remove the colour codes
|
||||
# 2. Remove the log level
|
||||
# 3. Capture everything inside of the square brackets
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, [row1, row2], log: :info)
|
||||
end)
|
||||
|
||||
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
|
||||
log = Regex.replace(~r/\[info\]/, log, "")
|
||||
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
|
||||
log_params = String.split(log["params"], ",")
|
||||
|
||||
# Compute the expected parameters in the right order.
|
||||
# This involves recreating the headers in the same order
|
||||
# as `insert_all`. The user values come first and then
|
||||
# the autogenerated id
|
||||
headers =
|
||||
row1
|
||||
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|
||||
|> Map.put(:bid, true)
|
||||
|> Map.keys()
|
||||
|
||||
row1_regex = [
|
||||
int: "#{int}",
|
||||
uuid: ["#{int}", uuid],
|
||||
inserted_at: inspect(datetime),
|
||||
updated_at: inspect(datetime),
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
row2_regex = [
|
||||
int: ["#{int2}", uuid2],
|
||||
uuid: uuid2,
|
||||
inserted_at: inspect(datetime2),
|
||||
updated_at: inspect(datetime2),
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
expected_param_regex =
|
||||
Enum.flat_map([row1_regex, row2_regex], fn row ->
|
||||
Enum.flat_map(headers, fn field ->
|
||||
case Keyword.get(row, field) do
|
||||
params when is_list(params) -> params
|
||||
param -> [param]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
assert length(log_params) == length(expected_param_regex)
|
||||
|
||||
Enum.zip(log_params, expected_param_regex)
|
||||
|> Enum.each(fn {log, expected_regex} ->
|
||||
assert log =~ expected_regex
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :insert_select
|
||||
@tag :placeholders
|
||||
test "for insert_all with entries and placeholders" do
|
||||
# Placeholders
|
||||
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
datetime2 = NaiveDateTime.add(datetime, 1)
|
||||
placeholder_map = %{datetime: datetime, datetime2: datetime2}
|
||||
|
||||
# Row 1
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
|
||||
|
||||
row1 = [
|
||||
int: int,
|
||||
uuid: uuid_query,
|
||||
inserted_at: {:placeholder, :datetime},
|
||||
updated_at: {:placeholder, :datetime}
|
||||
]
|
||||
|
||||
# Row 2
|
||||
int2 = 2
|
||||
uuid2 = Ecto.UUID.generate()
|
||||
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
|
||||
|
||||
row2 = [
|
||||
int: int_query,
|
||||
uuid: uuid2,
|
||||
inserted_at: {:placeholder, :datetime2},
|
||||
updated_at: {:placeholder, :datetime2}
|
||||
]
|
||||
|
||||
# Extract the parameters from the log:
|
||||
# 1. Remove the colour codes
|
||||
# 2. Remove the log level
|
||||
# 3. Capture everything inside of the square brackets
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, [row1, row2],
|
||||
placeholders: placeholder_map,
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
|
||||
log = Regex.replace(~r/\[info\]/, log, "")
|
||||
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
|
||||
log_params = String.split(log["params"], ",")
|
||||
|
||||
# Compute the expected parameters in the right order.
|
||||
# This involves recreating the headers in the same order
|
||||
# as `insert_all`. The placeholders come first and then
|
||||
# the user values and then the autogenerated id
|
||||
headers =
|
||||
row1
|
||||
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|
||||
|> Map.put(:bid, true)
|
||||
|> Map.drop([:inserted_at, :updated_at])
|
||||
|> Map.keys()
|
||||
|
||||
row1_regex = [
|
||||
int: "#{int}",
|
||||
uuid: ["#{int}", uuid],
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
row2_regex = [
|
||||
int: ["#{int2}", uuid2],
|
||||
uuid: uuid2,
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
row_param_regex =
|
||||
Enum.flat_map([row1_regex, row2_regex], fn row ->
|
||||
Enum.flat_map(headers, fn field ->
|
||||
case Keyword.get(row, field) do
|
||||
params when is_list(params) -> params
|
||||
param -> [param]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
placeholder_regex = [inspect(datetime), inspect(datetime2)]
|
||||
expected_param_regex = placeholder_regex ++ row_param_regex
|
||||
|
||||
assert length(log_params) == length(expected_param_regex)
|
||||
|
||||
Enum.zip(log_params, expected_param_regex)
|
||||
|> Enum.each(fn {log, expected_regex} ->
|
||||
assert log =~ expected_regex
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :with_conflict_target
|
||||
test "for insert_all with query with conflict query" do
|
||||
# Source query
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
source_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^int and l.uuid == ^uuid,
|
||||
select: %{uuid: l.uuid, int: l.int}
|
||||
|
||||
# Conflict query
|
||||
conflict_int = 0
|
||||
conflict_uuid = Ecto.UUID.generate()
|
||||
conflict_update = 2
|
||||
|
||||
conflict_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
|
||||
update: [set: [int: ^conflict_update]]
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, source_query,
|
||||
on_conflict: conflict_query,
|
||||
conflict_target: :bid,
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex =
|
||||
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<conflict_update>.+), (?<conflict_int>.+), \"(?<conflict_uuid>.+)\"\]/
|
||||
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# Query parameters
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
# Conflict query parameters
|
||||
assert param_logs["conflict_update"] == Integer.to_string(conflict_update)
|
||||
assert param_logs["conflict_int"] == Integer.to_string(conflict_int)
|
||||
assert param_logs["conflict_uuid"] == conflict_uuid
|
||||
end
|
||||
|
||||
@tag :insert_select
|
||||
@tag :with_conflict_target
|
||||
test "for insert_all with entries conflict query" do
|
||||
# Row 1
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
|
||||
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
row1 = [
|
||||
int: int,
|
||||
uuid: uuid_query,
|
||||
inserted_at: datetime,
|
||||
updated_at: datetime
|
||||
]
|
||||
|
||||
# Row 2
|
||||
int2 = 2
|
||||
uuid2 = Ecto.UUID.generate()
|
||||
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
|
||||
datetime2 = NaiveDateTime.add(datetime, 1)
|
||||
|
||||
row2 = [
|
||||
int: int_query,
|
||||
uuid: uuid2,
|
||||
inserted_at: datetime2,
|
||||
updated_at: datetime2
|
||||
]
|
||||
|
||||
# Conflict query
|
||||
conflict_int = 0
|
||||
conflict_uuid = Ecto.UUID.generate()
|
||||
conflict_update = 2
|
||||
|
||||
conflict_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
|
||||
update: [set: [int: ^conflict_update]]
|
||||
|
||||
# Extract the parameters from the log:
|
||||
# 1. Remove the colour codes
|
||||
# 2. Remove the log level
|
||||
# 3. Capture everything inside of the square brackets
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, [row1, row2],
|
||||
on_conflict: conflict_query,
|
||||
conflict_target: :bid,
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
|
||||
log = Regex.replace(~r/\[info\]/, log, "")
|
||||
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
|
||||
log_params = String.split(log["params"], ",")
|
||||
|
||||
# Compute the expected parameters in the right order.
|
||||
# This involves recreating the headers in the same order
|
||||
# as `insert_all`. The user values come first, then
|
||||
# the autogenerated id and then the conflict params
|
||||
headers =
|
||||
row1
|
||||
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|
||||
|> Map.put(:bid, true)
|
||||
|> Map.keys()
|
||||
|
||||
row1_regex = [
|
||||
int: "#{int}",
|
||||
uuid: ["#{int}", uuid],
|
||||
bid: @uuid_regex,
|
||||
inserted_at: inspect(datetime),
|
||||
updated_at: inspect(datetime)
|
||||
]
|
||||
|
||||
row2_regex = [
|
||||
int: ["#{int2}", uuid2],
|
||||
uuid: uuid2,
|
||||
bid: @uuid_regex,
|
||||
inserted_at: inspect(datetime2),
|
||||
updated_at: inspect(datetime2)
|
||||
]
|
||||
|
||||
row_param_regex =
|
||||
Enum.flat_map([row1_regex, row2_regex], fn row ->
|
||||
Enum.flat_map(headers, fn field ->
|
||||
case Keyword.get(row, field) do
|
||||
row_params when is_list(row_params) -> row_params
|
||||
row_param -> [row_param]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
conflict_param_regex = ["#{conflict_update}", "#{conflict_int}", conflict_uuid]
|
||||
expected_param_regex = row_param_regex ++ conflict_param_regex
|
||||
|
||||
assert length(log_params) == length(expected_param_regex)
|
||||
|
||||
Enum.zip(log_params, expected_param_regex)
|
||||
|> Enum.each(fn {log, expected_regex} ->
|
||||
assert log =~ expected_regex
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :insert_select
|
||||
@tag :placeholders
|
||||
@tag :with_conflict_target
|
||||
test "for insert_all with entries, placeholders and conflict query" do
|
||||
# Row 1
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
|
||||
|
||||
row1 = [
|
||||
int: int,
|
||||
uuid: uuid_query,
|
||||
inserted_at: {:placeholder, :datetime},
|
||||
updated_at: {:placeholder, :datetime2}
|
||||
]
|
||||
|
||||
# Row 2
|
||||
int2 = 2
|
||||
uuid2 = Ecto.UUID.generate()
|
||||
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
|
||||
|
||||
row2 = [
|
||||
int: int_query,
|
||||
uuid: uuid2,
|
||||
inserted_at: {:placeholder, :datetime},
|
||||
updated_at: {:placeholder, :datetime2}
|
||||
]
|
||||
|
||||
# Placeholders
|
||||
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
datetime2 = NaiveDateTime.add(datetime, 1)
|
||||
placeholder_map = %{datetime: datetime, datetime2: datetime2}
|
||||
|
||||
# Conflict query
|
||||
conflict_int = 0
|
||||
conflict_uuid = Ecto.UUID.generate()
|
||||
conflict_update = 2
|
||||
|
||||
conflict_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
|
||||
update: [set: [int: ^conflict_update]]
|
||||
|
||||
# Extract the parameters from the log:
|
||||
# 1. Remove the colour codes
|
||||
# 2. Remove the log level
|
||||
# 3. Capture everything inside of the square brackets
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert_all(Logging, [row1, row2],
|
||||
placeholders: placeholder_map,
|
||||
on_conflict: conflict_query,
|
||||
conflict_target: :bid,
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
|
||||
log = Regex.replace(~r/\[info\]/, log, "")
|
||||
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
|
||||
log_params = String.split(log["params"], ",")
|
||||
|
||||
# Compute the expected parameters in the right order.
|
||||
# This involves recreating the headers in the same order
|
||||
# as `insert_all`. The placeholders come first, then the
|
||||
# user value, then the autogenerated id and then the conflict
|
||||
# params
|
||||
headers =
|
||||
row1
|
||||
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|
||||
|> Map.put(:bid, true)
|
||||
|> Map.drop([:inserted_at, :updated_at])
|
||||
|> Map.keys()
|
||||
|
||||
row1_regex = [
|
||||
int: "#{int}",
|
||||
uuid: ["#{int}", uuid],
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
row2_regex = [
|
||||
int: ["#{int2}", uuid2],
|
||||
uuid: uuid2,
|
||||
bid: @uuid_regex
|
||||
]
|
||||
|
||||
row_param_regex =
|
||||
Enum.flat_map([row1_regex, row2_regex], fn row ->
|
||||
Enum.flat_map(headers, fn field ->
|
||||
case Keyword.get(row, field) do
|
||||
row_params when is_list(row_params) -> row_params
|
||||
row_param -> [row_param]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
placeholder_regex = [inspect(datetime), inspect(datetime2)]
|
||||
conflict_param_regex = ["#{conflict_update}", "#{conflict_int}", conflict_uuid]
|
||||
expected_param_regex = placeholder_regex ++ row_param_regex ++ conflict_param_regex
|
||||
|
||||
assert length(log_params) == length(expected_param_regex)
|
||||
|
||||
Enum.zip(log_params, expected_param_regex)
|
||||
|> Enum.each(fn {log, expected_regex} ->
|
||||
assert log =~ expected_regex
|
||||
end)
|
||||
end
|
||||
|
||||
test "for insert" do
|
||||
# Insert values
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert!(%Logging{uuid: uuid, int: int},
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex =
|
||||
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<inserted_at>.+), (?<updated_at>.+), \"(?<bid>.+)\"\]/
|
||||
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# User changes
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
# Autogenerated changes
|
||||
assert param_logs["inserted_at"] =~ @naive_datetime_regex
|
||||
assert param_logs["updated_at"] =~ @naive_datetime_regex
|
||||
# Filters
|
||||
assert param_logs["bid"] =~ @uuid_regex
|
||||
end
|
||||
|
||||
@tag :with_conflict_target
|
||||
test "for insert with conflict query" do
|
||||
# Insert values
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
# Conflict query
|
||||
conflict_int = 0
|
||||
conflict_uuid = Ecto.UUID.generate()
|
||||
conflict_update = 2
|
||||
|
||||
conflict_query =
|
||||
from l in Logging,
|
||||
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
|
||||
update: [set: [int: ^conflict_update]]
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.insert!(%Logging{uuid: uuid, int: int},
|
||||
on_conflict: conflict_query,
|
||||
conflict_target: :bid,
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex =
|
||||
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<inserted_at>.+), (?<updated_at>.+), \"(?<bid>.+)\", (?<conflict_update>.+), (?<conflict_int>.+), \"(?<conflict_uuid>.+)\"\]/
|
||||
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# User changes
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
# Autogenerated changes
|
||||
assert param_logs["inserted_at"] =~ @naive_datetime_regex
|
||||
assert param_logs["updated_at"] =~ @naive_datetime_regex
|
||||
# Filters
|
||||
assert param_logs["bid"] =~ @uuid_regex
|
||||
# Conflict query parameters
|
||||
assert param_logs["conflict_update"] == Integer.to_string(conflict_update)
|
||||
assert param_logs["conflict_int"] == Integer.to_string(conflict_int)
|
||||
assert param_logs["conflict_uuid"] == conflict_uuid
|
||||
end
|
||||
|
||||
test "for update" do
|
||||
# Update values
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
current = TestRepo.insert!(%Logging{})
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.update!(Ecto.Changeset.change(current, %{uuid: uuid, int: int}), log: :info)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<updated_at>.+), \"(?<bid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# User changes
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
# Autogenerated changes
|
||||
assert param_logs["updated_at"] =~ @naive_datetime_regex
|
||||
# Filters
|
||||
assert param_logs["bid"] == current.bid
|
||||
end
|
||||
|
||||
test "for delete" do
|
||||
current = TestRepo.insert!(%Logging{})
|
||||
|
||||
# Ensure parameters are complete and in correct order
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.delete!(current, log: :info)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[\"(?<bid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
# Filters
|
||||
assert param_logs["bid"] == current.bid
|
||||
end
|
||||
|
||||
test "for queries" do
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
# all
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.all(
|
||||
from(l in Logging,
|
||||
select: type(^"1", :integer),
|
||||
where: l.int == ^int and l.uuid == ^uuid
|
||||
),
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<tagged_int>.+), (?<int>.+), \"(?<uuid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
assert param_logs["tagged_int"] == Integer.to_string(int)
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
|
||||
# update_all
|
||||
update = 2
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
from(l in Logging,
|
||||
where: l.int == ^int and l.uuid == ^uuid,
|
||||
update: [set: [int: ^update]]
|
||||
)
|
||||
|> TestRepo.update_all([], log: :info)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<update>.+), (?<int>.+), \"(?<uuid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
assert param_logs["update"] == Integer.to_string(update)
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
|
||||
# delete_all
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.delete_all(from(l in Logging, where: l.int == ^int and l.uuid == ^uuid),
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
end
|
||||
|
||||
@tag :stream
|
||||
test "for queries with stream" do
|
||||
int = 1
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
stream =
|
||||
TestRepo.stream(from(l in Logging, where: l.int == ^int and l.uuid == ^uuid),
|
||||
log: :info
|
||||
)
|
||||
|
||||
TestRepo.transaction(fn -> Enum.to_list(stream) end)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
assert param_logs["int"] == Integer.to_string(int)
|
||||
assert param_logs["uuid"] == uuid
|
||||
end
|
||||
|
||||
@tag :array_type
|
||||
test "for queries with array type" do
|
||||
uuid = Ecto.UUID.generate()
|
||||
uuid2 = Ecto.UUID.generate()
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
TestRepo.all(from(a in ArrayLogging, where: a.uuids == ^[uuid, uuid2]),
|
||||
log: :info
|
||||
)
|
||||
end)
|
||||
|
||||
param_regex = ~r/\[(?<uuids>\[.+\])\]/
|
||||
param_logs = Regex.named_captures(param_regex, log)
|
||||
|
||||
assert param_logs["uuids"] == "[\"#{uuid}\", \"#{uuid2}\"]"
|
||||
end
|
||||
end
|
||||
end
|
||||
775
phoenix/deps/ecto_sql/integration_test/sql/migration.exs
Normal file
775
phoenix/deps/ecto_sql/integration_test/sql/migration.exs
Normal file
@@ -0,0 +1,775 @@
|
||||
defmodule Ecto.Integration.MigrationTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Ecto.Integration.{TestRepo, PoolRepo}
|
||||
|
||||
defmodule CreateMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
@table table(:create_table_migration)
|
||||
@index index(:create_table_migration, [:value], unique: true)
|
||||
|
||||
def up do
|
||||
create @table do
|
||||
add :value, :integer
|
||||
end
|
||||
create @index
|
||||
end
|
||||
|
||||
def down do
|
||||
drop @index
|
||||
drop @table
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AddColumnMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:add_col_migration) do
|
||||
add :value, :integer
|
||||
end
|
||||
|
||||
alter table(:add_col_migration) do
|
||||
add :to_be_added, :integer
|
||||
end
|
||||
|
||||
execute "INSERT INTO add_col_migration (value, to_be_added) VALUES (1, 2)"
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:add_col_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterColumnMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:alter_col_migration) do
|
||||
add :from_null_to_not_null, :integer
|
||||
add :from_not_null_to_null, :integer, null: false
|
||||
|
||||
add :from_default_to_no_default, :integer, default: 0
|
||||
add :from_no_default_to_default, :integer
|
||||
end
|
||||
|
||||
alter table(:alter_col_migration) do
|
||||
modify :from_null_to_not_null, :string, null: false
|
||||
modify :from_not_null_to_null, :string, null: true
|
||||
|
||||
modify :from_default_to_no_default, :integer, default: nil
|
||||
modify :from_no_default_to_default, :integer, default: 0
|
||||
end
|
||||
|
||||
execute "INSERT INTO alter_col_migration (from_null_to_not_null) VALUES ('foo')"
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:alter_col_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterColumnFromMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:modify_from_products) do
|
||||
add :value, :integer
|
||||
add :nullable, :integer, null: false
|
||||
end
|
||||
|
||||
if direction() == :up do
|
||||
flush()
|
||||
PoolRepo.insert_all "modify_from_products", [[value: 1, nullable: 1]]
|
||||
end
|
||||
|
||||
alter table(:modify_from_products) do
|
||||
modify :value, :bigint, from: :integer
|
||||
modify :nullable, :bigint, null: true, from: {:integer, null: false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterColumnFromPkeyMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:modify_from_authors, primary_key: false) do
|
||||
add :id, :integer, primary_key: true
|
||||
end
|
||||
create table(:modify_from_posts) do
|
||||
add :author_id, references(:modify_from_authors, type: :integer)
|
||||
end
|
||||
|
||||
if direction() == :up do
|
||||
flush()
|
||||
PoolRepo.insert_all "modify_from_authors", [[id: 1]]
|
||||
PoolRepo.insert_all "modify_from_posts", [[author_id: 1]]
|
||||
end
|
||||
|
||||
alter table(:modify_from_posts) do
|
||||
# remove the constraints modify_from_posts_author_id_fkey
|
||||
modify :author_id, :integer, from: references(:modify_from_authors, type: :integer)
|
||||
end
|
||||
alter table(:modify_from_authors) do
|
||||
modify :id, :bigint, from: :integer
|
||||
end
|
||||
alter table(:modify_from_posts) do
|
||||
# add the constraints modify_from_posts_author_id_fkey
|
||||
modify :author_id, references(:modify_from_authors, type: :bigint), from: :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterForeignKeyOnDeleteMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:alter_fk_users)
|
||||
|
||||
create table(:alter_fk_posts) do
|
||||
add :alter_fk_user_id, :id
|
||||
end
|
||||
|
||||
alter table(:alter_fk_posts) do
|
||||
modify :alter_fk_user_id, references(:alter_fk_users, on_delete: :nilify_all)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:alter_fk_posts)
|
||||
drop table(:alter_fk_users)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterForeignKeyOnUpdateMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:alter_fk_users)
|
||||
|
||||
create table(:alter_fk_posts) do
|
||||
add :alter_fk_user_id, :id
|
||||
end
|
||||
|
||||
alter table(:alter_fk_posts) do
|
||||
modify :alter_fk_user_id, references(:alter_fk_users, on_update: :update_all)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:alter_fk_posts)
|
||||
drop table(:alter_fk_users)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule DropColumnMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:drop_col_migration) do
|
||||
add :value, :integer
|
||||
add :to_be_removed, :integer
|
||||
end
|
||||
|
||||
execute "INSERT INTO drop_col_migration (value, to_be_removed) VALUES (1, 2)"
|
||||
|
||||
alter table(:drop_col_migration) do
|
||||
remove :to_be_removed
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:drop_col_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RenameColumnMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:rename_col_migration) do
|
||||
add :to_be_renamed, :integer
|
||||
end
|
||||
|
||||
rename table(:rename_col_migration), :to_be_renamed, to: :was_renamed
|
||||
|
||||
execute "INSERT INTO rename_col_migration (was_renamed) VALUES (1)"
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:rename_col_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule OnDeleteMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:parent1)
|
||||
create table(:parent2)
|
||||
|
||||
create table(:ref_migration) do
|
||||
add :parent1, references(:parent1, on_delete: :nilify_all)
|
||||
end
|
||||
|
||||
alter table(:ref_migration) do
|
||||
add :parent2, references(:parent2, on_delete: :delete_all)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:ref_migration)
|
||||
drop table(:parent1)
|
||||
drop table(:parent2)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule OnDeleteNilifyColumnsMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:parent) do
|
||||
add :col1, :integer
|
||||
add :col2, :integer
|
||||
end
|
||||
|
||||
create unique_index(:parent, [:id, :col1, :col2])
|
||||
|
||||
create table(:ref) do
|
||||
add :col1, :integer
|
||||
add :col2, :integer
|
||||
add :parent_id,
|
||||
references(:parent,
|
||||
with: [col1: :col1, col2: :col2],
|
||||
on_delete: {:nilify, [:parent_id, :col2]}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:ref)
|
||||
drop table(:parent)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule OnDeleteDefaultAllMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:parent, primary_key: [type: :bigint]) do
|
||||
add :col1, :integer
|
||||
add :col2, :integer
|
||||
end
|
||||
|
||||
create unique_index(:parent, [:id, :col1, :col2])
|
||||
|
||||
create table(:ref) do
|
||||
add :col1, :integer, default: 2
|
||||
add :col2, :integer, default: 3
|
||||
|
||||
add :parent_id,
|
||||
references(:parent,
|
||||
with: [col1: :col1, col2: :col2],
|
||||
on_delete: :default_all
|
||||
), default: 1
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:ref)
|
||||
drop table(:parent)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule OnDeleteDefaultColumnsMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:parent, primary_key: [type: :bigint]) do
|
||||
add :col1, :integer
|
||||
add :col2, :integer
|
||||
end
|
||||
|
||||
create unique_index(:parent, [:id, :col1, :col2])
|
||||
|
||||
create table(:ref) do
|
||||
add :col1, :integer, default: 2
|
||||
add :col2, :integer, default: 3
|
||||
|
||||
add :parent_id,
|
||||
references(:parent,
|
||||
with: [col1: :col1, col2: :col2],
|
||||
on_delete: {:default, [:parent_id, :col2]}
|
||||
), default: 1
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:ref)
|
||||
drop table(:parent)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule CompositeForeignKeyMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:composite_parent) do
|
||||
add :key_id, :integer
|
||||
end
|
||||
|
||||
create unique_index(:composite_parent, [:id, :key_id])
|
||||
|
||||
create table(:composite_child) do
|
||||
add :parent_key_id, :integer
|
||||
add :parent_id, references(:composite_parent, with: [parent_key_id: :key_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RenameIndexMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:composite_parent) do
|
||||
add :key_id, :integer
|
||||
end
|
||||
|
||||
create unique_index(:composite_parent, [:id, :key_id], name: "old_index_name")
|
||||
|
||||
rename index(:composite_parent, [:id, :key_id], name: "old_index_name"), to: "new_index_name"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ReferencesRollbackMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:parent) do
|
||||
add :name, :string
|
||||
end
|
||||
|
||||
create table(:child) do
|
||||
add :parent_id, references(:parent)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RenameMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
@table_current table(:posts_migration)
|
||||
@table_new table(:new_posts_migration)
|
||||
|
||||
def up do
|
||||
create @table_current
|
||||
rename @table_current, to: @table_new
|
||||
end
|
||||
|
||||
def down do
|
||||
drop @table_new
|
||||
end
|
||||
end
|
||||
|
||||
defmodule PrefixMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
@prefix "ecto_prefix_test"
|
||||
|
||||
def up do
|
||||
execute TestRepo.create_prefix(@prefix)
|
||||
create table(:first, prefix: @prefix)
|
||||
create table(:second, prefix: @prefix) do
|
||||
add :first_id, references(:first)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:second, prefix: @prefix)
|
||||
drop table(:first, prefix: @prefix)
|
||||
execute TestRepo.drop_prefix(@prefix)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule NoSQLMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:collection, options: [capped: true])
|
||||
execute create: "collection"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Parent do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "parent" do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule NoErrorTableMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:existing) do
|
||||
add :name, :string
|
||||
end
|
||||
|
||||
create_if_not_exists table(:existing) do
|
||||
add :name, :string
|
||||
end
|
||||
|
||||
create_if_not_exists table(:existing)
|
||||
|
||||
drop_if_exists table(:existing)
|
||||
drop_if_exists table(:existing)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule NoErrorIndexMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists index(:posts, [:title])
|
||||
create_if_not_exists index(:posts, [:title])
|
||||
drop_if_exists index(:posts, [:title])
|
||||
drop_if_exists index(:posts, [:title])
|
||||
end
|
||||
end
|
||||
|
||||
defmodule InferredDropIndexMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create index(:posts, [:title])
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AlterPrimaryKeyMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:no_pk, primary_key: false) do
|
||||
add :dummy, :string
|
||||
end
|
||||
alter table(:no_pk) do
|
||||
add :id, :serial, primary_key: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defmodule AddColumnIfNotExistsMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:add_col_if_not_exists_migration)
|
||||
|
||||
alter table(:add_col_if_not_exists_migration) do
|
||||
add_if_not_exists :value, :integer
|
||||
add_if_not_exists :to_be_added, :integer
|
||||
end
|
||||
|
||||
execute "INSERT INTO add_col_if_not_exists_migration (value, to_be_added) VALUES (1, 2)"
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:add_col_if_not_exists_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule DropColumnIfExistsMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:drop_col_if_exists_migration) do
|
||||
add :value, :integer
|
||||
add :to_be_removed, :integer
|
||||
end
|
||||
|
||||
execute "INSERT INTO drop_col_if_exists_migration (value, to_be_removed) VALUES (1, 2)"
|
||||
|
||||
alter table(:drop_col_if_exists_migration) do
|
||||
remove_if_exists :to_be_removed, :integer
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:drop_col_if_exists_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule NoErrorOnConditionalColumnMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:no_error_on_conditional_column_migration)
|
||||
|
||||
alter table(:no_error_on_conditional_column_migration) do
|
||||
add_if_not_exists :value, :integer
|
||||
add_if_not_exists :value, :integer
|
||||
|
||||
remove_if_exists :value, :integer
|
||||
remove_if_exists :value, :integer
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:no_error_on_conditional_column_migration)
|
||||
end
|
||||
end
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
import Ecto.Migrator, only: [up: 4, down: 4]
|
||||
|
||||
# Avoid migration out of order warnings
|
||||
@moduletag :capture_log
|
||||
@base_migration 1_000_000
|
||||
|
||||
setup do
|
||||
{:ok, migration_number: System.unique_integer([:positive]) + @base_migration}
|
||||
end
|
||||
|
||||
test "create and drop table and indexes", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, CreateMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, CreateMigration, log: false)
|
||||
end
|
||||
|
||||
test "correctly infers how to drop index", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, InferredDropIndexMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, InferredDropIndexMigration, log: false)
|
||||
end
|
||||
|
||||
test "supports on delete", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, OnDeleteMigration, log: false)
|
||||
|
||||
parent1 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent1")
|
||||
parent2 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent2")
|
||||
|
||||
writer = "INSERT INTO ref_migration (parent1, parent2) VALUES (#{parent1.id}, #{parent2.id})"
|
||||
PoolRepo.query!(writer)
|
||||
|
||||
reader = from r in "ref_migration", select: {r.parent1, r.parent2}
|
||||
assert PoolRepo.all(reader) == [{parent1.id, parent2.id}]
|
||||
|
||||
PoolRepo.delete!(parent1)
|
||||
assert PoolRepo.all(reader) == [{nil, parent2.id}]
|
||||
|
||||
PoolRepo.delete!(parent2)
|
||||
assert PoolRepo.all(reader) == []
|
||||
|
||||
assert :ok == down(PoolRepo, num, OnDeleteMigration, log: false)
|
||||
end
|
||||
|
||||
test "composite foreign keys", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, CompositeForeignKeyMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("composite_parent", [[key_id: 2]])
|
||||
assert [id] = PoolRepo.all(from p in "composite_parent", select: p.id)
|
||||
|
||||
catch_error(PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 1]]))
|
||||
assert {1, nil} = PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 2]])
|
||||
|
||||
assert :ok == down(PoolRepo, num, CompositeForeignKeyMigration, log: false)
|
||||
end
|
||||
|
||||
test "rename index", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, RenameIndexMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, RenameIndexMigration, log: false)
|
||||
end
|
||||
|
||||
test "rolls back references in change/1", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, ReferencesRollbackMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, ReferencesRollbackMigration, log: false)
|
||||
end
|
||||
|
||||
test "create table if not exists and drop table if exists does not raise on failure", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, NoErrorTableMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :create_index_if_not_exists
|
||||
test "create index if not exists and drop index if exists does not raise on failure", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, NoErrorIndexMigration, log: false)
|
||||
end
|
||||
|
||||
test "raises on NoSQL migrations", %{migration_number: num} do
|
||||
assert_raise ArgumentError, ~r"does not support keyword lists in :options", fn ->
|
||||
up(PoolRepo, num, NoSQLMigration, log: false)
|
||||
end
|
||||
end
|
||||
|
||||
@tag :add_column
|
||||
test "add column", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AddColumnMigration, log: false)
|
||||
assert [2] == PoolRepo.all from p in "add_col_migration", select: p.to_be_added
|
||||
:ok = down(PoolRepo, num, AddColumnMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :modify_column
|
||||
test "modify column", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterColumnMigration, log: false)
|
||||
|
||||
assert ["foo"] ==
|
||||
PoolRepo.all from p in "alter_col_migration", select: p.from_null_to_not_null
|
||||
assert [nil] ==
|
||||
PoolRepo.all from p in "alter_col_migration", select: p.from_not_null_to_null
|
||||
assert [nil] ==
|
||||
PoolRepo.all from p in "alter_col_migration", select: p.from_default_to_no_default
|
||||
assert [0] ==
|
||||
PoolRepo.all from p in "alter_col_migration", select: p.from_no_default_to_default
|
||||
|
||||
query = "INSERT INTO `alter_col_migration` (\"from_not_null_to_null\") VALUES ('foo')"
|
||||
assert catch_error(PoolRepo.query!(query))
|
||||
|
||||
:ok = down(PoolRepo, num, AlterColumnMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :modify_column
|
||||
test "modify column with from", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterColumnFromMigration, log: false)
|
||||
|
||||
assert [1] ==
|
||||
PoolRepo.all from p in "modify_from_products", select: p.value
|
||||
|
||||
:ok = down(PoolRepo, num, AlterColumnFromMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :alter_primary_key
|
||||
test "modify column with from and pkey", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)
|
||||
|
||||
assert [1] ==
|
||||
PoolRepo.all from p in "modify_from_posts", select: p.author_id
|
||||
|
||||
:ok = down(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :alter_foreign_key
|
||||
test "modify foreign key's on_delete constraint", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("alter_fk_users", [[]])
|
||||
assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id
|
||||
|
||||
PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
|
||||
PoolRepo.delete_all("alter_fk_users")
|
||||
assert [nil] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id
|
||||
|
||||
:ok = down(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :assigns_id_type
|
||||
test "modify foreign key's on_update constraint", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("alter_fk_users", [[]])
|
||||
assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id
|
||||
|
||||
PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
|
||||
PoolRepo.update_all("alter_fk_users", set: [id: 12345])
|
||||
assert [12345] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id
|
||||
|
||||
PoolRepo.delete_all("alter_fk_posts")
|
||||
:ok = down(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :remove_column
|
||||
test "remove column", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, DropColumnMigration, log: false)
|
||||
assert catch_error(PoolRepo.all from p in "drop_col_migration", select: p.to_be_removed)
|
||||
:ok = down(PoolRepo, num, DropColumnMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :rename_column
|
||||
test "rename column", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, RenameColumnMigration, log: false)
|
||||
assert [1] == PoolRepo.all from p in "rename_col_migration", select: p.was_renamed
|
||||
:ok = down(PoolRepo, num, RenameColumnMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :rename_table
|
||||
test "rename table", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, RenameMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, RenameMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :prefix
|
||||
test "prefix", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, PrefixMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, PrefixMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :alter_primary_key
|
||||
test "alter primary key", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :add_column_if_not_exists
|
||||
@tag :remove_column_if_exists
|
||||
test "add if not exists and remove if exists does not raise on failure", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
|
||||
assert :ok == down(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :add_column_if_not_exists
|
||||
test "add column if not exists", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
|
||||
assert [2] == PoolRepo.all from p in "add_col_if_not_exists_migration", select: p.to_be_added
|
||||
:ok = down(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :remove_column_if_exists
|
||||
test "remove column when exists", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, DropColumnIfExistsMigration, log: false)
|
||||
assert catch_error(PoolRepo.all from p in "drop_col_if_exists_migration", select: p.to_be_removed)
|
||||
:ok = down(PoolRepo, num, DropColumnIfExistsMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :on_delete_nilify_column_list
|
||||
test "nilify list of columns on_delete constraint", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("parent", [%{col1: 1, col2: 2}])
|
||||
assert [{id, col1, col2}] = PoolRepo.all from p in "parent", select: {p.id, p.col1, p.col2}
|
||||
|
||||
PoolRepo.insert_all("ref", [[parent_id: id, col1: col1, col2: col2]])
|
||||
PoolRepo.delete_all("parent")
|
||||
assert [{nil, col1, nil}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
|
||||
|
||||
:ok = down(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :on_delete_default_all
|
||||
test "default all on_delete constraint", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, OnDeleteDefaultAllMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("parent", [%{id: 1, col1: 2, col2: 3}])
|
||||
{id, col1, col2} = {Enum.random(10..1000), Enum.random(10..1000), Enum.random(10..1000)}
|
||||
|
||||
PoolRepo.insert_all("parent", [%{id: id, col1: col1, col2: col2}])
|
||||
PoolRepo.insert_all("ref", [%{parent_id: id, col1: col1, col2: col2}])
|
||||
PoolRepo.delete_all(from p in "parent", where: p.id == ^id)
|
||||
assert [{1, 2, 3}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
|
||||
|
||||
:ok = down(PoolRepo, num, OnDeleteDefaultAllMigration, log: false)
|
||||
end
|
||||
|
||||
@tag :on_delete_default_column_list
|
||||
test "default list of columns on_delete constraint", %{migration_number: num} do
|
||||
assert :ok == up(PoolRepo, num, OnDeleteDefaultColumnsMigration, log: false)
|
||||
|
||||
PoolRepo.insert_all("parent", [%{id: 1, col1: 20, col2: 3}])
|
||||
|
||||
{id, col2} = {Enum.random(10..1000), Enum.random(10..1000)}
|
||||
|
||||
PoolRepo.insert_all("parent", [%{id: id, col1: 20, col2: col2}])
|
||||
PoolRepo.insert_all("ref", [%{parent_id: id, col1: 20, col2: col2}])
|
||||
PoolRepo.delete_all(from p in "parent", where: p.id == ^id)
|
||||
assert [{1, 20, 3}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
|
||||
|
||||
:ok = down(PoolRepo, num, OnDeleteDefaultColumnsMigration, log: false)
|
||||
end
|
||||
end
|
||||
264
phoenix/deps/ecto_sql/integration_test/sql/migrator.exs
Normal file
264
phoenix/deps/ecto_sql/integration_test/sql/migrator.exs
Normal file
@@ -0,0 +1,264 @@
|
||||
Code.require_file "../support/file_helpers.exs", __DIR__
|
||||
|
||||
defmodule Ecto.Integration.MigratorTest do
|
||||
use Ecto.Integration.Case
|
||||
|
||||
import Support.FileHelpers
|
||||
import ExUnit.CaptureLog
|
||||
import Ecto.Migrator
|
||||
|
||||
alias Ecto.Integration.{TestRepo, PoolRepo}
|
||||
alias Ecto.Migration.SchemaMigration
|
||||
|
||||
setup config do
|
||||
Process.register(self(), config.test)
|
||||
PoolRepo.delete_all(SchemaMigration)
|
||||
:ok
|
||||
end
|
||||
|
||||
defmodule AnotherSchemaMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
execute TestRepo.create_prefix("bad_schema_migrations"),
|
||||
TestRepo.drop_prefix("bad_schema_migrations")
|
||||
|
||||
create table(:schema_migrations, prefix: "bad_schema_migrations") do
|
||||
add :version, :string
|
||||
add :inserted_at, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BrokenLinkMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
Task.start_link(fn -> raise "oops" end)
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule GoodMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:good_migration)
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:good_migration)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadMigration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
execute "CREATE WHAT"
|
||||
end
|
||||
end
|
||||
|
||||
test "migrations up and down" do
|
||||
assert migrated_versions(PoolRepo) == []
|
||||
assert up(PoolRepo, 31, GoodMigration, log: false) == :ok
|
||||
|
||||
[migration] = PoolRepo.all(SchemaMigration)
|
||||
assert migration.version == 31
|
||||
assert migration.inserted_at
|
||||
|
||||
assert migrated_versions(PoolRepo) == [31]
|
||||
assert up(PoolRepo, 31, GoodMigration, log: false) == :already_up
|
||||
assert migrated_versions(PoolRepo) == [31]
|
||||
assert down(PoolRepo, 32, GoodMigration, log: false) == :already_down
|
||||
assert migrated_versions(PoolRepo) == [31]
|
||||
assert down(PoolRepo, 31, GoodMigration, log: false) == :ok
|
||||
assert migrated_versions(PoolRepo) == []
|
||||
end
|
||||
|
||||
@tag :prefix
|
||||
test "does not commit migration if insert into schema migration fails" do
|
||||
# First we create a new schema migration table in another prefix
|
||||
assert up(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
|
||||
assert migrated_versions(PoolRepo) == [33]
|
||||
|
||||
catch_error(up(PoolRepo, 34, GoodMigration, log: false, prefix: "bad_schema_migrations"))
|
||||
catch_error(PoolRepo.all("good_migration"))
|
||||
catch_error(PoolRepo.all("good_migration", prefix: "bad_schema_migrations"))
|
||||
|
||||
assert down(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
|
||||
end
|
||||
|
||||
test "ecto-generated migration queries pass schema_migration in telemetry options" do
|
||||
handler = fn _event_name, _measurements, metadata ->
|
||||
send(self(), metadata)
|
||||
end
|
||||
|
||||
# migration table creation
|
||||
Process.put(:telemetry, handler)
|
||||
migrated_versions(PoolRepo, log: false)
|
||||
assert_received %{options: [schema_migration: true]}
|
||||
|
||||
# transaction begin statement
|
||||
Process.put(:telemetry, handler)
|
||||
migrated_versions(PoolRepo, skip_table_creation: true, log: false)
|
||||
assert_received %{options: [schema_migration: true]}
|
||||
|
||||
# retrieving the migration versions
|
||||
Process.put(:telemetry, handler)
|
||||
migrated_versions(PoolRepo, migration_lock: false, skip_table_creation: true, log: false)
|
||||
assert_received %{options: [schema_migration: true]}
|
||||
end
|
||||
|
||||
test "bad execute migration" do
|
||||
assert catch_error(up(PoolRepo, 31, BadMigration, log: false))
|
||||
assert DynamicSupervisor.which_children(Ecto.MigratorSupervisor) == []
|
||||
end
|
||||
|
||||
test "broken link migration" do
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:ok, pid} = Task.start_link(fn -> up(PoolRepo, 31, BrokenLinkMigration, log: false) end)
|
||||
assert_receive {:EXIT, ^pid, _}
|
||||
end) =~ "oops"
|
||||
|
||||
assert capture_log(fn ->
|
||||
catch_exit(up(PoolRepo, 31, BrokenLinkMigration, log: false))
|
||||
end) =~ "oops"
|
||||
end
|
||||
|
||||
test "run up to/step migration", config do
|
||||
in_tmp fn path ->
|
||||
create_migration(47, config)
|
||||
create_migration(48, config)
|
||||
|
||||
assert [47] = run(PoolRepo, path, :up, step: 1, log: false)
|
||||
assert count_entries() == 1
|
||||
|
||||
assert [48] = run(PoolRepo, path, :up, to: 48, log: false)
|
||||
end
|
||||
end
|
||||
|
||||
test "run down to/step migration", config do
|
||||
in_tmp fn path ->
|
||||
migrations = [
|
||||
create_migration(49, config),
|
||||
create_migration(50, config),
|
||||
]
|
||||
|
||||
assert [49, 50] = run(PoolRepo, path, :up, all: true, log: false)
|
||||
purge migrations
|
||||
|
||||
assert [50] = run(PoolRepo, path, :down, step: 1, log: false)
|
||||
purge migrations
|
||||
|
||||
assert count_entries() == 1
|
||||
assert [50] = run(PoolRepo, path, :up, to: 50, log: false)
|
||||
end
|
||||
end
|
||||
|
||||
test "runs all migrations", config do
|
||||
in_tmp fn path ->
|
||||
migrations = [
|
||||
create_migration(53, config),
|
||||
create_migration(54, config),
|
||||
]
|
||||
|
||||
assert [53, 54] = run(PoolRepo, path, :up, all: true, log: false)
|
||||
assert [] = run(PoolRepo, path, :up, all: true, log: false)
|
||||
purge migrations
|
||||
|
||||
assert [54, 53] = run(PoolRepo, path, :down, all: true, log: false)
|
||||
purge migrations
|
||||
|
||||
assert count_entries() == 0
|
||||
assert [53, 54] = run(PoolRepo, path, :up, all: true, log: false)
|
||||
end
|
||||
end
|
||||
|
||||
test "does not commit half transactions on bad syntax", config do
|
||||
in_tmp fn path ->
|
||||
migrations = [
|
||||
create_migration(64, config),
|
||||
create_migration("65_+", config)
|
||||
]
|
||||
|
||||
assert_raise SyntaxError, fn ->
|
||||
run(PoolRepo, path, :up, all: true, log: false)
|
||||
end
|
||||
|
||||
refute_received {:up, _}
|
||||
assert count_entries() == 0
|
||||
purge migrations
|
||||
end
|
||||
end
|
||||
|
||||
@tag :lock_for_migrations
|
||||
test "raises when connection pool is too small" do
|
||||
config = Application.fetch_env!(:ecto_sql, PoolRepo)
|
||||
config = Keyword.merge(config, pool_size: 1)
|
||||
Application.put_env(:ecto_sql, __MODULE__.SingleConnectionRepo, config)
|
||||
|
||||
defmodule SingleConnectionRepo do
|
||||
use Ecto.Repo, otp_app: :ecto_sql, adapter: PoolRepo.__adapter__()
|
||||
end
|
||||
|
||||
{:ok, _pid} = SingleConnectionRepo.start_link()
|
||||
|
||||
in_tmp fn path ->
|
||||
exception_message = ~r/Migrations failed to run because the connection pool size is less than 2/
|
||||
|
||||
assert_raise Ecto.MigrationError, exception_message, fn ->
|
||||
run(SingleConnectionRepo, path, :up, all: true, log: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "does not raise when connection pool is too small but there is no lock" do
|
||||
config = Application.fetch_env!(:ecto_sql, PoolRepo)
|
||||
config = Keyword.merge(config, pool_size: 1, migration_lock: nil)
|
||||
Application.put_env(:ecto_sql, __MODULE__.SingleConnectionNoLockRepo, config)
|
||||
|
||||
defmodule SingleConnectionNoLockRepo do
|
||||
use Ecto.Repo, otp_app: :ecto_sql, adapter: PoolRepo.__adapter__()
|
||||
end
|
||||
|
||||
{:ok, _pid} = SingleConnectionNoLockRepo.start_link()
|
||||
|
||||
in_tmp fn path ->
|
||||
run(SingleConnectionNoLockRepo, path, :up, all: true, log: false)
|
||||
end
|
||||
end
|
||||
|
||||
defp count_entries() do
|
||||
PoolRepo.aggregate(SchemaMigration, :count, :version)
|
||||
end
|
||||
|
||||
defp create_migration(num, config) do
|
||||
module = Module.concat(__MODULE__, "Migration#{num}")
|
||||
|
||||
File.write! "#{num}_migration_#{num}.exs", """
|
||||
defmodule #{module} do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
send #{inspect config.test}, {:up, #{inspect num}}
|
||||
end
|
||||
|
||||
def down do
|
||||
send #{inspect config.test}, {:down, #{inspect num}}
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
module
|
||||
end
|
||||
|
||||
defp purge(modules) do
|
||||
Enum.each(List.wrap(modules), fn m ->
|
||||
:code.delete m
|
||||
:code.purge m
|
||||
end)
|
||||
end
|
||||
end
|
||||
15
phoenix/deps/ecto_sql/integration_test/sql/query_many.exs
Normal file
15
phoenix/deps/ecto_sql/integration_test/sql/query_many.exs
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule Ecto.Integration.QueryManyTest do
|
||||
use Ecto.Integration.Case, async: true
|
||||
|
||||
alias Ecto.Integration.TestRepo
|
||||
|
||||
test "query_many!/4" do
|
||||
results = TestRepo.query_many!("SELECT 1; SELECT 2;")
|
||||
assert [%{rows: [[1]], num_rows: 1}, %{rows: [[2]], num_rows: 1}] = results
|
||||
end
|
||||
|
||||
test "query_many!/4 with iodata" do
|
||||
results = TestRepo.query_many!(["SELECT", ?\s, ?1, ";", ?\s, "SELECT", ?\s, ?2, ";"])
|
||||
assert [%{rows: [[1]], num_rows: 1}, %{rows: [[2]], num_rows: 1}] = results
|
||||
end
|
||||
end
|
||||
316
phoenix/deps/ecto_sql/integration_test/sql/sandbox.exs
Normal file
316
phoenix/deps/ecto_sql/integration_test/sql/sandbox.exs
Normal file
@@ -0,0 +1,316 @@
|
||||
defmodule Ecto.Integration.SandboxTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Ecto.Adapters.SQL.Sandbox
|
||||
alias Ecto.Integration.{PoolRepo, TestRepo}
|
||||
alias Ecto.Integration.Post
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
Application.put_env(:ecto_sql, __MODULE__.DynamicRepo, Application.compile_env(:ecto_sql, TestRepo))
|
||||
|
||||
defmodule DynamicRepo do
|
||||
use Ecto.Repo, otp_app: :ecto_sql, adapter: TestRepo.__adapter__()
|
||||
end
|
||||
|
||||
describe "errors" do
|
||||
test "raises if repo doesn't exist" do
|
||||
assert_raise UndefinedFunctionError, ~r"function UnknownRepo.get_dynamic_repo/0 is undefined", fn ->
|
||||
Sandbox.mode(UnknownRepo, :manual)
|
||||
end
|
||||
end
|
||||
|
||||
test "raises if repo is not started" do
|
||||
assert_raise RuntimeError, ~r"could not lookup Ecto repo #{inspect DynamicRepo} because it was not started", fn ->
|
||||
Sandbox.mode(DynamicRepo, :manual)
|
||||
end
|
||||
end
|
||||
|
||||
test "raises if repo is not using sandbox" do
|
||||
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
|
||||
Sandbox.mode(PoolRepo, :manual)
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
|
||||
Sandbox.checkout(PoolRepo)
|
||||
end
|
||||
end
|
||||
|
||||
test "includes link to SQL sandbox on ownership errors" do
|
||||
assert_raise DBConnection.OwnershipError,
|
||||
~r"See Ecto.Adapters.SQL.Sandbox docs for more information.", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "mode" do
|
||||
test "uses the repository when checked out" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
Sandbox.checkout(TestRepo)
|
||||
assert TestRepo.all(Post) == []
|
||||
Sandbox.checkin(TestRepo)
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
end
|
||||
|
||||
test "uses the repository when allowed from another process" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
|
||||
parent = self()
|
||||
|
||||
Task.start_link fn ->
|
||||
Sandbox.checkout(TestRepo)
|
||||
Sandbox.allow(TestRepo, self(), parent)
|
||||
send(parent, :allowed)
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
assert_receive :allowed
|
||||
assert TestRepo.all(Post) == []
|
||||
end
|
||||
|
||||
test "uses the repository when allowed from another process by registered name" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
|
||||
parent = self()
|
||||
Process.register(parent, __MODULE__)
|
||||
|
||||
Task.start_link fn ->
|
||||
Sandbox.checkout(TestRepo)
|
||||
Sandbox.allow(TestRepo, self(), __MODULE__)
|
||||
send(parent, :allowed)
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
assert_receive :allowed
|
||||
assert TestRepo.all(Post) == []
|
||||
|
||||
Process.unregister(__MODULE__)
|
||||
end
|
||||
|
||||
test "uses the repository when shared from another process" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
|
||||
parent = self()
|
||||
|
||||
Task.start_link(fn ->
|
||||
Sandbox.checkout(TestRepo)
|
||||
Sandbox.mode(TestRepo, {:shared, self()})
|
||||
send(parent, :shared)
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
assert_receive :shared
|
||||
assert Task.async(fn -> TestRepo.all(Post) end) |> Task.await == []
|
||||
after
|
||||
Sandbox.mode(TestRepo, :manual)
|
||||
end
|
||||
|
||||
test "works with a dynamic repo" do
|
||||
repo_pid = start_supervised!({DynamicRepo, name: nil})
|
||||
DynamicRepo.put_dynamic_repo(repo_pid)
|
||||
|
||||
assert Sandbox.mode(DynamicRepo, :manual) == :ok
|
||||
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
DynamicRepo.all(Post)
|
||||
end
|
||||
|
||||
Sandbox.checkout(DynamicRepo)
|
||||
assert DynamicRepo.all(Post) == []
|
||||
end
|
||||
|
||||
test "works with a repo pid" do
|
||||
repo_pid = start_supervised!({DynamicRepo, name: nil})
|
||||
DynamicRepo.put_dynamic_repo(repo_pid)
|
||||
|
||||
assert Sandbox.mode(repo_pid, :manual) == :ok
|
||||
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
DynamicRepo.all(Post)
|
||||
end
|
||||
|
||||
Sandbox.checkout(repo_pid)
|
||||
assert DynamicRepo.all(Post) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "savepoints" do
|
||||
test "runs inside a sandbox that is rolled back on checkin" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
assert TestRepo.insert(%Post{})
|
||||
assert TestRepo.all(Post) != []
|
||||
Sandbox.checkin(TestRepo)
|
||||
Sandbox.checkout(TestRepo)
|
||||
assert TestRepo.all(Post) == []
|
||||
Sandbox.checkin(TestRepo)
|
||||
end
|
||||
|
||||
test "runs inside a sandbox that may be disabled" do
|
||||
Sandbox.checkout(TestRepo, sandbox: false)
|
||||
assert TestRepo.insert(%Post{})
|
||||
assert TestRepo.all(Post) != []
|
||||
Sandbox.checkin(TestRepo)
|
||||
|
||||
Sandbox.checkout(TestRepo)
|
||||
assert {1, _} = TestRepo.delete_all(Post)
|
||||
Sandbox.checkin(TestRepo)
|
||||
|
||||
Sandbox.checkout(TestRepo, sandbox: false)
|
||||
assert {1, _} = TestRepo.delete_all(Post)
|
||||
Sandbox.checkin(TestRepo)
|
||||
end
|
||||
|
||||
test "runs inside a sandbox with caller data when preloading associations" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
assert TestRepo.insert(%Post{})
|
||||
parent = self()
|
||||
|
||||
Task.start_link fn ->
|
||||
Sandbox.allow(TestRepo, parent, self())
|
||||
assert [_] = TestRepo.all(Post) |> TestRepo.preload([:author, :comments])
|
||||
send parent, :success
|
||||
end
|
||||
|
||||
assert_receive :success
|
||||
end
|
||||
|
||||
test "runs inside a sidebox with custom ownership timeout" do
|
||||
:ok = Sandbox.checkout(TestRepo, ownership_timeout: 200)
|
||||
parent = self()
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:ok, pid} =
|
||||
Task.start(fn ->
|
||||
Sandbox.allow(TestRepo, parent, self())
|
||||
TestRepo.transaction(fn -> Process.sleep(500) end)
|
||||
end)
|
||||
|
||||
ref = Process.monitor(pid)
|
||||
assert_receive {:DOWN, ^ref, _, ^pid, _}, 1000
|
||||
end) =~ "it owned the connection for longer than 200ms"
|
||||
end
|
||||
|
||||
test "does not taint the sandbox on query errors" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
|
||||
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
|
||||
{:error, _} = TestRepo.query("INVALID")
|
||||
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
|
||||
|
||||
Sandbox.checkin(TestRepo)
|
||||
end
|
||||
end
|
||||
|
||||
describe "transactions" do
|
||||
@tag :transaction_isolation
|
||||
test "with custom isolation level" do
|
||||
Sandbox.checkout(TestRepo, isolation: "READ UNCOMMITTED")
|
||||
|
||||
# Setting it to the same level later on works
|
||||
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
|
||||
|
||||
# Even inside a transaction
|
||||
TestRepo.transaction fn ->
|
||||
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
|
||||
end
|
||||
end
|
||||
|
||||
test "disconnects on transaction timeouts" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
|
||||
assert capture_log(fn ->
|
||||
{:error, :rollback} =
|
||||
TestRepo.transaction(fn -> Process.sleep(1000) end, timeout: 100)
|
||||
end) =~ "timed out"
|
||||
|
||||
Sandbox.checkin(TestRepo)
|
||||
end
|
||||
end
|
||||
|
||||
describe "checkouts" do
|
||||
test "with transaction inside checkout" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
refute TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
|
||||
TestRepo.checkout(fn ->
|
||||
assert TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
TestRepo.transaction(fn ->
|
||||
assert TestRepo.checked_out?()
|
||||
assert TestRepo.in_transaction?()
|
||||
end)
|
||||
assert TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
end)
|
||||
|
||||
refute TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
end
|
||||
|
||||
test "with checkout inside transaction" do
|
||||
Sandbox.checkout(TestRepo)
|
||||
refute TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
|
||||
TestRepo.transaction(fn ->
|
||||
assert TestRepo.checked_out?()
|
||||
assert TestRepo.in_transaction?()
|
||||
TestRepo.checkout(fn ->
|
||||
assert TestRepo.checked_out?()
|
||||
assert TestRepo.in_transaction?()
|
||||
end)
|
||||
assert TestRepo.checked_out?()
|
||||
assert TestRepo.in_transaction?()
|
||||
end)
|
||||
|
||||
refute TestRepo.checked_out?()
|
||||
refute TestRepo.in_transaction?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "start_owner!/2" do
|
||||
test "checks out the connection" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
|
||||
owner = Sandbox.start_owner!(TestRepo)
|
||||
assert TestRepo.all(Post) == []
|
||||
|
||||
:ok = Sandbox.stop_owner(owner)
|
||||
refute Process.alive?(owner)
|
||||
end
|
||||
|
||||
test "can set shared mode" do
|
||||
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||||
TestRepo.all(Post)
|
||||
end
|
||||
|
||||
parent = self()
|
||||
|
||||
Task.start_link(fn ->
|
||||
owner = Sandbox.start_owner!(TestRepo, shared: true)
|
||||
send(parent, {:owner, owner})
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
assert_receive {:owner, owner}
|
||||
assert TestRepo.all(Post) == []
|
||||
:ok = Sandbox.stop_owner(owner)
|
||||
after
|
||||
Sandbox.mode(TestRepo, :manual)
|
||||
end
|
||||
end
|
||||
end
|
||||
178
phoenix/deps/ecto_sql/integration_test/sql/sql.exs
Normal file
178
phoenix/deps/ecto_sql/integration_test/sql/sql.exs
Normal file
@@ -0,0 +1,178 @@
|
||||
defmodule Ecto.Integration.SQLTest do
|
||||
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
|
||||
|
||||
alias Ecto.Integration.PoolRepo
|
||||
alias Ecto.Integration.TestRepo
|
||||
alias Ecto.Integration.Barebone
|
||||
alias Ecto.Integration.Post
|
||||
alias Ecto.Integration.CorruptedPk
|
||||
alias Ecto.Integration.Tag
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
test "fragmented types" do
|
||||
datetime = ~N[2014-01-16 20:26:51]
|
||||
TestRepo.insert!(%Post{inserted_at: datetime})
|
||||
query = from p in Post, where: fragment("? >= ?", p.inserted_at, ^datetime), select: p.inserted_at
|
||||
assert [^datetime] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
test "fragmented schemaless types" do
|
||||
TestRepo.insert!(%Post{visits: 123})
|
||||
assert [123] = TestRepo.all(from p in "posts", select: type(fragment("visits"), :integer))
|
||||
end
|
||||
|
||||
test "type casting negative integers" do
|
||||
TestRepo.insert!(%Post{visits: -42})
|
||||
assert [-42] = TestRepo.all(from(p in Post, select: type(p.visits, :integer)))
|
||||
end
|
||||
|
||||
@tag :array_type
|
||||
test "fragment array types" do
|
||||
text1 = "foo"
|
||||
text2 = "bar"
|
||||
result = TestRepo.query!("SELECT $1::text[]", [[text1, text2]])
|
||||
assert result.rows == [[[text1, text2]]]
|
||||
end
|
||||
|
||||
@tag :array_type
|
||||
test "Converts empty array correctly" do
|
||||
result = TestRepo.query!("SELECT array[1,2,3] = $1", [[]])
|
||||
assert result.rows == [[false]]
|
||||
|
||||
result = TestRepo.query!("SELECT array[]::integer[] = $1", [[]])
|
||||
assert result.rows == [[true]]
|
||||
|
||||
%{id: tag_id} = TestRepo.insert!(%Tag{uuids: []})
|
||||
query = from t in Tag, where: t.uuids == []
|
||||
assert [%{id: ^tag_id}] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
test "query!/4 with dynamic repo" do
|
||||
TestRepo.put_dynamic_repo(:unknown)
|
||||
assert_raise RuntimeError, ~r/:unknown/, fn -> TestRepo.query!("SELECT 1") end
|
||||
end
|
||||
|
||||
test "query!/4" do
|
||||
result = TestRepo.query!("SELECT 1")
|
||||
assert result.rows == [[1]]
|
||||
end
|
||||
|
||||
test "query!/4 with iodata" do
|
||||
result = TestRepo.query!(["SELECT", ?\s, ?1])
|
||||
assert result.rows == [[1]]
|
||||
end
|
||||
|
||||
test "disconnect_all/2" do
|
||||
assert :ok = PoolRepo.disconnect_all(0)
|
||||
end
|
||||
|
||||
test "to_sql/3" do
|
||||
{sql, []} = TestRepo.to_sql(:all, Barebone)
|
||||
assert sql =~ "SELECT"
|
||||
assert sql =~ "barebones"
|
||||
|
||||
{sql, [0]} = TestRepo.to_sql(:update_all, from(b in Barebone, update: [set: [num: ^0]]))
|
||||
assert sql =~ "UPDATE"
|
||||
assert sql =~ "barebones"
|
||||
assert sql =~ "SET"
|
||||
|
||||
{sql, []} = TestRepo.to_sql(:delete_all, Barebone)
|
||||
assert sql =~ "DELETE"
|
||||
assert sql =~ "barebones"
|
||||
end
|
||||
|
||||
test "raises when primary key is not unique on struct operation" do
|
||||
schema = %CorruptedPk{a: "abc"}
|
||||
TestRepo.insert!(schema)
|
||||
TestRepo.insert!(schema)
|
||||
TestRepo.insert!(schema)
|
||||
|
||||
assert_raise Ecto.MultiplePrimaryKeyError,
|
||||
~r|expected delete on corrupted_pk to return at most one entry but got 3 entries|,
|
||||
fn -> TestRepo.delete!(schema) end
|
||||
end
|
||||
|
||||
test "Repo.insert! escape" do
|
||||
TestRepo.insert!(%Post{title: "'"})
|
||||
|
||||
query = from(p in Post, select: p.title)
|
||||
assert ["'"] == TestRepo.all(query)
|
||||
end
|
||||
|
||||
test "Repo.update! escape" do
|
||||
p = TestRepo.insert!(%Post{title: "hello"})
|
||||
TestRepo.update!(Ecto.Changeset.change(p, title: "'"))
|
||||
|
||||
query = from(p in Post, select: p.title)
|
||||
assert ["'"] == TestRepo.all(query)
|
||||
end
|
||||
|
||||
@tag :insert_cell_wise_defaults
|
||||
test "Repo.insert_all escape" do
|
||||
TestRepo.insert_all(Post, [%{title: "'"}])
|
||||
|
||||
query = from(p in Post, select: p.title)
|
||||
assert ["'"] == TestRepo.all(query)
|
||||
end
|
||||
|
||||
test "Repo.update_all escape" do
|
||||
TestRepo.insert!(%Post{title: "hello"})
|
||||
|
||||
TestRepo.update_all(Post, set: [title: "'"])
|
||||
reader = from(p in Post, select: p.title)
|
||||
assert ["'"] == TestRepo.all(reader)
|
||||
|
||||
query = from(Post, where: "'" != "")
|
||||
TestRepo.update_all(query, set: [title: "''"])
|
||||
assert ["''"] == TestRepo.all(reader)
|
||||
end
|
||||
|
||||
test "Repo.delete_all escape" do
|
||||
TestRepo.insert!(%Post{title: "hello"})
|
||||
assert [_] = TestRepo.all(Post)
|
||||
|
||||
TestRepo.delete_all(from(Post, where: "'" == "'"))
|
||||
assert [] == TestRepo.all(Post)
|
||||
end
|
||||
|
||||
test "load" do
|
||||
inserted_at = ~N[2016-01-01 09:00:00]
|
||||
TestRepo.insert!(%Post{title: "title1", inserted_at: inserted_at, public: false})
|
||||
|
||||
result = Ecto.Adapters.SQL.query!(TestRepo, "SELECT * FROM posts", [])
|
||||
posts = Enum.map(result.rows, &TestRepo.load(Post, {result.columns, &1}))
|
||||
assert [%Post{title: "title1", inserted_at: ^inserted_at, public: false}] = posts
|
||||
end
|
||||
|
||||
test "returns true when table exists" do
|
||||
assert Ecto.Adapters.SQL.table_exists?(TestRepo, "posts")
|
||||
end
|
||||
|
||||
test "returns false table doesn't exists" do
|
||||
refute Ecto.Adapters.SQL.table_exists?(TestRepo, "unknown")
|
||||
end
|
||||
|
||||
test "returns result as a formatted table" do
|
||||
TestRepo.insert_all(Post, [%{title: "my post title", counter: 1, public: nil}])
|
||||
|
||||
# resolve correct query for each adapter
|
||||
query = from(p in Post, select: [p.title, p.counter, p.public])
|
||||
{query, _} = Ecto.Adapters.SQL.to_sql(:all, TestRepo, query)
|
||||
|
||||
table =
|
||||
query
|
||||
|> TestRepo.query!()
|
||||
|> Ecto.Adapters.SQL.format_table()
|
||||
|
||||
assert table == "+---------------+---------+--------+\n| title | counter | public |\n+---------------+---------+--------+\n| my post title | 1 | NULL |\n+---------------+---------+--------+"
|
||||
end
|
||||
|
||||
test "format_table edge cases" do
|
||||
assert Ecto.Adapters.SQL.format_table(nil) == ""
|
||||
assert Ecto.Adapters.SQL.format_table(%{columns: nil, rows: nil}) == ""
|
||||
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: []}) == ""
|
||||
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: [["test"]]}) == ""
|
||||
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: []}) == "+------+\n| test |\n+------+\n+------+"
|
||||
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: nil}) == "+------+\n| test |\n+------+\n+------+"
|
||||
end
|
||||
end
|
||||
44
phoenix/deps/ecto_sql/integration_test/sql/stream.exs
Normal file
44
phoenix/deps/ecto_sql/integration_test/sql/stream.exs
Normal file
@@ -0,0 +1,44 @@
|
||||
defmodule Ecto.Integration.StreamTest do
|
||||
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
|
||||
|
||||
alias Ecto.Integration.TestRepo
|
||||
alias Ecto.Integration.Post
|
||||
alias Ecto.Integration.Comment
|
||||
import Ecto.Query
|
||||
|
||||
test "stream empty" do
|
||||
assert {:ok, []} = TestRepo.transaction(fn() ->
|
||||
TestRepo.stream(Post)
|
||||
|> Enum.to_list()
|
||||
end)
|
||||
|
||||
assert {:ok, []} = TestRepo.transaction(fn() ->
|
||||
TestRepo.stream(from p in Post)
|
||||
|> Enum.to_list()
|
||||
end)
|
||||
end
|
||||
|
||||
test "stream without schema" do
|
||||
%Post{} = TestRepo.insert!(%Post{title: "title1"})
|
||||
%Post{} = TestRepo.insert!(%Post{title: "title2"})
|
||||
|
||||
assert {:ok, ["title1", "title2"]} = TestRepo.transaction(fn() ->
|
||||
TestRepo.stream(from(p in "posts", order_by: p.title, select: p.title))
|
||||
|> Enum.to_list()
|
||||
end)
|
||||
end
|
||||
|
||||
test "stream with assoc" do
|
||||
p1 = TestRepo.insert!(%Post{title: "1"})
|
||||
|
||||
%Comment{id: cid1} = TestRepo.insert!(%Comment{text: "1", post_id: p1.id})
|
||||
%Comment{id: cid2} = TestRepo.insert!(%Comment{text: "2", post_id: p1.id})
|
||||
|
||||
stream = TestRepo.stream(Ecto.assoc(p1, :comments))
|
||||
assert {:ok, [c1, c2]} = TestRepo.transaction(fn() ->
|
||||
Enum.to_list(stream)
|
||||
end)
|
||||
assert c1.id == cid1
|
||||
assert c2.id == cid2
|
||||
end
|
||||
end
|
||||
153
phoenix/deps/ecto_sql/integration_test/sql/subquery.exs
Normal file
153
phoenix/deps/ecto_sql/integration_test/sql/subquery.exs
Normal file
@@ -0,0 +1,153 @@
|
||||
defmodule Ecto.Integration.SubQueryTest do
|
||||
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
|
||||
|
||||
alias Ecto.Integration.TestRepo
|
||||
import Ecto.Query
|
||||
alias Ecto.Integration.Post
|
||||
alias Ecto.Integration.Comment
|
||||
|
||||
test "from: subqueries with select source" do
|
||||
TestRepo.insert!(%Post{title: "hello", public: true})
|
||||
|
||||
query = from p in Post, select: p
|
||||
assert ["hello"] =
|
||||
TestRepo.all(from p in subquery(query), select: p.title)
|
||||
assert [post] =
|
||||
TestRepo.all(from p in subquery(query), select: p)
|
||||
|
||||
assert %NaiveDateTime{} = post.inserted_at
|
||||
assert post.__meta__.state == :loaded
|
||||
end
|
||||
|
||||
@tag :map_boolean_in_expression
|
||||
test "from: subqueries with map and select expression" do
|
||||
TestRepo.insert!(%Post{title: "hello", public: true})
|
||||
|
||||
query = from p in Post, select: %{title: p.title, pub: not p.public}
|
||||
assert ["hello"] =
|
||||
TestRepo.all(from p in subquery(query), select: p.title)
|
||||
assert [%{title: "hello", pub: false}] =
|
||||
TestRepo.all(from p in subquery(query), select: p)
|
||||
assert [{"hello", %{title: "hello", pub: false}}] =
|
||||
TestRepo.all(from p in subquery(query), select: {p.title, p})
|
||||
assert [{%{title: "hello", pub: false}, false}] =
|
||||
TestRepo.all(from p in subquery(query), select: {p, p.pub})
|
||||
end
|
||||
|
||||
@tag :map_boolean_in_expression
|
||||
test "from: subqueries with map update and select expression" do
|
||||
TestRepo.insert!(%Post{title: "hello", public: true})
|
||||
|
||||
query = from p in Post, select: %{p | public: not p.public}
|
||||
assert ["hello"] =
|
||||
TestRepo.all(from p in subquery(query), select: p.title)
|
||||
assert [%Post{title: "hello", public: false}] =
|
||||
TestRepo.all(from p in subquery(query), select: p)
|
||||
assert [{"hello", %Post{title: "hello", public: false}}] =
|
||||
TestRepo.all(from p in subquery(query), select: {p.title, p})
|
||||
assert [{%Post{title: "hello", public: false}, false}] =
|
||||
TestRepo.all(from p in subquery(query), select: {p, p.public})
|
||||
end
|
||||
|
||||
test "from: subqueries with map update on virtual field and select expression" do
|
||||
TestRepo.insert!(%Post{title: "hello"})
|
||||
|
||||
query = from p in Post, select: %{p | temp: p.title}
|
||||
assert ["hello"] =
|
||||
TestRepo.all(from p in subquery(query), select: p.temp)
|
||||
assert [%Post{title: "hello", temp: "hello"}] =
|
||||
TestRepo.all(from p in subquery(query), select: p)
|
||||
end
|
||||
|
||||
@tag :subquery_aggregates
|
||||
test "from: subqueries with aggregates" do
|
||||
TestRepo.insert!(%Post{visits: 10})
|
||||
TestRepo.insert!(%Post{visits: 11})
|
||||
TestRepo.insert!(%Post{visits: 13})
|
||||
|
||||
query = from p in Post, select: [:visits], order_by: [asc: :visits]
|
||||
assert [13] = TestRepo.all(from p in subquery(query), select: max(p.visits))
|
||||
query = from p in Post, select: [:visits], order_by: [asc: :visits], limit: 2
|
||||
assert [11] = TestRepo.all(from p in subquery(query), select: max(p.visits))
|
||||
|
||||
query = from p in Post, order_by: [asc: :visits]
|
||||
assert [13] = TestRepo.all(from p in subquery(query), select: max(p.visits))
|
||||
query = from p in Post, order_by: [asc: :visits], limit: 2
|
||||
assert [11] = TestRepo.all(from p in subquery(query), select: max(p.visits))
|
||||
end
|
||||
|
||||
test "from: subqueries with parameters" do
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello"})
|
||||
TestRepo.insert!(%Post{visits: 11, title: "hello"})
|
||||
TestRepo.insert!(%Post{visits: 13, title: "world"})
|
||||
|
||||
query = from p in Post, where: p.visits >= ^11 and p.visits <= ^13
|
||||
query = from p in subquery(query), where: p.title == ^"hello", select: fragment("? + ?", p.visits, ^1)
|
||||
assert [12] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
test "join: subqueries with select source" do
|
||||
%{id: id} = TestRepo.insert!(%Post{title: "hello", public: true})
|
||||
TestRepo.insert!(%Comment{post_id: id})
|
||||
|
||||
query = from p in Post, select: p
|
||||
assert ["hello"] =
|
||||
TestRepo.all(from c in Comment, join: p in subquery(query), on: c.post_id == p.id, select: p.title)
|
||||
assert [%Post{inserted_at: %NaiveDateTime{}}] =
|
||||
TestRepo.all(from c in Comment, join: p in subquery(query), on: c.post_id == p.id, select: p)
|
||||
end
|
||||
|
||||
test "join: subqueries with parameters" do
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello"})
|
||||
TestRepo.insert!(%Post{visits: 11, title: "hello"})
|
||||
TestRepo.insert!(%Post{visits: 13, title: "world"})
|
||||
TestRepo.insert!(%Comment{})
|
||||
TestRepo.insert!(%Comment{})
|
||||
|
||||
query = from p in Post, where: p.visits >= ^11 and p.visits <= ^13
|
||||
query = from c in Comment,
|
||||
join: p in subquery(query),
|
||||
on: true,
|
||||
where: p.title == ^"hello",
|
||||
select: fragment("? + ?", p.visits, ^1)
|
||||
assert [12, 12] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
@tag :subquery_in_order_by
|
||||
test "subqueries in order by" do
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello"})
|
||||
TestRepo.insert!(%Post{visits: 11, title: "hello"})
|
||||
|
||||
query = from p in Post, as: :p, order_by: [asc: exists(from p in Post, where: p.visits > parent_as(:p).visits)]
|
||||
|
||||
assert [%{visits: 11}, %{visits: 10}] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
@tag :multicolumn_distinct
|
||||
@tag :subquery_in_distinct
|
||||
test "subqueries in distinct" do
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello1"})
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello2"})
|
||||
TestRepo.insert!(%Post{visits: 11, title: "hello"})
|
||||
|
||||
query = from p in Post, as: :p, distinct: exists(from p in Post, where: p.visits > parent_as(:p).visits), order_by: [asc: :title]
|
||||
|
||||
assert [%{title: "hello"}, %{title: "hello1"}] = TestRepo.all(query)
|
||||
end
|
||||
|
||||
@tag :subquery_in_group_by
|
||||
test "subqueries in group by" do
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello1"})
|
||||
TestRepo.insert!(%Post{visits: 10, title: "hello2"})
|
||||
TestRepo.insert!(%Post{visits: 11, title: "hello"})
|
||||
|
||||
query = from p in Post, as: :p, select: sum(p.visits), group_by: exists(from p in Post, where: p.visits > parent_as(:p).visits), order_by: [sum(p.visits)]
|
||||
|
||||
query
|
||||
|> TestRepo.all()
|
||||
|> Enum.map(&Decimal.new/1)
|
||||
|> Enum.zip([Decimal.new(11), Decimal.new(20)])
|
||||
|> Enum.all?(fn {a, b} -> Decimal.eq?(a, b) end)
|
||||
|> assert()
|
||||
end
|
||||
end
|
||||
277
phoenix/deps/ecto_sql/integration_test/sql/transaction.exs
Normal file
277
phoenix/deps/ecto_sql/integration_test/sql/transaction.exs
Normal file
@@ -0,0 +1,277 @@
|
||||
defmodule Ecto.Integration.TransactionTest do
|
||||
# We can keep this test async as long as it
|
||||
# is the only one access the transactions table
|
||||
use Ecto.Integration.Case, async: true
|
||||
|
||||
import Ecto.Query
|
||||
alias Ecto.Integration.PoolRepo # Used for writes
|
||||
alias Ecto.Integration.TestRepo # Used for reads
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
defmodule UniqueError do
|
||||
defexception message: "unique error"
|
||||
end
|
||||
|
||||
setup do
|
||||
PoolRepo.delete_all "transactions"
|
||||
:ok
|
||||
end
|
||||
|
||||
defmodule Trans do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "transactions" do
|
||||
field :num, :integer
|
||||
end
|
||||
end
|
||||
|
||||
test "transaction returns value" do
|
||||
refute PoolRepo.in_transaction?()
|
||||
{:ok, val} = PoolRepo.transaction(fn ->
|
||||
assert PoolRepo.in_transaction?()
|
||||
{:ok, val} =
|
||||
PoolRepo.transaction(fn ->
|
||||
assert PoolRepo.in_transaction?()
|
||||
42
|
||||
end)
|
||||
assert PoolRepo.in_transaction?()
|
||||
val
|
||||
end)
|
||||
refute PoolRepo.in_transaction?()
|
||||
assert val == 42
|
||||
end
|
||||
|
||||
test "transaction re-raises" do
|
||||
assert_raise UniqueError, fn ->
|
||||
PoolRepo.transaction(fn ->
|
||||
PoolRepo.transaction(fn ->
|
||||
raise UniqueError
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# tag is required for TestRepo, since it is checkout in
|
||||
# Ecto.Integration.Case setup
|
||||
@tag isolation_level: :snapshot
|
||||
test "transaction commits" do
|
||||
# mssql requires that all transactions that use same shared lock are set
|
||||
# to :snapshot isolation level
|
||||
opts = [isolation_level: :snapshot]
|
||||
|
||||
PoolRepo.transaction(fn ->
|
||||
e = PoolRepo.insert!(%Trans{num: 1})
|
||||
assert [^e] = PoolRepo.all(Trans)
|
||||
assert [] = TestRepo.all(Trans)
|
||||
end, opts)
|
||||
|
||||
assert [%Trans{num: 1}] = PoolRepo.all(Trans)
|
||||
end
|
||||
|
||||
@tag isolation_level: :snapshot
|
||||
test "transaction rolls back" do
|
||||
opts = [isolation_level: :snapshot]
|
||||
try do
|
||||
PoolRepo.transaction(fn ->
|
||||
e = PoolRepo.insert!(%Trans{num: 2})
|
||||
assert [^e] = PoolRepo.all(Trans)
|
||||
assert [] = TestRepo.all(Trans)
|
||||
raise UniqueError
|
||||
end, opts)
|
||||
rescue
|
||||
UniqueError -> :ok
|
||||
end
|
||||
|
||||
assert [] = TestRepo.all(Trans)
|
||||
end
|
||||
|
||||
test "transaction rolls back per repository" do
|
||||
message = "cannot call rollback outside of transaction"
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
PoolRepo.rollback(:done)
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
TestRepo.transaction fn ->
|
||||
PoolRepo.rollback(:done)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@tag :assigns_id_type
|
||||
test "transaction rolls back with reason on aborted transaction" do
|
||||
e1 = PoolRepo.insert!(%Trans{num: 13})
|
||||
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
TestRepo.transaction fn ->
|
||||
PoolRepo.insert!(%Trans{id: e1.id, num: 14})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "nested transaction partial rollback" do
|
||||
assert PoolRepo.transaction(fn ->
|
||||
e1 = PoolRepo.insert!(%Trans{num: 3})
|
||||
assert [^e1] = PoolRepo.all(Trans)
|
||||
|
||||
try do
|
||||
PoolRepo.transaction(fn ->
|
||||
e2 = PoolRepo.insert!(%Trans{num: 4})
|
||||
assert [^e1, ^e2] = PoolRepo.all(from(t in Trans, order_by: t.num))
|
||||
raise UniqueError
|
||||
end)
|
||||
rescue
|
||||
UniqueError -> :ok
|
||||
end
|
||||
|
||||
assert_raise DBConnection.ConnectionError, "transaction rolling back",
|
||||
fn() -> PoolRepo.insert!(%Trans{num: 5}) end
|
||||
end) == {:error, :rollback}
|
||||
|
||||
assert TestRepo.all(Trans) == []
|
||||
end
|
||||
|
||||
test "manual rollback doesn't bubble up" do
|
||||
x = PoolRepo.transaction(fn ->
|
||||
e = PoolRepo.insert!(%Trans{num: 6})
|
||||
assert [^e] = PoolRepo.all(Trans)
|
||||
PoolRepo.rollback(:oops)
|
||||
end)
|
||||
|
||||
assert x == {:error, :oops}
|
||||
assert [] = TestRepo.all(Trans)
|
||||
end
|
||||
|
||||
test "manual rollback bubbles up on nested transaction" do
|
||||
assert PoolRepo.transaction(fn ->
|
||||
e = PoolRepo.insert!(%Trans{num: 7})
|
||||
assert [^e] = PoolRepo.all(Trans)
|
||||
assert {:error, :oops} = PoolRepo.transaction(fn ->
|
||||
PoolRepo.rollback(:oops)
|
||||
end)
|
||||
assert_raise DBConnection.ConnectionError, "transaction rolling back",
|
||||
fn() -> PoolRepo.insert!(%Trans{num: 8}) end
|
||||
end) == {:error, :rollback}
|
||||
|
||||
assert [] = TestRepo.all(Trans)
|
||||
end
|
||||
|
||||
test "transactions are not shared in repo" do
|
||||
pid = self()
|
||||
opts = [isolation_level: :snapshot]
|
||||
|
||||
new_pid = spawn_link fn ->
|
||||
PoolRepo.transaction(fn ->
|
||||
e = PoolRepo.insert!(%Trans{num: 9})
|
||||
assert [^e] = PoolRepo.all(Trans)
|
||||
send(pid, :in_transaction)
|
||||
receive do
|
||||
:commit -> :ok
|
||||
after
|
||||
5000 -> raise "timeout"
|
||||
end
|
||||
end, opts)
|
||||
send(pid, :committed)
|
||||
end
|
||||
|
||||
receive do
|
||||
:in_transaction -> :ok
|
||||
after
|
||||
5000 -> raise "timeout"
|
||||
end
|
||||
|
||||
# mssql requires that all transactions that use same shared lock
|
||||
# set transaction isolation level to "snapshot" so this must be wrapped into
|
||||
# explicit transaction
|
||||
PoolRepo.transaction(fn ->
|
||||
assert [] = PoolRepo.all(Trans)
|
||||
end, opts)
|
||||
|
||||
send(new_pid, :commit)
|
||||
receive do
|
||||
:committed -> :ok
|
||||
after
|
||||
5000 -> raise "timeout"
|
||||
end
|
||||
|
||||
assert [%Trans{num: 9}] = PoolRepo.all(Trans)
|
||||
end
|
||||
|
||||
## Checkout
|
||||
|
||||
describe "with checkouts" do
|
||||
test "transaction inside checkout" do
|
||||
PoolRepo.checkout(fn ->
|
||||
refute PoolRepo.in_transaction?()
|
||||
PoolRepo.transaction(fn ->
|
||||
assert PoolRepo.in_transaction?()
|
||||
end)
|
||||
refute PoolRepo.in_transaction?()
|
||||
end)
|
||||
end
|
||||
|
||||
test "checkout inside transaction" do
|
||||
PoolRepo.transaction(fn ->
|
||||
assert PoolRepo.in_transaction?()
|
||||
PoolRepo.checkout(fn ->
|
||||
assert PoolRepo.in_transaction?()
|
||||
end)
|
||||
assert PoolRepo.in_transaction?()
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :transaction_checkout_raises
|
||||
test "checkout raises on transaction attempt" do
|
||||
assert_raise DBConnection.ConnectionError, ~r"connection was checked out with status", fn ->
|
||||
PoolRepo.checkout(fn -> PoolRepo.query!("BEGIN") end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Logging
|
||||
|
||||
defp register_telemetry() do
|
||||
Process.put(:telemetry, fn _, measurements, event -> send(self(), {measurements, event}) end)
|
||||
end
|
||||
|
||||
test "log begin, commit and rollback" do
|
||||
register_telemetry()
|
||||
|
||||
PoolRepo.transaction(fn ->
|
||||
assert_received {measurements, %{params: [], result: {:ok, _res}}}
|
||||
assert is_integer(measurements.query_time) and measurements.query_time >= 0
|
||||
assert is_integer(measurements.queue_time) and measurements.queue_time >= 0
|
||||
|
||||
refute_received %{}
|
||||
register_telemetry()
|
||||
end)
|
||||
|
||||
assert_received {measurements, %{params: [], result: {:ok, _res}}}
|
||||
assert is_integer(measurements.query_time) and measurements.query_time >= 0
|
||||
refute Map.has_key?(measurements, :queue_time)
|
||||
|
||||
assert PoolRepo.transaction(fn ->
|
||||
refute_received %{}
|
||||
register_telemetry()
|
||||
PoolRepo.rollback(:log_rollback)
|
||||
end) == {:error, :log_rollback}
|
||||
|
||||
assert_received {measurements, %{params: [], result: {:ok, _res}}}
|
||||
assert is_integer(measurements.query_time) and measurements.query_time >= 0
|
||||
refute Map.has_key?(measurements, :queue_time)
|
||||
end
|
||||
|
||||
test "log queries inside transactions" do
|
||||
PoolRepo.transaction(fn ->
|
||||
register_telemetry()
|
||||
assert [] = PoolRepo.all(Trans)
|
||||
|
||||
assert_received {measurements, %{params: [], result: {:ok, _res}}}
|
||||
assert is_integer(measurements.query_time) and measurements.query_time >= 0
|
||||
assert is_integer(measurements.decode_time) and measurements.query_time >= 0
|
||||
refute Map.has_key?(measurements, :queue_time)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Support.FileHelpers do
|
||||
import ExUnit.Assertions
|
||||
|
||||
@doc """
|
||||
Returns the `tmp_path` for tests.
|
||||
"""
|
||||
def tmp_path do
|
||||
Path.expand("../../tmp", __DIR__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes the given function in a temp directory
|
||||
tailored for this test case and test.
|
||||
"""
|
||||
defmacro in_tmp(fun) do
|
||||
{name, _arity} = __CALLER__.function || raise "in_tmp must be called inside a function"
|
||||
path = Path.join([tmp_path(), "#{__CALLER__.module}", "#{name}"])
|
||||
|
||||
quote do
|
||||
path = unquote(path)
|
||||
File.rm_rf!(path)
|
||||
File.mkdir_p!(path)
|
||||
File.cd!(path, fn -> unquote(fun).(path) end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts a file was generated.
|
||||
"""
|
||||
def assert_file(file) do
|
||||
assert File.regular?(file), "Expected #{file} to exist, but does not"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts a file was generated and that it matches a given pattern.
|
||||
"""
|
||||
def assert_file(file, callback) when is_function(callback, 1) do
|
||||
assert_file(file)
|
||||
callback.(File.read!(file))
|
||||
end
|
||||
|
||||
def assert_file(file, match) do
|
||||
assert_file(file, &assert(&1 =~ match))
|
||||
end
|
||||
end
|
||||
166
phoenix/deps/ecto_sql/integration_test/support/migration.exs
Normal file
166
phoenix/deps/ecto_sql/integration_test/support/migration.exs
Normal file
@@ -0,0 +1,166 @@
|
||||
defmodule Ecto.Integration.Migration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# IO.puts "TESTING MIGRATION LOCK"
|
||||
# Process.sleep(10000)
|
||||
|
||||
create table(:users, comment: "users table") do
|
||||
add :name, :string, comment: "name column"
|
||||
add :custom_id, :uuid
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:posts) do
|
||||
add :title, :string, size: 100
|
||||
add :counter, :integer
|
||||
add :blob, :binary
|
||||
add :bid, :binary_id
|
||||
add :uuid, :uuid
|
||||
add :meta, :map
|
||||
add :links, {:map, :string}
|
||||
add :intensities, {:map, :float}
|
||||
add :public, :boolean
|
||||
add :cost, :decimal, precision: 2, scale: 1
|
||||
add :visits, :integer
|
||||
add :wrapped_visits, :integer
|
||||
add :intensity, :float
|
||||
add :author_id, :integer
|
||||
add :posted, :date
|
||||
add :read_only, :string
|
||||
timestamps(null: true)
|
||||
end
|
||||
|
||||
create table(:posts_users, primary_key: false) do
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
end
|
||||
|
||||
create table(:posts_users_pk) do
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
# Add a unique index on uuid. We use this
|
||||
# to verify the behaviour that the index
|
||||
# only matters if the UUID column is not NULL.
|
||||
create unique_index(:posts, [:uuid], comment: "posts index")
|
||||
|
||||
create table(:permalinks) do
|
||||
add :uniform_resource_locator, :string
|
||||
add :title, :string
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
end
|
||||
|
||||
create unique_index(:permalinks, [:post_id])
|
||||
create unique_index(:permalinks, [:uniform_resource_locator])
|
||||
|
||||
create table(:comments) do
|
||||
add :text, :string, size: 100
|
||||
add :lock_version, :integer, default: 1
|
||||
add :post_id, references(:posts)
|
||||
add :author_id, references(:users)
|
||||
end
|
||||
|
||||
create table(:customs, primary_key: false) do
|
||||
add :bid, :binary_id, primary_key: true
|
||||
add :uuid, :uuid
|
||||
end
|
||||
|
||||
create unique_index(:customs, [:uuid])
|
||||
|
||||
create table(:customs_customs, primary_key: false) do
|
||||
add :custom_id1, references(:customs, column: :bid, type: :binary_id)
|
||||
add :custom_id2, references(:customs, column: :bid, type: :binary_id)
|
||||
end
|
||||
|
||||
create table(:barebones) do
|
||||
add :num, :integer
|
||||
end
|
||||
|
||||
create table(:transactions) do
|
||||
add :num, :integer
|
||||
end
|
||||
|
||||
create table(:lock_counters) do
|
||||
add :count, :integer
|
||||
end
|
||||
|
||||
create table(:orders) do
|
||||
add :label, :string
|
||||
add :item, :map
|
||||
add :items, :map
|
||||
add :meta, :map
|
||||
add :permalink_id, references(:permalinks)
|
||||
end
|
||||
|
||||
unless :array_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:tags) do
|
||||
add :ints, {:array, :integer}
|
||||
add :uuids, {:array, :uuid}, default: []
|
||||
add :items, {:array, :map}
|
||||
end
|
||||
|
||||
create table(:array_loggings) do
|
||||
add :uuids, {:array, :uuid}, default: []
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
|
||||
unless :bitstring_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:bitstrings) do
|
||||
add :bs, :bitstring
|
||||
add :bs_with_default, :bitstring, default: <<42::6>>
|
||||
add :bs_with_size, :bitstring, size: 10
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Duration) do
|
||||
unless :duration_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:durations) do
|
||||
add :dur, :duration
|
||||
add :dur_with_fields, :duration, fields: "MONTH"
|
||||
add :dur_with_precision, :duration, precision: 4
|
||||
add :dur_with_fields_and_precision, :duration, fields: "HOUR TO SECOND", precision: 1
|
||||
add :dur_with_default, :duration, default: "10 MONTH"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
create table(:composite_pk, primary_key: false) do
|
||||
add :a, :integer, primary_key: true
|
||||
add :b, :integer, primary_key: true
|
||||
add :name, :string
|
||||
end
|
||||
|
||||
create table(:corrupted_pk, primary_key: false) do
|
||||
add :a, :string
|
||||
end
|
||||
|
||||
create table(:posts_users_composite_pk) do
|
||||
add :post_id, references(:posts), primary_key: true
|
||||
add :user_id, references(:users), primary_key: true
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:posts_users_composite_pk, [:post_id, :user_id])
|
||||
|
||||
create table(:usecs) do
|
||||
add :naive_datetime_usec, :naive_datetime_usec
|
||||
add :utc_datetime_usec, :utc_datetime_usec
|
||||
end
|
||||
|
||||
create table(:bits) do
|
||||
add :bit, :bit
|
||||
end
|
||||
|
||||
create table(:loggings, primary_key: false) do
|
||||
add :bid, :binary_id, primary_key: true
|
||||
add :int, :integer
|
||||
add :uuid, :uuid
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
||||
23
phoenix/deps/ecto_sql/integration_test/support/repo.exs
Normal file
23
phoenix/deps/ecto_sql/integration_test/support/repo.exs
Normal file
@@ -0,0 +1,23 @@
|
||||
defmodule Ecto.Integration.Repo do
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
use Ecto.Repo, unquote(opts)
|
||||
|
||||
@query_event __MODULE__
|
||||
|> Module.split()
|
||||
|> Enum.map(& &1 |> Macro.underscore() |> String.to_atom())
|
||||
|> Kernel.++([:query])
|
||||
|
||||
def init(_, opts) do
|
||||
fun = &Ecto.Integration.Repo.handle_event/4
|
||||
:telemetry.attach_many(__MODULE__, [[:custom], @query_event], fun, :ok)
|
||||
{:ok, opts}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(event, latency, metadata, _config) do
|
||||
handler = Process.delete(:telemetry) || fn _, _, _ -> :ok end
|
||||
handler.(event, latency, metadata)
|
||||
end
|
||||
end
|
||||
72
phoenix/deps/ecto_sql/lib/ecto/adapter/migration.ex
Normal file
72
phoenix/deps/ecto_sql/lib/ecto/adapter/migration.ex
Normal file
@@ -0,0 +1,72 @@
|
||||
defmodule Ecto.Adapter.Migration do
|
||||
@moduledoc """
|
||||
Specifies the adapter migrations API.
|
||||
"""
|
||||
|
||||
alias Ecto.Migration.Constraint
|
||||
alias Ecto.Migration.Table
|
||||
alias Ecto.Migration.Index
|
||||
alias Ecto.Migration.Reference
|
||||
|
||||
@type adapter_meta :: Ecto.Adapter.adapter_meta()
|
||||
|
||||
@type drop_mode :: :restrict | :cascade
|
||||
|
||||
@typedoc "All migration commands"
|
||||
@type command ::
|
||||
raw ::
|
||||
String.t()
|
||||
| {:create, Table.t(), [table_subcommand]}
|
||||
| {:create_if_not_exists, Table.t(), [table_subcommand]}
|
||||
| {:alter, Table.t(), [table_subcommand]}
|
||||
| {:drop, Table.t(), drop_mode()}
|
||||
| {:drop_if_exists, Table.t(), drop_mode()}
|
||||
| {:create, Index.t()}
|
||||
| {:create_if_not_exists, Index.t()}
|
||||
| {:drop, Index.t(), drop_mode()}
|
||||
| {:drop_if_exists, Index.t(), drop_mode()}
|
||||
| {:create, Constraint.t()}
|
||||
| {:drop, Constraint.t(), drop_mode()}
|
||||
| {:drop_if_exists, Constraint.t(), drop_mode()}
|
||||
|
||||
@typedoc "All commands allowed within the block passed to `table/2`"
|
||||
@type table_subcommand ::
|
||||
{:add, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(), Keyword.t()}
|
||||
| {:add_if_not_exists, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:modify, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:remove, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:remove, field :: atom}
|
||||
| {:remove_if_exists, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary()}
|
||||
| {:remove_if_exists, field :: atom}
|
||||
|
||||
@typedoc """
|
||||
A struct that represents a table or index in a database schema.
|
||||
|
||||
These database objects can be modified through the use of a Data
|
||||
Definition Language, hence the name DDL object.
|
||||
"""
|
||||
@type ddl_object :: Table.t() | Index.t()
|
||||
|
||||
@doc """
|
||||
Checks if the adapter supports ddl transaction.
|
||||
"""
|
||||
@callback supports_ddl_transaction? :: boolean
|
||||
|
||||
@doc """
|
||||
Executes migration commands.
|
||||
"""
|
||||
@callback execute_ddl(adapter_meta, command, options :: Keyword.t()) ::
|
||||
{:ok, [{Logger.level(), Logger.message(), Logger.metadata()}]}
|
||||
|
||||
@doc """
|
||||
Locks the migrations table and emits the locked versions for callback execution.
|
||||
|
||||
It returns the result of calling the given function with a list of versions.
|
||||
"""
|
||||
@callback lock_for_migrations(adapter_meta, options :: Keyword.t(), fun) ::
|
||||
result
|
||||
when fun: (-> result), result: var
|
||||
end
|
||||
62
phoenix/deps/ecto_sql/lib/ecto/adapter/structure.ex
Normal file
62
phoenix/deps/ecto_sql/lib/ecto/adapter/structure.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule Ecto.Adapter.Structure do
|
||||
@moduledoc """
|
||||
Specifies the adapter structure (dump/load) API.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Dumps the given structure.
|
||||
|
||||
The path will be looked in the `config` under :dump_path or
|
||||
default to the structure path inside `default`.
|
||||
|
||||
Returns an `:ok` tuple if it was dumped successfully, an error tuple otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
structure_dump("priv/repo", username: "postgres",
|
||||
database: "ecto_test",
|
||||
hostname: "localhost")
|
||||
|
||||
"""
|
||||
@callback structure_dump(default :: String.t(), config :: Keyword.t()) ::
|
||||
{:ok, String.t()} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Loads the given structure.
|
||||
|
||||
The path will be looked in the `config` under :dump_path or
|
||||
default to the structure path inside `default`.
|
||||
|
||||
Returns an `:ok` tuple if it was loaded successfully, an error tuple otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
structure_load("priv/repo", username: "postgres",
|
||||
database: "ecto_test",
|
||||
hostname: "localhost")
|
||||
|
||||
"""
|
||||
@callback structure_load(default :: String.t(), config :: Keyword.t()) ::
|
||||
{:ok, String.t()} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Runs the dump command for the given repo / config.
|
||||
|
||||
Calling this function will setup authentication and run the dump cli
|
||||
command with your provided `args`.
|
||||
|
||||
The options in `opts` are passed directly to `System.cmd/3`.
|
||||
|
||||
Returns `{output, exit_status}` where `output` is a string of the stdout
|
||||
(as long as no option `into` is provided, see `System.cmd/3`) and `exit_status`
|
||||
is the exit status of the invocation. (`0` for success)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dump_cmd(["--data-only", "--table", "table_name"], [stdout_to_stderr: true], Acme.Repo.config())
|
||||
{"--\n-- PostgreSQL database dump\n--\n" <> _rest, 0}
|
||||
|
||||
"""
|
||||
@callback dump_cmd(args :: [String.t()], opts :: Keyword.t(), config :: Keyword.t()) ::
|
||||
{output :: Collectable.t(), exit_status :: non_neg_integer()}
|
||||
end
|
||||
602
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql.ex
Normal file
602
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql.ex
Normal file
@@ -0,0 +1,602 @@
|
||||
defmodule Ecto.Adapters.MyXQL do
|
||||
@moduledoc """
|
||||
Adapter module for MySQL.
|
||||
|
||||
It uses `MyXQL` for communicating to the database.
|
||||
|
||||
## Options
|
||||
|
||||
MySQL options split in different categories described
|
||||
below. All options can be given via the repository
|
||||
configuration:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
...
|
||||
|
||||
The `:prepare` option may be specified per operation:
|
||||
|
||||
YourApp.Repo.all(Queryable, prepare: :unnamed)
|
||||
|
||||
### Connection options
|
||||
|
||||
* `:protocol` - Set to `:socket` for using UNIX domain socket, or `:tcp` for TCP
|
||||
(default: `:socket`)
|
||||
* `:socket` - Connect to MySQL via UNIX sockets in the given path.
|
||||
* `:hostname` - Server hostname
|
||||
* `:port` - Server port (default: 3306)
|
||||
* `:username` - Username
|
||||
* `:password` - User password
|
||||
* `:database` - the database to connect to
|
||||
* `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
|
||||
* `:ssl` - Accepts a list of options to enable TLS for the client connection,
|
||||
or `false` to disable it. See the documentation for [Erlang's `ssl` module](`e:ssl:ssl`)
|
||||
for a list of options (default: false)
|
||||
* `:connect_timeout` - The timeout for establishing new connections (default: 5000)
|
||||
* `:cli_protocol` - The protocol used for the mysql client connection (default: `"tcp"`).
|
||||
This option is only used for `mix ecto.load` and `mix ecto.dump`,
|
||||
via the `mysql` command. For more information, please check
|
||||
[MySQL docs](https://dev.mysql.com/doc/en/connecting.html)
|
||||
* `:socket_options` - Specifies socket configuration
|
||||
* `:show_sensitive_data_on_connection_error` - show connection data and
|
||||
configuration whenever there is an error attempting to connect to the
|
||||
database
|
||||
|
||||
The `:socket_options` are particularly useful when configuring the size
|
||||
of both send and receive buffers. For example, when Ecto starts with a
|
||||
pool of 20 connections, the memory usage may quickly grow from 20MB to
|
||||
50MB based on the operating system default values for TCP buffers. It is
|
||||
advised to stick with the operating system defaults but they can be
|
||||
tweaked if desired:
|
||||
|
||||
socket_options: [recbuf: 8192, sndbuf: 8192]
|
||||
|
||||
We also recommend developers to consult the `MyXQL.start_link/1` documentation
|
||||
for a complete listing of all supported options.
|
||||
|
||||
### Storage options
|
||||
|
||||
* `:charset` - the database encoding (default: "utf8mb4")
|
||||
* `:collation` - the collation order
|
||||
* `:dump_path` - where to place dumped structures
|
||||
* `:dump_prefixes` - list of prefixes that will be included in the
|
||||
structure dump. For MySQL, this list must be of length 1. Multiple
|
||||
prefixes are not supported. When specified, the prefixes will have
|
||||
their definitions dumped along with the data in their migration table.
|
||||
When it is not specified, only the configured database and its migration
|
||||
table are dumped.
|
||||
|
||||
### After connect callback
|
||||
|
||||
If you want to execute a callback as soon as connection is established
|
||||
to the database, you can use the `:after_connect` configuration. For
|
||||
example, in your repository configuration you can add:
|
||||
|
||||
after_connect: {MyXQL, :query!, ["SET variable = value", []]}
|
||||
|
||||
You can also specify your own module that will receive the MyXQL
|
||||
connection as argument.
|
||||
|
||||
## Limitations
|
||||
|
||||
There are some limitations when using Ecto with MySQL that one
|
||||
needs to be aware of.
|
||||
|
||||
### Engine
|
||||
|
||||
Tables created by Ecto are guaranteed to use InnoDB, regardless
|
||||
of the MySQL version.
|
||||
|
||||
### UUIDs
|
||||
|
||||
MySQL does not support UUID types. Ecto emulates them by using
|
||||
`binary(16)`.
|
||||
|
||||
### Read after writes
|
||||
|
||||
Because MySQL does not support RETURNING clauses in INSERT and
|
||||
UPDATE, it does not support the `:read_after_writes` option of
|
||||
`Ecto.Schema.field/3`.
|
||||
|
||||
### DDL Transaction
|
||||
|
||||
MySQL does not support migrations inside transactions as it
|
||||
automatically commits after some commands like CREATE TABLE.
|
||||
Therefore MySQL migrations does not run inside transactions.
|
||||
|
||||
## Old MySQL versions
|
||||
|
||||
### JSON support
|
||||
|
||||
MySQL introduced a native JSON type in v5.7.8, if your server is
|
||||
using this version or higher, you may use `:map` type for your
|
||||
column in migration:
|
||||
|
||||
add :some_field, :map
|
||||
|
||||
If you're using older server versions, use a `TEXT` field instead:
|
||||
|
||||
add :some_field, :text
|
||||
|
||||
in either case, the adapter will automatically encode/decode the
|
||||
value from JSON.
|
||||
|
||||
### usec in datetime
|
||||
|
||||
Old MySQL versions did not support usec in datetime while
|
||||
more recent versions would round or truncate the usec value.
|
||||
|
||||
Therefore, in case the user decides to use microseconds in
|
||||
datetimes and timestamps with MySQL, be aware of such
|
||||
differences and consult the documentation for your MySQL
|
||||
version.
|
||||
|
||||
If your version of MySQL supports microsecond precision, you
|
||||
will be able to utilize Ecto's usec types.
|
||||
|
||||
## Multiple Result Support
|
||||
|
||||
MyXQL supports the execution of queries that return multiple
|
||||
results, such as text queries with multiple statements separated
|
||||
by semicolons or stored procedures. These can be executed with
|
||||
`Ecto.Adapters.SQL.query_many/4` or the `YourRepo.query_many/3`
|
||||
shortcut.
|
||||
|
||||
Be default, these queries will be executed with the `:query_type`
|
||||
option set to `:text`. To take advantage of prepared statements
|
||||
when executing a stored procedure, set the `:query_type` option
|
||||
to `:binary`.
|
||||
"""
|
||||
|
||||
# Inherit all behaviour from Ecto.Adapters.SQL
|
||||
use Ecto.Adapters.SQL, driver: :myxql
|
||||
|
||||
# And provide a custom storage implementation
|
||||
@behaviour Ecto.Adapter.Storage
|
||||
@behaviour Ecto.Adapter.Structure
|
||||
|
||||
@default_prepare_opt :named
|
||||
|
||||
## Custom MySQL types
|
||||
|
||||
@impl true
|
||||
def loaders({:array, _}, type), do: [&json_decode/1, type]
|
||||
def loaders({:map, _}, type), do: [&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
|
||||
def loaders(:map, type), do: [&json_decode/1, type]
|
||||
def loaders(:float, type), do: [&float_decode/1, type]
|
||||
def loaders(:boolean, type), do: [&bool_decode/1, type]
|
||||
def loaders(:binary_id, type), do: [Ecto.UUID, type]
|
||||
def loaders(_, type), do: [type]
|
||||
|
||||
defp bool_decode(<<0>>), do: {:ok, false}
|
||||
defp bool_decode(<<1>>), do: {:ok, true}
|
||||
defp bool_decode(<<0::size(1)>>), do: {:ok, false}
|
||||
defp bool_decode(<<1::size(1)>>), do: {:ok, true}
|
||||
defp bool_decode(0), do: {:ok, false}
|
||||
defp bool_decode(1), do: {:ok, true}
|
||||
defp bool_decode(x), do: {:ok, x}
|
||||
|
||||
defp float_decode(%Decimal{} = decimal), do: {:ok, Decimal.to_float(decimal)}
|
||||
defp float_decode(x), do: {:ok, x}
|
||||
|
||||
defp json_decode(x) when is_binary(x), do: {:ok, MyXQL.json_library().decode!(x)}
|
||||
defp json_decode(x), do: {:ok, x}
|
||||
|
||||
## Query API
|
||||
|
||||
@impl Ecto.Adapter.Queryable
|
||||
def execute(adapter_meta, query_meta, query, params, opts) do
|
||||
prepare = Keyword.get(opts, :prepare, @default_prepare_opt)
|
||||
|
||||
unless valid_prepare?(prepare) do
|
||||
raise ArgumentError,
|
||||
"expected option `:prepare` to be either `:named` or `:unnamed`, got: #{inspect(prepare)}"
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.execute(prepare, adapter_meta, query_meta, query, params, opts)
|
||||
end
|
||||
|
||||
defp valid_prepare?(prepare) when prepare in [:named, :unnamed], do: true
|
||||
defp valid_prepare?(_), do: false
|
||||
|
||||
## Storage API
|
||||
|
||||
@impl true
|
||||
def storage_up(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
charset = opts[:charset] || "utf8mb4"
|
||||
|
||||
check_existence_command =
|
||||
"SELECT TRUE FROM information_schema.schemata WHERE schema_name = '#{database}'"
|
||||
|
||||
case run_query(check_existence_command, opts) do
|
||||
{:ok, %{num_rows: 1}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
_ ->
|
||||
create_command =
|
||||
~s(CREATE DATABASE `#{database}` DEFAULT CHARACTER SET = #{charset})
|
||||
|> concat_if(opts[:collation], &"DEFAULT COLLATE = #{&1}")
|
||||
|
||||
case run_query(create_command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mysql: %{name: :ER_DB_CREATE_EXISTS}}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp concat_if(content, nil, _fun), do: content
|
||||
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
|
||||
|
||||
@impl true
|
||||
def storage_down(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
command = "DROP DATABASE `#{database}`"
|
||||
|
||||
case run_query(command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mysql: %{name: :ER_DB_DROP_EXISTS}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, %{mysql: %{name: :ER_BAD_DB_ERROR}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
|
||||
{:exit, :killed} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ecto.Adapter.Storage
|
||||
def storage_status(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
|
||||
check_database_query =
|
||||
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = '#{database}'"
|
||||
|
||||
case run_query(check_database_query, opts) do
|
||||
{:ok, %{num_rows: 0}} -> :down
|
||||
{:ok, %{num_rows: _num_rows}} -> :up
|
||||
other -> {:error, other}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def supports_ddl_transaction? do
|
||||
false
|
||||
end
|
||||
|
||||
@impl true
|
||||
def lock_for_migrations(meta, opts, fun) do
|
||||
%{opts: adapter_opts, repo: repo} = meta
|
||||
|
||||
if Keyword.fetch(adapter_opts, :pool_size) == {:ok, 1} do
|
||||
Ecto.Adapters.SQL.raise_migration_pool_size_error()
|
||||
end
|
||||
|
||||
opts = Keyword.merge(opts, timeout: :infinity, telemetry_options: [schema_migration: true])
|
||||
|
||||
{:ok, result} =
|
||||
transaction(meta, opts, fn ->
|
||||
lock_name = "\'ecto_#{inspect(repo)}\'"
|
||||
|
||||
try do
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, "SELECT GET_LOCK(#{lock_name}, -1)", [], opts)
|
||||
fun.()
|
||||
after
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, "SELECT RELEASE_LOCK(#{lock_name})", [], opts)
|
||||
end
|
||||
end)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
@impl true
|
||||
def insert(adapter_meta, schema_meta, params, on_conflict, returning, opts) do
|
||||
%{source: source, prefix: prefix} = schema_meta
|
||||
{_, query_params, _} = on_conflict
|
||||
|
||||
key = primary_key!(schema_meta, returning)
|
||||
{fields, values} = :lists.unzip(params)
|
||||
sql = @conn.insert(prefix, source, fields, [fields], on_conflict, [], [])
|
||||
|
||||
opts =
|
||||
if is_nil(Keyword.get(opts, :cache_statement)) do
|
||||
[{:cache_statement, "ecto_insert_#{source}_#{length(fields)}"} | opts]
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do
|
||||
{:ok, %{num_rows: 0}} ->
|
||||
raise "insert operation failed to insert any row in the database. " <>
|
||||
"This may happen if you have trigger or other database conditions rejecting operations. " <>
|
||||
"The emitted SQL was: #{sql}"
|
||||
|
||||
# We were used to check if num_rows was 1 or 2 (in case of upserts)
|
||||
# but MariaDB supports tables with System Versioning, and in those
|
||||
# cases num_rows can be more than 2.
|
||||
{:ok, %{last_insert_id: last_insert_id}} ->
|
||||
{:ok, last_insert_id(key, last_insert_id)}
|
||||
|
||||
{:error, err} ->
|
||||
case @conn.to_constraints(err, source: source) do
|
||||
[] -> raise err
|
||||
constraints -> {:invalid, constraints}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp primary_key!(%{autogenerate_id: {_, key, _type}}, [key]), do: key
|
||||
defp primary_key!(_, []), do: nil
|
||||
|
||||
defp primary_key!(%{schema: schema}, returning) do
|
||||
raise ArgumentError,
|
||||
"MySQL does not support :read_after_writes in schemas for non-primary keys. " <>
|
||||
"The following fields in #{inspect(schema)} are tagged as such: #{inspect(returning)}"
|
||||
end
|
||||
|
||||
defp last_insert_id(nil, _last_insert_id), do: []
|
||||
defp last_insert_id(_key, 0), do: []
|
||||
defp last_insert_id(key, last_insert_id), do: [{key, last_insert_id}]
|
||||
|
||||
@impl true
|
||||
def structure_dump(default, config) do
|
||||
table = config[:migration_source] || "schema_migrations"
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
database = dump_database!(config[:dump_prefixes], config[:database])
|
||||
|
||||
with {:ok, versions} <- select_versions(database, table, config),
|
||||
{:ok, contents} <- mysql_dump(database, config),
|
||||
{:ok, contents} <- append_versions(table, versions, contents) do
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, contents)
|
||||
{:ok, path}
|
||||
end
|
||||
end
|
||||
|
||||
defp dump_database!([prefix], _), do: prefix
|
||||
defp dump_database!(nil, config_database), do: config_database
|
||||
|
||||
defp dump_database!(_, _) do
|
||||
raise ArgumentError,
|
||||
"cannot dump multiple prefixes with MySQL. Please run the command separately for each prefix."
|
||||
end
|
||||
|
||||
defp select_versions(database, table, config) do
|
||||
case run_query(~s[SELECT version FROM `#{database}`.`#{table}` ORDER BY version], config) do
|
||||
{:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &hd/1)}
|
||||
{:error, %{mysql: %{name: :ER_NO_SUCH_TABLE}}} -> {:ok, []}
|
||||
{:error, _} = error -> error
|
||||
{:exit, exit} -> {:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
|
||||
defp mysql_dump(database, config) do
|
||||
args = ["--no-data", "--routines", "--no-create-db", database]
|
||||
|
||||
case run_with_cmd("mysqldump", config, args) do
|
||||
{output, 0} -> {:ok, output}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
defp append_versions(_table, [], contents) do
|
||||
{:ok, contents}
|
||||
end
|
||||
|
||||
defp append_versions(table, versions, contents) do
|
||||
{:ok,
|
||||
contents <>
|
||||
Enum.map_join(versions, &~s[INSERT INTO `#{table}` (version) VALUES (#{&1});\n])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_load(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
|
||||
case File.read(path) do
|
||||
{:ok, contents} ->
|
||||
args = [
|
||||
"--silent",
|
||||
"--batch",
|
||||
"--unbuffered",
|
||||
"--init-command=SET FOREIGN_KEY_CHECKS = 0;",
|
||||
"--database",
|
||||
config[:database]
|
||||
]
|
||||
|
||||
case run_with_port("mysql", config, args, contents) do
|
||||
{_output, 0} -> {:ok, path}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "could not read #{inspect(path)}: #{:file.format_error(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def dump_cmd(args, opts \\ [], config) when is_list(config) and is_list(args) do
|
||||
args =
|
||||
if database = config[:database] do
|
||||
args ++ [database]
|
||||
else
|
||||
args
|
||||
end
|
||||
|
||||
run_with_cmd("mysqldump", config, args, opts)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp run_query(sql, opts) do
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
{:ok, _} = Application.ensure_all_started(:myxql)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.drop([:name, :log, :pool, :pool_size])
|
||||
|> Keyword.put(:backoff_type, :stop)
|
||||
|> Keyword.put(:max_restarts, 0)
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(Ecto.Adapters.SQL.StorageSupervisor, fn ->
|
||||
{:ok, conn} = MyXQL.start_link(opts)
|
||||
|
||||
value = MyXQL.query(conn, sql, [], opts)
|
||||
GenServer.stop(conn)
|
||||
value
|
||||
end)
|
||||
|
||||
timeout = Keyword.get(opts, :timeout, 15_000)
|
||||
|
||||
case Task.yield(task, timeout) || Task.shutdown(task) do
|
||||
{:ok, {:ok, result}} ->
|
||||
{:ok, result}
|
||||
|
||||
{:ok, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:exit, exit}
|
||||
|
||||
nil ->
|
||||
{:error, RuntimeError.exception("command timed out")}
|
||||
end
|
||||
end
|
||||
|
||||
defp exit_to_exception({%{__struct__: struct} = error, _})
|
||||
when struct in [MyXQL.Error, DBConnection.Error],
|
||||
do: error
|
||||
|
||||
defp exit_to_exception(reason), do: RuntimeError.exception(Exception.format_exit(reason))
|
||||
|
||||
defp run_with_cmd(cmd, opts, opt_args, cmd_opts \\ []) do
|
||||
unless System.find_executable(cmd) do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
{args, env} = args_env(opts, opt_args)
|
||||
|
||||
cmd_opts =
|
||||
cmd_opts
|
||||
|> Keyword.put_new(:stderr_to_stdout, true)
|
||||
|> Keyword.update(:env, env, &Enum.concat(env, &1))
|
||||
|
||||
System.cmd(cmd, args, cmd_opts)
|
||||
end
|
||||
|
||||
defp run_with_port(cmd, opts, opt_args, contents) do
|
||||
abs_cmd = System.find_executable(cmd)
|
||||
|
||||
unless abs_cmd do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
abs_cmd = String.to_charlist(abs_cmd)
|
||||
{args, env} = args_env(opts, opt_args)
|
||||
|
||||
port_opts = [
|
||||
:use_stdio,
|
||||
:exit_status,
|
||||
:binary,
|
||||
:hide,
|
||||
:stderr_to_stdout,
|
||||
env: validate_env(env),
|
||||
args: args
|
||||
]
|
||||
|
||||
port = Port.open({:spawn_executable, abs_cmd}, port_opts)
|
||||
Port.command(port, contents)
|
||||
# Use this as a signal to close the port since we cannot
|
||||
# send an exit command to mysql in batch mode
|
||||
Port.command(port, ";SELECT '__ECTO_EOF__';\n")
|
||||
collect_output(port, "")
|
||||
end
|
||||
|
||||
defp args_env(opts, opt_args) do
|
||||
env =
|
||||
if password = opts[:password] do
|
||||
[{"MYSQL_PWD", password}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
host = opts[:hostname] || System.get_env("MYSQL_HOST") || "localhost"
|
||||
port = opts[:port] || System.get_env("MYSQL_TCP_PORT") || "3306"
|
||||
protocol = opts[:cli_protocol] || System.get_env("MYSQL_CLI_PROTOCOL") || "tcp"
|
||||
|
||||
user_args =
|
||||
if username = opts[:username] do
|
||||
["--user", username]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
args =
|
||||
[
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
to_string(port),
|
||||
"--protocol",
|
||||
protocol
|
||||
] ++ user_args ++ opt_args
|
||||
|
||||
{args, env}
|
||||
end
|
||||
|
||||
defp validate_env(enum) do
|
||||
Enum.map(enum, fn
|
||||
{k, nil} ->
|
||||
{String.to_charlist(k), false}
|
||||
|
||||
{k, v} ->
|
||||
{String.to_charlist(k), String.to_charlist(v)}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "invalid environment key-value #{inspect(other)}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp collect_output(port, acc) do
|
||||
receive do
|
||||
{^port, {:data, data}} ->
|
||||
acc = acc <> data
|
||||
|
||||
if acc =~ "__ECTO_EOF__" do
|
||||
Port.close(port)
|
||||
{acc, 0}
|
||||
else
|
||||
collect_output(port, acc)
|
||||
end
|
||||
|
||||
{^port, {:exit_status, status}} ->
|
||||
{acc, status}
|
||||
end
|
||||
end
|
||||
end
|
||||
1630
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql/connection.ex
Normal file
1630
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
548
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres.ex
Normal file
548
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres.ex
Normal file
@@ -0,0 +1,548 @@
|
||||
defmodule Ecto.Adapters.Postgres do
|
||||
@moduledoc """
|
||||
Adapter module for PostgreSQL.
|
||||
|
||||
It uses `Postgrex` for communicating to the database.
|
||||
|
||||
## Features
|
||||
|
||||
* Full query support (including joins, preloads and associations)
|
||||
* Support for transactions
|
||||
* Support for data migrations
|
||||
* Support for ecto.create and ecto.drop operations
|
||||
* Support for transactional tests via `Ecto.Adapters.SQL`
|
||||
|
||||
## Options
|
||||
|
||||
Postgres options split in different categories described
|
||||
below. All options can be given via the repository
|
||||
configuration:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
...
|
||||
|
||||
The `:prepare` option may be specified per operation:
|
||||
|
||||
YourApp.Repo.all(Queryable, prepare: :unnamed)
|
||||
|
||||
### Migration options
|
||||
|
||||
* `:migration_lock` - prevent multiple nodes from running migrations at the same
|
||||
time by obtaining a lock. The value `:table_lock` will lock migrations by wrapping
|
||||
the entire migration inside a database transaction, including inserting the
|
||||
migration version into the migration source (by default, "schema_migrations").
|
||||
You may alternatively select `:pg_advisory_lock` which has the benefit
|
||||
of allowing concurrent operations such as creating indexes. (default: `:table_lock`)
|
||||
|
||||
When using the `:pg_advisory_lock` migration lock strategy and Ecto cannot obtain
|
||||
the lock due to another instance occupying the lock, Ecto will wait for 5 seconds
|
||||
and then retry infinity times. This is configurable on the repo with keys
|
||||
`:migration_advisory_lock_retry_interval_ms` and `:migration_advisory_lock_max_tries`.
|
||||
If the retries are exhausted, the migration will fail.
|
||||
|
||||
Some downsides to using advisory locks is that some Postgres-compatible systems or plugins
|
||||
may not support session level locks well and therefore result in inconsistent behavior.
|
||||
For example, PgBouncer when using pool_modes other than session won't work well with
|
||||
advisory locks. CockroachDB is another system that is designed in a way that advisory
|
||||
locks don't make sense for their distributed database.
|
||||
|
||||
### Connection options
|
||||
|
||||
* `:hostname` - Server hostname
|
||||
* `:socket_dir` - Connect to Postgres via UNIX sockets in the given directory
|
||||
The socket name is derived based on the port. This is the preferred method
|
||||
for configuring sockets and it takes precedence over the hostname. If you are
|
||||
connecting to a socket outside of the Postgres convention, use `:socket` instead;
|
||||
* `:socket` - Connect to Postgres via UNIX sockets in the given path.
|
||||
This option takes precedence over the `:hostname` and `:socket_dir`
|
||||
* `:username` - Username
|
||||
* `:password` - User password
|
||||
* `:port` - Server port (default: 5432)
|
||||
* `:database` - the database to connect to
|
||||
* `:maintenance_database` - Specifies the name of the database to connect to when
|
||||
creating or dropping the database. Defaults to `"postgres"`
|
||||
* `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
|
||||
* `:ssl` - Accepts a list of options to enable TLS for the client connection,
|
||||
or `false` to disable it. See the documentation for [Erlang's `ssl` module](`e:ssl:ssl`)
|
||||
for a list of options (default: false)
|
||||
* `:parameters` - Keyword list of connection parameters
|
||||
* `:connect_timeout` - The timeout for establishing new connections (default: 5000)
|
||||
* `:prepare` - How to prepare queries, either `:named` to use named queries
|
||||
or `:unnamed` to force unnamed queries (default: `:named`)
|
||||
* `:socket_options` - Specifies socket configuration
|
||||
* `:show_sensitive_data_on_connection_error` - show connection data and
|
||||
configuration whenever there is an error attempting to connect to the
|
||||
database
|
||||
|
||||
The `:socket_options` are particularly useful when configuring the size
|
||||
of both send and receive buffers. For example, when Ecto starts with a
|
||||
pool of 20 connections, the memory usage may quickly grow from 20MB to
|
||||
50MB based on the operating system default values for TCP buffers. It is
|
||||
advised to stick with the operating system defaults but they can be
|
||||
tweaked if desired:
|
||||
|
||||
socket_options: [recbuf: 8192, sndbuf: 8192]
|
||||
|
||||
We also recommend developers to consult the `Postgrex.start_link/1`
|
||||
documentation for a complete listing of all supported options.
|
||||
|
||||
### Storage options
|
||||
|
||||
* `:encoding` - the database encoding (default: "UTF8")
|
||||
or `:unspecified` to remove encoding parameter (alternative engine compatibility)
|
||||
* `:template` - the template to create the database from
|
||||
* `:lc_collate` - the collation order
|
||||
* `:lc_ctype` - the character classification
|
||||
* `:dump_path` - where to place dumped structures
|
||||
* `:dump_prefixes` - list of prefixes that will be included in the structure dump.
|
||||
When specified, the prefixes will have their definitions dumped along with the
|
||||
data in their migration table. When it is not specified, the configured
|
||||
database has the definitions dumped from all of its schemas but only
|
||||
the data from the migration table from the `public` schema is included.
|
||||
* `:force_drop` - force the database to be dropped even
|
||||
if it has connections to it (requires PostgreSQL 13+)
|
||||
|
||||
### After connect callback
|
||||
|
||||
If you want to execute a callback as soon as connection is established
|
||||
to the database, you can use the `:after_connect` configuration. For
|
||||
example, in your repository configuration you can add:
|
||||
|
||||
after_connect: {Postgrex, :query!, ["SET search_path TO global_prefix", []]}
|
||||
|
||||
You can also specify your own module that will receive the Postgrex
|
||||
connection as argument.
|
||||
|
||||
## Extensions
|
||||
|
||||
Both PostgreSQL and its adapter for Elixir, Postgrex, support an
|
||||
extension system. If you want to use custom extensions for Postgrex
|
||||
alongside Ecto, you must define a type module with your extensions.
|
||||
Create a new file anywhere in your application with the following:
|
||||
|
||||
Postgrex.Types.define(MyApp.PostgresTypes, [MyExtension.Foo, MyExtensionBar])
|
||||
|
||||
Once your type module is defined, you can configure the repository to use it:
|
||||
|
||||
config :my_app, MyApp.Repo, types: MyApp.PostgresTypes
|
||||
|
||||
## Unix socket connection
|
||||
|
||||
You may desire to communicate with Postgres via Unix sockets.
|
||||
If your PG server is started on the same machine as your code, you could check that:
|
||||
|
||||
```bash
|
||||
% sudo grep unix_socket_directories /var/lib/postgres/data/postgresql.conf
|
||||
unix_socket_directories = '/run/postgresql'
|
||||
```
|
||||
|
||||
```bash
|
||||
% ls -lah /run/postgresql
|
||||
итого 4,0K
|
||||
drwxr-xr-x 2 postgres postgres 80 июн 4 10:58 .
|
||||
drwxr-xr-x 35 root root 840 июн 4 21:02 ..
|
||||
srwxrwxrwx 1 postgres postgres 0 июн 5 07:41 .s.PGSQL.5432
|
||||
-rw------- 1 postgres postgres 61 июн 5 07:41 .s.PGSQL.5432.lock
|
||||
```
|
||||
|
||||
So you have postgresql started and listening on the socket.
|
||||
Then you may use it as follows:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
socket_dir: "/run/postgresql"
|
||||
"""
|
||||
|
||||
# Inherit all behaviour from Ecto.Adapters.SQL
|
||||
use Ecto.Adapters.SQL, driver: :postgrex
|
||||
|
||||
require Logger
|
||||
|
||||
# And provide a custom storage implementation
|
||||
@behaviour Ecto.Adapter.Storage
|
||||
@behaviour Ecto.Adapter.Structure
|
||||
|
||||
@default_maintenance_database "postgres"
|
||||
@default_prepare_opt :named
|
||||
|
||||
@doc """
|
||||
All Ecto extensions for Postgrex.
|
||||
|
||||
Currently Ecto does not define any of its own extensions for Postgrex.
|
||||
If this changes in a future release, you will need to call this function
|
||||
when defining your own custom extensions:
|
||||
|
||||
Postgrex.Types.define(MyApp.PostgresTypes,
|
||||
[MyExtension.Foo, MyExtensionBar] ++ Ecto.Adapters.Postgres.extensions())
|
||||
"""
|
||||
def extensions do
|
||||
[]
|
||||
end
|
||||
|
||||
# Support arrays in place of IN
|
||||
@impl true
|
||||
def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
|
||||
def dumpers({:in, sub}, {:in, sub}), do: [{:array, sub}]
|
||||
def dumpers(:binary_id, type), do: [type, Ecto.UUID]
|
||||
def dumpers(_, type), do: [type]
|
||||
|
||||
## Query API
|
||||
|
||||
@impl Ecto.Adapter.Queryable
|
||||
def execute(adapter_meta, query_meta, query, params, opts) do
|
||||
prepare = Keyword.get(opts, :prepare, @default_prepare_opt)
|
||||
|
||||
unless valid_prepare?(prepare) do
|
||||
raise ArgumentError,
|
||||
"expected option `:prepare` to be either `:named` or `:unnamed`, got: #{inspect(prepare)}"
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.execute(prepare, adapter_meta, query_meta, query, params, opts)
|
||||
end
|
||||
|
||||
defp valid_prepare?(prepare) when prepare in [:named, :unnamed], do: true
|
||||
defp valid_prepare?(_), do: false
|
||||
|
||||
## Storage API
|
||||
|
||||
@impl true
|
||||
def storage_up(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
encoding = if opts[:encoding] == :unspecified, do: nil, else: opts[:encoding] || "UTF8"
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
check_existence_command = "SELECT FROM pg_database WHERE datname = '#{database}'"
|
||||
|
||||
case run_query(check_existence_command, opts) do
|
||||
{:ok, %{num_rows: 1}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
_ ->
|
||||
create_command =
|
||||
~s(CREATE DATABASE "#{database}")
|
||||
|> concat_if(encoding, &"ENCODING '#{&1}'")
|
||||
|> concat_if(opts[:template], &"TEMPLATE=#{&1}")
|
||||
|> concat_if(opts[:lc_ctype], &"LC_CTYPE='#{&1}'")
|
||||
|> concat_if(opts[:lc_collate], &"LC_COLLATE='#{&1}'")
|
||||
|
||||
case run_query(create_command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{postgres: %{code: :duplicate_database}}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp concat_if(content, nil, _), do: content
|
||||
defp concat_if(content, false, _), do: content
|
||||
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
|
||||
|
||||
@impl true
|
||||
def storage_down(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
command =
|
||||
"DROP DATABASE \"#{database}\""
|
||||
|> concat_if(opts[:force_drop], fn _ -> "WITH (FORCE)" end)
|
||||
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
case run_query(command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{postgres: %{code: :invalid_catalog_name}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ecto.Adapter.Storage
|
||||
def storage_status(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
check_database_query =
|
||||
"SELECT datname FROM pg_catalog.pg_database WHERE datname = '#{database}'"
|
||||
|
||||
case run_query(check_database_query, opts) do
|
||||
{:ok, %{num_rows: 0}} -> :down
|
||||
{:ok, %{num_rows: _num_rows}} -> :up
|
||||
other -> {:error, other}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def supports_ddl_transaction? do
|
||||
true
|
||||
end
|
||||
|
||||
@impl true
|
||||
def lock_for_migrations(meta, opts, fun) do
|
||||
%{opts: adapter_opts, repo: repo} = meta
|
||||
|
||||
if Keyword.fetch(adapter_opts, :pool_size) == {:ok, 1} do
|
||||
Ecto.Adapters.SQL.raise_migration_pool_size_error()
|
||||
end
|
||||
|
||||
opts = Keyword.merge(opts, timeout: :infinity, telemetry_options: [schema_migration: true])
|
||||
config = repo.config()
|
||||
lock_strategy = Keyword.get(config, :migration_lock, :table_lock)
|
||||
do_lock_for_migrations(lock_strategy, meta, opts, config, fun)
|
||||
end
|
||||
|
||||
defp do_lock_for_migrations(:pg_advisory_lock, meta, opts, config, fun) do
|
||||
lock = :erlang.phash2({:ecto, opts[:prefix], meta.repo})
|
||||
|
||||
retry_state = %{
|
||||
retry_interval_ms: config[:migration_advisory_lock_retry_interval_ms] || 5000,
|
||||
max_tries: config[:migration_advisory_lock_max_tries] || :infinity,
|
||||
tries: 0
|
||||
}
|
||||
|
||||
advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
|
||||
defp do_lock_for_migrations(:table_lock, meta, opts, _config, fun) do
|
||||
{:ok, res} =
|
||||
transaction(meta, opts, fn ->
|
||||
# SHARE UPDATE EXCLUSIVE MODE is the first lock that locks
|
||||
# itself but still allows updates to happen, see
|
||||
# # https://www.postgresql.org/docs/9.4/explicit-locking.html
|
||||
source = Keyword.get(opts, :migration_source, "schema_migrations")
|
||||
table = if prefix = opts[:prefix], do: ~s|"#{prefix}"."#{source}"|, else: ~s|"#{source}"|
|
||||
lock_statement = "LOCK TABLE #{table} IN SHARE UPDATE EXCLUSIVE MODE"
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, lock_statement, [], opts)
|
||||
|
||||
fun.()
|
||||
end)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
defp advisory_lock(meta, opts, lock, retry_state, fun) do
|
||||
result =
|
||||
checkout(meta, opts, fn ->
|
||||
case Ecto.Adapters.SQL.query(meta, "SELECT pg_try_advisory_lock(#{lock})", [], opts) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
try do
|
||||
{:ok, fun.()}
|
||||
after
|
||||
release_advisory_lock(meta, opts, lock)
|
||||
end
|
||||
|
||||
_ ->
|
||||
:no_advisory_lock
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, fun_result} ->
|
||||
fun_result
|
||||
|
||||
:no_advisory_lock ->
|
||||
maybe_retry_advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp release_advisory_lock(meta, opts, lock) do
|
||||
case Ecto.Adapters.SQL.query(meta, "SELECT pg_advisory_unlock(#{lock})", [], opts) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
raise "failed to release advisory lock"
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_retry_advisory_lock(meta, opts, lock, retry_state, fun) do
|
||||
%{retry_interval_ms: interval, max_tries: max_tries, tries: tries} = retry_state
|
||||
|
||||
if max_tries != :infinity && max_tries <= tries do
|
||||
raise "failed to obtain advisory lock. Tried #{max_tries} times waiting #{interval}ms between tries"
|
||||
else
|
||||
if Keyword.get(opts, :log_migrator_sql, false) do
|
||||
Logger.info(
|
||||
"Migration lock occupied for #{inspect(meta.repo)}. Retry #{tries + 1}/#{max_tries} at #{interval}ms intervals."
|
||||
)
|
||||
end
|
||||
|
||||
Process.sleep(interval)
|
||||
retry_state = %{retry_state | tries: tries + 1}
|
||||
advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_dump(default, config) do
|
||||
table = config[:migration_source] || "schema_migrations"
|
||||
|
||||
with {:ok, versions} <- select_versions(table, config),
|
||||
{:ok, path} <- pg_dump(default, config),
|
||||
do: append_versions(table, versions, path)
|
||||
end
|
||||
|
||||
defp select_versions(table, config) do
|
||||
prefixes = config[:dump_prefixes] || ["public"]
|
||||
|
||||
result =
|
||||
Enum.reduce_while(prefixes, [], fn prefix, versions ->
|
||||
case run_query(~s[SELECT version FROM #{prefix}."#{table}" ORDER BY version], config) do
|
||||
{:ok, %{rows: rows}} -> {:cont, Enum.map(rows, &{prefix, hd(&1)}) ++ versions}
|
||||
{:error, %{postgres: %{code: :undefined_table}}} -> {:cont, versions}
|
||||
{:error, _} = error -> {:halt, error}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:error, _} = error -> error
|
||||
versions -> {:ok, versions}
|
||||
end
|
||||
end
|
||||
|
||||
defp pg_dump(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
prefixes = config[:dump_prefixes] || []
|
||||
non_prefix_args = ["--file", path, "--schema-only", "--no-acl", "--no-owner"]
|
||||
|
||||
args =
|
||||
Enum.reduce(prefixes, non_prefix_args, fn prefix, acc ->
|
||||
["-n", prefix | acc]
|
||||
end)
|
||||
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
|
||||
case run_with_cmd("pg_dump", config, args) do
|
||||
{_output, 0} ->
|
||||
{:ok, path}
|
||||
|
||||
{output, _} ->
|
||||
{:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
defp append_versions(_table, [], path) do
|
||||
{:ok, path}
|
||||
end
|
||||
|
||||
defp append_versions(table, versions, path) do
|
||||
sql =
|
||||
Enum.map_join(versions, fn {prefix, version} ->
|
||||
~s[INSERT INTO #{prefix}."#{table}" (version) VALUES (#{version});\n]
|
||||
end)
|
||||
|
||||
File.open!(path, [:append], fn file ->
|
||||
IO.write(file, sql)
|
||||
end)
|
||||
|
||||
{:ok, path}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_load(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
args = ["--quiet", "--file", path, "-vON_ERROR_STOP=1", "--single-transaction"]
|
||||
|
||||
case run_with_cmd("psql", config, args) do
|
||||
{_output, 0} -> {:ok, path}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def dump_cmd(args, opts \\ [], config) when is_list(config) and is_list(args),
|
||||
do: run_with_cmd("pg_dump", config, args, opts)
|
||||
|
||||
## Helpers
|
||||
|
||||
defp run_query(sql, opts) do
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
{:ok, _} = Application.ensure_all_started(:postgrex)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.drop([:name, :log, :pool, :pool_size])
|
||||
|> Keyword.put(:backoff_type, :stop)
|
||||
|> Keyword.put(:max_restarts, 0)
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(Ecto.Adapters.SQL.StorageSupervisor, fn ->
|
||||
{:ok, conn} = Postgrex.start_link(opts)
|
||||
|
||||
value = Postgrex.query(conn, sql, [], opts)
|
||||
GenServer.stop(conn)
|
||||
value
|
||||
end)
|
||||
|
||||
timeout = Keyword.get(opts, :timeout, 15_000)
|
||||
|
||||
case Task.yield(task, timeout) || Task.shutdown(task) do
|
||||
{:ok, {:ok, result}} ->
|
||||
{:ok, result}
|
||||
|
||||
{:ok, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, {%{__struct__: struct} = error, _}}
|
||||
when struct in [Postgrex.Error, DBConnection.Error] ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, reason} ->
|
||||
{:error, RuntimeError.exception(Exception.format_exit(reason))}
|
||||
|
||||
nil ->
|
||||
{:error, RuntimeError.exception("command timed out")}
|
||||
end
|
||||
end
|
||||
|
||||
defp run_with_cmd(cmd, opts, opt_args, cmd_opts \\ []) do
|
||||
unless System.find_executable(cmd) do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
env = [{"PGCONNECT_TIMEOUT", "10"}]
|
||||
|
||||
env =
|
||||
if password = opts[:password] do
|
||||
[{"PGPASSWORD", password} | env]
|
||||
else
|
||||
env
|
||||
end
|
||||
|
||||
args = []
|
||||
args = if username = opts[:username], do: ["--username", username | args], else: args
|
||||
args = if port = opts[:port], do: ["--port", to_string(port) | args], else: args
|
||||
args = if database = opts[:database], do: ["--dbname", database | args], else: args
|
||||
|
||||
host = opts[:socket_dir] || opts[:hostname] || System.get_env("PGHOST") || "localhost"
|
||||
|
||||
if opts[:socket] do
|
||||
IO.warn(
|
||||
":socket option is ignored when connecting in structure_load/2 and structure_dump/2," <>
|
||||
" use :socket_dir or :hostname instead"
|
||||
)
|
||||
end
|
||||
|
||||
args = ["--host", host | args]
|
||||
args = args ++ opt_args
|
||||
|
||||
cmd_opts =
|
||||
cmd_opts
|
||||
|> Keyword.put_new(:stderr_to_stdout, true)
|
||||
|> Keyword.update(:env, env, &Enum.concat(env, &1))
|
||||
|
||||
System.cmd(cmd, args, cmd_opts)
|
||||
end
|
||||
end
|
||||
2080
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres/connection.ex
Normal file
2080
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
1522
phoenix/deps/ecto_sql/lib/ecto/adapters/sql.ex
Normal file
1522
phoenix/deps/ecto_sql/lib/ecto/adapters/sql.ex
Normal file
File diff suppressed because it is too large
Load Diff
14
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/application.ex
Normal file
14
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/application.ex
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Ecto.Adapters.SQL.Application do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{DynamicSupervisor, strategy: :one_for_one, name: Ecto.MigratorSupervisor},
|
||||
{Task.Supervisor, name: Ecto.Adapters.SQL.StorageSupervisor}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Ecto.Adapters.SQL.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
154
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/connection.ex
Normal file
154
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/connection.ex
Normal file
@@ -0,0 +1,154 @@
|
||||
defmodule Ecto.Adapters.SQL.Connection do
|
||||
@moduledoc """
|
||||
Specifies the behaviour to be implemented by all SQL connections.
|
||||
"""
|
||||
|
||||
@typedoc "The query name"
|
||||
@type name :: String.t()
|
||||
|
||||
@typedoc "The SQL statement"
|
||||
@type statement :: String.t()
|
||||
|
||||
@typedoc "The cached query which is a DBConnection Query"
|
||||
@type cached :: map
|
||||
|
||||
@type connection :: DBConnection.conn()
|
||||
@type params :: [term]
|
||||
|
||||
@doc """
|
||||
Receives options and returns `DBConnection` supervisor child
|
||||
specification.
|
||||
"""
|
||||
@callback child_spec(options :: Keyword.t()) :: :supervisor.child_spec() | {module, Keyword.t()}
|
||||
|
||||
@doc """
|
||||
Prepares and executes the given query with `DBConnection`.
|
||||
"""
|
||||
@callback prepare_execute(connection, name, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, cached, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Executes a cached query.
|
||||
"""
|
||||
@callback execute(connection, cached, params, options :: Keyword.t()) ::
|
||||
{:ok, cached, term} | {:ok, term} | {:error | :reset, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Runs the given statement as a query.
|
||||
"""
|
||||
@callback query(connection, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Runs the given statement as a multi-result query.
|
||||
"""
|
||||
@callback query_many(connection, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Returns a stream that prepares and executes the given query with
|
||||
`DBConnection`.
|
||||
"""
|
||||
@callback stream(connection, statement, params, options :: Keyword.t()) ::
|
||||
Enum.t()
|
||||
|
||||
@doc """
|
||||
Receives the exception returned by `c:query/4`.
|
||||
|
||||
The constraints are in the keyword list and must return the
|
||||
constraint type, like `:unique`, and the constraint name as
|
||||
a string, for example:
|
||||
|
||||
[unique: "posts_title_index"]
|
||||
|
||||
Must return an empty list if the error does not come
|
||||
from any constraint.
|
||||
"""
|
||||
@callback to_constraints(exception :: Exception.t(), options :: Keyword.t()) :: Keyword.t()
|
||||
|
||||
## Queries
|
||||
|
||||
@doc """
|
||||
Receives a query and must return a SELECT query.
|
||||
"""
|
||||
@callback all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Receives a query and values to update and must return an UPDATE query.
|
||||
"""
|
||||
@callback update_all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Receives a query and must return a DELETE query.
|
||||
"""
|
||||
@callback delete_all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns an INSERT for the given `rows` in `table` returning
|
||||
the given `returning`.
|
||||
"""
|
||||
@callback insert(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
header :: [atom],
|
||||
rows :: [[atom | nil]],
|
||||
on_conflict :: Ecto.Adapter.Schema.on_conflict(),
|
||||
returning :: [atom],
|
||||
placeholders :: [term]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns an UPDATE for the given `fields` in `table` filtered by
|
||||
`filters` returning the given `returning`.
|
||||
"""
|
||||
@callback update(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
fields :: [atom],
|
||||
filters :: [atom],
|
||||
returning :: [atom]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns a DELETE for the `filters` returning the given `returning`.
|
||||
"""
|
||||
@callback delete(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
filters :: [atom],
|
||||
returning :: [atom]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Executes an EXPLAIN query or similar depending on the adapter to obtains statistics of the given query.
|
||||
|
||||
Receives the `connection`, `query`, `params` for the query,
|
||||
and all `opts` including those related to the EXPLAIN statement and shared opts.
|
||||
|
||||
Must execute the explain query and return the result.
|
||||
"""
|
||||
@callback explain_query(
|
||||
connection,
|
||||
query :: String.t(),
|
||||
params :: Keyword.t(),
|
||||
opts :: Keyword.t()
|
||||
) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
## DDL
|
||||
|
||||
@doc """
|
||||
Receives a DDL command and returns a query that executes it.
|
||||
"""
|
||||
@callback execute_ddl(command :: Ecto.Adapter.Migration.command()) :: String.t() | [iodata]
|
||||
|
||||
@doc """
|
||||
Receives a query result and returns a list of logs.
|
||||
"""
|
||||
@callback ddl_logs(result :: term) :: [{Logger.level(), Logger.message(), Logger.metadata()}]
|
||||
|
||||
@doc """
|
||||
Returns a queryable to check if the given `table` exists.
|
||||
"""
|
||||
@callback table_exists_query(table :: String.t()) :: {iodata, [term]}
|
||||
end
|
||||
705
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/sandbox.ex
Normal file
705
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/sandbox.ex
Normal file
@@ -0,0 +1,705 @@
|
||||
defmodule Ecto.Adapters.SQL.Sandbox do
|
||||
@moduledoc ~S"""
|
||||
A pool for concurrent transactional tests.
|
||||
|
||||
The sandbox pool is implemented on top of an ownership mechanism.
|
||||
When started, the pool is in automatic mode, which means the
|
||||
repository will automatically check connections out as with any
|
||||
other pool.
|
||||
|
||||
The `mode/2` function can be used to change the pool mode from
|
||||
automatic to either manual or shared. In the latter two modes,
|
||||
the connection must be explicitly checked out before use.
|
||||
When explicit checkouts are made, the sandbox will wrap the
|
||||
connection in a transaction by default and control who has
|
||||
access to it. This means developers have a safe mechanism for
|
||||
running concurrent tests against the database.
|
||||
|
||||
## Database support
|
||||
|
||||
While both PostgreSQL and MySQL support SQL Sandbox, only PostgreSQL
|
||||
supports concurrent tests while running the SQL Sandbox. Therefore, do
|
||||
not run concurrent tests with MySQL as you may run into deadlocks due to
|
||||
its transaction implementation.
|
||||
|
||||
## Example
|
||||
|
||||
The first step is to configure your database to use the
|
||||
`Ecto.Adapters.SQL.Sandbox` pool. You set those options in your
|
||||
`config/config.exs` (or preferably `config/test.exs`) if you
|
||||
haven't yet:
|
||||
|
||||
config :my_app, Repo,
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
Now with the test database properly configured, you can write
|
||||
transactional tests:
|
||||
|
||||
# At the end of your test_helper.exs
|
||||
# Set the pool mode to manual for explicit checkouts
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual)
|
||||
|
||||
defmodule PostTest do
|
||||
# Once the mode is manual, tests can also be async
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo)
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "create post" do
|
||||
# Use the repository as usual
|
||||
assert %Post{} = Repo.insert!(%Post{})
|
||||
end
|
||||
end
|
||||
|
||||
## Collaborating processes
|
||||
|
||||
The example above is straight-forward because we have only
|
||||
a single process using the database connection. However,
|
||||
sometimes a test may need to interact with multiple processes,
|
||||
all using the same connection so they all belong to the same
|
||||
transaction.
|
||||
|
||||
Before we discuss solutions, let's see what happens if we try
|
||||
to use a connection from a new process without explicitly
|
||||
checking it out first:
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
end
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
The test above will fail with an error similar to:
|
||||
|
||||
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.35.0>
|
||||
|
||||
That's because the `setup` block is checking out the connection only
|
||||
for the test process. Once the worker attempts to perform a query,
|
||||
there is no connection assigned to it and it will fail.
|
||||
|
||||
The sandbox module provides two ways of doing so, via allowances or
|
||||
by running in shared mode.
|
||||
|
||||
### Allowances
|
||||
|
||||
The idea behind allowances is that you can explicitly tell a process
|
||||
which checked out connection it should use, allowing multiple processes
|
||||
to collaborate over the same connection. Let's give it a try:
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
allow = Process.whereis(MyApp.Worker)
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), allow)
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
And that's it, by calling `allow/3`, we are explicitly assigning
|
||||
the parent's connection (i.e. the test process' connection) to
|
||||
the task.
|
||||
|
||||
Besides calling `allow/3` allowance can also be provided to processes
|
||||
via [Caller Tracking](`m:Task#module-ancestor-and-caller-tracking`).
|
||||
|
||||
Because allowances use an explicit mechanism, their advantage
|
||||
is that you can still run your tests in async mode. The downside
|
||||
is that you need to explicitly control and allow every single
|
||||
process. This is not always possible. In such cases, you will
|
||||
want to use shared mode.
|
||||
|
||||
### Shared mode
|
||||
|
||||
Shared mode allows a process to share its connection with any other
|
||||
process automatically, without relying on explicit allowances.
|
||||
Let's change the example above to use shared mode:
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
# Setting the shared mode must be done only after checkout
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
By calling `mode({:shared, self()})`, any process that needs
|
||||
to talk to the database will now use the same connection as the
|
||||
one checked out by the test process during the `setup` block.
|
||||
|
||||
Make sure to always check a connection out before setting the mode
|
||||
to `{:shared, self()}`.
|
||||
|
||||
The advantage of shared mode is that by calling a single function,
|
||||
you will ensure all upcoming processes and operations will use that
|
||||
shared connection, without a need to explicitly allow them. The
|
||||
downside is that tests can no longer run concurrently in shared mode.
|
||||
|
||||
Also, beware that if the test process terminates while the worker is
|
||||
using the connection, the connection will be taken away from the worker,
|
||||
which will error. Therefore it is important to guarantee the work is done
|
||||
before the test concludes. In the example above, we are using a `call`,
|
||||
which is synchronous, avoiding the problem, but you may need to explicitly
|
||||
flush the worker or terminate it under such scenarios in your tests.
|
||||
|
||||
### Summing up
|
||||
|
||||
There are two mechanisms for explicit ownerships:
|
||||
|
||||
* Using allowances - requires explicit allowances.
|
||||
Tests may run concurrently.
|
||||
|
||||
* Using shared mode - does not require explicit allowances.
|
||||
Tests cannot run concurrently.
|
||||
|
||||
## FAQ
|
||||
|
||||
When running the sandbox mode concurrently, developers may run into
|
||||
issues we explore in the upcoming sections.
|
||||
|
||||
### "owner exited"
|
||||
|
||||
In some situations, you may see error reports similar to the one below:
|
||||
|
||||
23:59:59.999 [error] Postgrex.Protocol (#PID<>) disconnected:
|
||||
** (DBConnection.Error) owner #PID<> exited
|
||||
Client #PID<> is still using a connection from owner
|
||||
|
||||
Such errors are usually followed by another error report from another
|
||||
process that failed while executing a database query.
|
||||
|
||||
To understand the failure, we need to answer the question: who are the
|
||||
owner and client processes? The owner process is the one that checks
|
||||
out the connection, which, in the majority of cases, is the test process,
|
||||
the one running your tests. In other words, the error happens because
|
||||
the test process has finished, either because the test succeeded or
|
||||
because it failed, while the client process was trying to get information
|
||||
from the database. Since the owner process, the one that owns the
|
||||
connection, no longer exists, Ecto will check the connection back in
|
||||
and notify the client process using the connection that the connection
|
||||
owner is no longer available.
|
||||
|
||||
This can happen in different situations. For example, imagine you query
|
||||
a GenServer in your test that is using a database connection:
|
||||
|
||||
test "gets results from GenServer" do
|
||||
{:ok, pid} = MyAppServer.start_link()
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
|
||||
end
|
||||
|
||||
In the test above, we spawn the server and allow it to perform database
|
||||
queries using the connection owned by the test process. Since we gave
|
||||
a timeout of 1 second, in case the database takes longer than one second
|
||||
to reply, the test process will fail, due to the timeout, making the
|
||||
"owner down" message to be printed because the server process is still
|
||||
waiting on a connection reply.
|
||||
|
||||
In some situations, such failures may be intermittent. Imagine that you
|
||||
allow a process that queries the database every half second:
|
||||
|
||||
test "queries periodically" do
|
||||
{:ok, pid} = PeriodicServer.start_link()
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
# assertions
|
||||
end
|
||||
|
||||
Because the server is querying the database from time to time, there is
|
||||
a chance that, when the test exits, the periodic process may be querying
|
||||
the database, regardless of test success or failure.
|
||||
|
||||
To address this, you can tell ExUnit to manage your processes:
|
||||
|
||||
test "queries periodically" do
|
||||
pid = start_supervised!(PeriodicServer)
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
# assertions
|
||||
end
|
||||
|
||||
By using `start_supervised!/1`, ExUnit guarantees the process finishes
|
||||
before your test (the connection owner).
|
||||
|
||||
In some situations, however, the dynamic processes are directly started
|
||||
inside a `DynamicSupervisor` or a `Task.Supervisor`. You can guarantee
|
||||
proper termination in such scenarios by adding an `on_exit` callback
|
||||
that waits until all supervised children terminate:
|
||||
|
||||
on_exit(fn ->
|
||||
for {_, pid, _, _} <- DynamicSupervisor.which_children(MyApp.DynamicSupervisor) do
|
||||
ref = Process.monitor(pid)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}, :infinity
|
||||
end
|
||||
end)
|
||||
|
||||
### "owner timed out because it owned the connection for longer than Nms"
|
||||
|
||||
In some situations, you may see error reports similar to the one below:
|
||||
|
||||
09:56:43.081 [error] Postgrex.Protocol (#PID<>) disconnected:
|
||||
** (DBConnection.ConnectionError) owner #PID<> timed out
|
||||
because it owned the connection for longer than 120000ms
|
||||
|
||||
If you have a long running test (or you're debugging with IEx.pry),
|
||||
the timeout for the connection ownership may be too short. You can
|
||||
increase the timeout by setting the `:ownership_timeout` options for
|
||||
your repo config in `config/config.exs` (or preferably in `config/test.exs`):
|
||||
|
||||
config :my_app, MyApp.Repo,
|
||||
ownership_timeout: NEW_TIMEOUT_IN_MILLISECONDS
|
||||
|
||||
The `:ownership_timeout` option is part of `DBConnection.Ownership`
|
||||
and defaults to 120000ms. Timeouts are given as integers in milliseconds.
|
||||
|
||||
Alternately, if this is an issue for only a handful of long-running tests,
|
||||
you can pass an `:ownership_timeout` option when calling
|
||||
`Ecto.Adapters.SQL.Sandbox.checkout/2` instead of setting a longer timeout
|
||||
globally in your config.
|
||||
|
||||
### Deferred constraints
|
||||
|
||||
Some databases allow to defer constraint validation to the transaction
|
||||
commit time, instead of the particular statement execution time. This
|
||||
feature, for instance, allows for a cyclic foreign key referencing.
|
||||
Since the SQL Sandbox mode rolls back transactions, tests might report
|
||||
false positives because deferred constraints are never checked by the
|
||||
database. To manually force deferred constraints validation when using
|
||||
PostgreSQL use the following line right at the end of your test case:
|
||||
|
||||
Repo.query!("SET CONSTRAINTS ALL IMMEDIATE")
|
||||
|
||||
### Database locks and deadlocks
|
||||
|
||||
Since the sandbox relies on concurrent transactional tests, there is
|
||||
a chance your tests may trigger deadlocks in your database. This is
|
||||
specially true with MySQL, where the solutions presented here are not
|
||||
enough to avoid deadlocks and therefore making the use of concurrent tests
|
||||
with MySQL prohibited.
|
||||
|
||||
However, even on databases like PostgreSQL, performance degradations or
|
||||
deadlocks may still occur. For example, imagine a "users" table with a
|
||||
unique index on the "email" column. Now consider multiple tests are
|
||||
trying to insert the same user email to the database. They will attempt
|
||||
to retrieve the same database lock, causing only one test to succeed and
|
||||
run while all other tests wait for the lock.
|
||||
|
||||
In other situations, two different tests may proceed in a way that
|
||||
each test retrieves locks desired by the other, leading to a situation
|
||||
that cannot be resolved, a deadlock. For instance:
|
||||
|
||||
```text
|
||||
Transaction 1: Transaction 2:
|
||||
begin
|
||||
begin
|
||||
update posts where id = 1
|
||||
update posts where id = 2
|
||||
update posts where id = 1
|
||||
update posts where id = 2
|
||||
**deadlock**
|
||||
```
|
||||
|
||||
There are different ways to avoid such problems. One of them is
|
||||
to make sure your tests work on distinct data. Regardless of
|
||||
your choice between using fixtures or factories for test data,
|
||||
make sure you get a new set of data per test. This is specially
|
||||
important for data that is meant to be unique like user emails.
|
||||
|
||||
For example, instead of:
|
||||
|
||||
def insert_user do
|
||||
Repo.insert!(%User{email: "sample@example.com"})
|
||||
end
|
||||
|
||||
prefer:
|
||||
|
||||
def insert_user do
|
||||
Repo.insert!(%User{email: "sample-#{counter()}@example.com"})
|
||||
end
|
||||
|
||||
defp counter do
|
||||
System.unique_integer([:positive])
|
||||
end
|
||||
|
||||
In fact, avoiding unique emails like above can also have a positive
|
||||
impact on the test suite performance, as it reduces contention and
|
||||
wait between concurrent tests. We have heard reports where using
|
||||
dynamic values for uniquely indexed columns, as we did for email
|
||||
above, made a test suite run between 2x to 3x faster.
|
||||
|
||||
Deadlocks may happen in other circumstances. If you believe you
|
||||
are hitting a scenario that has not been described here, please
|
||||
report an issue so we can improve our examples. As a last resort,
|
||||
you can always disable the test triggering the deadlock from
|
||||
running asynchronously by setting "async: false".
|
||||
"""
|
||||
|
||||
defmodule Connection do
|
||||
@moduledoc false
|
||||
if Code.ensure_loaded?(DBConnection) do
|
||||
@behaviour DBConnection
|
||||
end
|
||||
|
||||
def connect(_opts) do
|
||||
raise "should never be invoked"
|
||||
end
|
||||
|
||||
def disconnect(err, {conn_mod, state, _in_transaction?}) do
|
||||
conn_mod.disconnect(err, state)
|
||||
end
|
||||
|
||||
def checkout(state), do: proxy(:checkout, state, [])
|
||||
def checkin(state), do: proxy(:checkin, state, [])
|
||||
def ping(state), do: proxy(:ping, state, [])
|
||||
|
||||
def handle_begin(opts, {conn_mod, state, false}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
|
||||
case conn_mod.handle_begin(opts, state) do
|
||||
{:ok, value, state} ->
|
||||
{:ok, value, {conn_mod, state, true}}
|
||||
|
||||
{kind, err, state} ->
|
||||
{kind, err, {conn_mod, state, false}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_commit(opts, {conn_mod, state, true}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
proxy(:handle_commit, {conn_mod, state, false}, [opts])
|
||||
end
|
||||
|
||||
def handle_rollback(opts, {conn_mod, state, _}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
proxy(:handle_rollback, {conn_mod, state, false}, [opts])
|
||||
end
|
||||
|
||||
def handle_status(opts, state),
|
||||
do: proxy(:handle_status, state, [maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_prepare(query, opts, state),
|
||||
do: proxy(:handle_prepare, state, [query, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_execute(query, params, opts, state),
|
||||
do: proxy(:handle_execute, state, [query, params, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_close(query, opts, state),
|
||||
do: proxy(:handle_close, state, [query, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_declare(query, params, opts, state),
|
||||
do: proxy(:handle_declare, state, [query, params, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_fetch(query, cursor, opts, state),
|
||||
do: proxy(:handle_fetch, state, [query, cursor, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_deallocate(query, cursor, opts, state),
|
||||
do: proxy(:handle_deallocate, state, [query, cursor, maybe_savepoint(opts, state)])
|
||||
|
||||
defp maybe_savepoint(opts, {_, _, in_transaction?}) do
|
||||
if not in_transaction? and Keyword.get(opts, :sandbox_subtransaction, true) do
|
||||
[mode: :savepoint] ++ opts
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy(fun, {conn_mod, state, in_transaction?}, args) do
|
||||
result = apply(conn_mod, fun, args ++ [state])
|
||||
pos = :erlang.tuple_size(result)
|
||||
:erlang.setelement(pos, result, {conn_mod, :erlang.element(pos, result), in_transaction?})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a process that will check out and own a connection, then returns that process's pid.
|
||||
|
||||
The process is not linked to the caller, so it is your responsibility to ensure that it will be
|
||||
stopped with `stop_owner/1`. In tests, this is done in an `ExUnit.Callbacks.on_exit/2` callback:
|
||||
|
||||
setup tags do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
## `start_owner!/2` vs `checkout/2`
|
||||
|
||||
`start_owner!/2` should be used in place of `checkout/2`.
|
||||
|
||||
`start_owner!/2` solves the problem of unlinked processes started in a test outliving the test process and causing ownership errors.
|
||||
For example, `LiveView`'s `live(...)` test helper starts a process linked to the LiveView supervisor, not the test process.
|
||||
These errors can be eliminated by having the owner of the connection be a separate process from the test process.
|
||||
|
||||
Outside of that scenario, `checkout/2` involves less overhead than this function and so can be preferable.
|
||||
|
||||
## Options
|
||||
|
||||
* `:shared` - if `true`, the pool runs in the shared mode. Defaults to `false`
|
||||
|
||||
The remaining options are passed to `checkout/2`.
|
||||
"""
|
||||
@doc since: "3.4.4"
|
||||
@spec start_owner!(Ecto.Repo.t() | pid(), keyword()) :: pid()
|
||||
def start_owner!(repo, opts \\ []) do
|
||||
parent = self()
|
||||
|
||||
{:ok, pid} =
|
||||
Agent.start(fn ->
|
||||
{shared, opts} = Keyword.pop(opts, :shared, false)
|
||||
:ok = checkout(repo, opts)
|
||||
|
||||
if shared do
|
||||
:ok = mode(repo, {:shared, self()})
|
||||
else
|
||||
:ok = allow(repo, self(), parent)
|
||||
end
|
||||
end)
|
||||
|
||||
pid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops an owner process started by `start_owner!/2`.
|
||||
"""
|
||||
@doc since: "3.4.4"
|
||||
@spec stop_owner(pid()) :: :ok
|
||||
def stop_owner(pid) do
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the mode for the `repo` pool.
|
||||
|
||||
The modes can be:
|
||||
|
||||
* `:auto` - this is the default mode. When trying to use the repository,
|
||||
processes can automatically checkout a connection without calling
|
||||
`checkout/2` or `start_owner/2` before. This is the mode you will run
|
||||
on before your test suite starts
|
||||
|
||||
* `:manual` - in this mode, the connection always has to be explicitly
|
||||
checked before used. Other processes are allowed to use the same
|
||||
connection if they are explicitly allowed via `allow/4`. You usually
|
||||
set the mode to manual at the end of your `test/test_helper.exs` file.
|
||||
This is also the mode you will run your async tests in
|
||||
|
||||
* `{:shared, pid}` - after checking out a connection in manual mode,
|
||||
you can change the mode to `{:shared, pid}`, where pid is the process
|
||||
that owns the connection, most often `{:shared, self()}`. This makes it
|
||||
so all processes can use the same connection as the one owned by the
|
||||
current process. This is the mode you will run your sync tests in
|
||||
|
||||
Whenever you change the mode to `:manual` or `:auto`, all existing
|
||||
connections are checked in. Therefore, it is recommend to set those
|
||||
modes before your test suite starts, as otherwise you will check in
|
||||
connections being used in any other test running concurrently.
|
||||
|
||||
If successful, returns `:ok` (this is always successful for `:auto`
|
||||
and `:manual` modes). It may return `:not_owner` or `:not_found`
|
||||
when setting `{:shared, pid}` and the given `pid` does not own any
|
||||
connection for the repo. May return `:already_shared` if another
|
||||
process set the ownership mode to `{:shared, _}` and is still alive.
|
||||
"""
|
||||
@spec mode(Ecto.Repo.t() | pid(), :auto | :manual | {:shared, pid()}) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
def mode(repo, mode)
|
||||
when (is_atom(repo) or is_pid(repo)) and mode in [:auto, :manual]
|
||||
when (is_atom(repo) or is_pid(repo)) and elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
|
||||
%{pid: pool, opts: opts} = lookup_meta!(repo)
|
||||
DBConnection.Ownership.ownership_mode(pool, mode, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks a connection out for the given `repo`.
|
||||
|
||||
The process calling `checkout/2` will own the connection
|
||||
until it calls `checkin/2` or until it crashes in which case
|
||||
the connection will be automatically reclaimed by the pool.
|
||||
|
||||
If successful, returns `:ok`. If the caller already has a
|
||||
connection, it returns `{:already, :owner | :allowed}`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:sandbox` - when true the connection is wrapped in
|
||||
a transaction. Defaults to true.
|
||||
|
||||
* `:isolation` - set the query to the given isolation level.
|
||||
|
||||
* `:ownership_timeout` - limits how long the connection can be
|
||||
owned. Defaults to the value in your repo config in
|
||||
`config/config.exs` (or preferably in `config/test.exs`), or
|
||||
120000 ms if not set. The timeout exists for sanity checking
|
||||
purposes, to ensure there is no connection leakage, and can
|
||||
be bumped whenever necessary.
|
||||
|
||||
"""
|
||||
@spec checkout(Ecto.Repo.t() | pid(), keyword()) :: :ok | {:already, :owner | :allowed}
|
||||
def checkout(repo, opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
%{pid: pool, opts: pool_opts} = lookup_meta!(repo)
|
||||
|
||||
pool_opts =
|
||||
if Keyword.get(opts, :sandbox, true) do
|
||||
[
|
||||
post_checkout: &post_checkout(&1, &2, opts),
|
||||
pre_checkin: &pre_checkin(&1, &2, &3, opts)
|
||||
] ++ pool_opts
|
||||
else
|
||||
pool_opts
|
||||
end
|
||||
|
||||
pool_opts_overrides = Keyword.take(opts, [:ownership_timeout, :isolation_level])
|
||||
pool_opts = Keyword.merge(pool_opts, pool_opts_overrides)
|
||||
|
||||
case DBConnection.Ownership.ownership_checkout(pool, pool_opts) do
|
||||
:ok ->
|
||||
if isolation = opts[:isolation] do
|
||||
set_transaction_isolation_level(repo, isolation)
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
defp set_transaction_isolation_level(repo, isolation) do
|
||||
query = "SET TRANSACTION ISOLATION LEVEL #{isolation}"
|
||||
|
||||
case Ecto.Adapters.SQL.query(repo, query, [], sandbox_subtransaction: false) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
checkin(repo, [])
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks in the connection back into the sandbox pool.
|
||||
"""
|
||||
@spec checkin(Ecto.Repo.t() | pid()) :: :ok | :not_owner | :not_found
|
||||
def checkin(repo, _opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
%{pid: pool, opts: opts} = lookup_meta!(repo)
|
||||
DBConnection.Ownership.ownership_checkin(pool, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Allows the `allow` process to use the same connection as `parent`.
|
||||
|
||||
`allow` may be a PID or a locally registered name.
|
||||
|
||||
If the allowance is successful, this function returns `:ok`. If `allow` is already an
|
||||
owner or already allowed, it returns `{:already, :owner | :allowed}`. If `parent` has not
|
||||
checked out a connection from the repo, it returns `:not_found`.
|
||||
"""
|
||||
@spec allow(Ecto.Repo.t() | pid(), pid(), term()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
def allow(repo, parent, allow, opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
case GenServer.whereis(allow) do
|
||||
pid when is_pid(pid) ->
|
||||
%{pid: pool, opts: meta_opts} = lookup_meta!(repo)
|
||||
opts = Keyword.merge(meta_opts, opts)
|
||||
DBConnection.Ownership.ownership_allow(pool, parent, pid, opts)
|
||||
|
||||
other ->
|
||||
raise """
|
||||
only PID or a locally registered process can be allowed to \
|
||||
use the same connection as parent but the lookup returned #{inspect(other)}
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs a function outside of the sandbox.
|
||||
"""
|
||||
@spec unboxed_run(Ecto.Repo.t() | pid(), (-> result)) :: result when result: var
|
||||
def unboxed_run(repo, fun) when is_atom(repo) or is_pid(repo) do
|
||||
checkin(repo)
|
||||
checkout(repo, sandbox: false)
|
||||
|
||||
try do
|
||||
fun.()
|
||||
after
|
||||
checkin(repo)
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_meta!(repo) do
|
||||
%{opts: opts} =
|
||||
meta =
|
||||
repo
|
||||
|> find_repo()
|
||||
|> Ecto.Adapter.lookup_meta()
|
||||
|
||||
if opts[:pool] != DBConnection.Ownership do
|
||||
raise """
|
||||
cannot invoke sandbox operation with pool #{inspect(opts[:pool])}.
|
||||
To use the SQL Sandbox, configure your repository pool as:
|
||||
|
||||
pool: #{inspect(__MODULE__)}
|
||||
"""
|
||||
end
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp find_repo(repo) when is_atom(repo), do: repo.get_dynamic_repo()
|
||||
defp find_repo(repo), do: repo
|
||||
|
||||
defp post_checkout(conn_mod, conn_state, opts) do
|
||||
case conn_mod.handle_begin([mode: :transaction] ++ opts, conn_state) do
|
||||
{:ok, _, conn_state} ->
|
||||
{:ok, Connection, {conn_mod, conn_state, false}}
|
||||
|
||||
{:transaction, _conn_state} ->
|
||||
raise """
|
||||
Ecto SQL sandbox transaction cannot be started because there is already\
|
||||
a transaction running.
|
||||
|
||||
This either means some code is starting a transaction before the sandbox\
|
||||
or a connection was not appropriately rolled back after use.
|
||||
"""
|
||||
|
||||
{_error_or_disconnect, err, conn_state} ->
|
||||
{:disconnect, err, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp pre_checkin(:checkin, Connection, {conn_mod, conn_state, _in_transaction?}, opts) do
|
||||
case conn_mod.handle_rollback([mode: :transaction] ++ opts, conn_state) do
|
||||
{:ok, _, conn_state} ->
|
||||
{:ok, conn_mod, conn_state}
|
||||
|
||||
{:idle, _conn_state} ->
|
||||
raise """
|
||||
Ecto SQL sandbox transaction was already committed/rolled back.
|
||||
|
||||
The sandbox works by running each test in a transaction and closing the\
|
||||
transaction afterwards. However, the transaction has already terminated.\
|
||||
Your test code is likely committing or rolling back transactions manually,\
|
||||
either by invoking procedures or running custom SQL commands.
|
||||
|
||||
One option is to manually checkout a connection without a sandbox:
|
||||
|
||||
Ecto.Adapters.SQL.Sandbox.checkout(repo, sandbox: false)
|
||||
|
||||
But remember you will have to undo any database changes performed by such tests.
|
||||
"""
|
||||
|
||||
{_error_or_disconnect, err, conn_state} ->
|
||||
{:disconnect, err, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp pre_checkin(_, Connection, {conn_mod, conn_state, _in_transaction?}, _opts) do
|
||||
{:ok, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
43
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/stream.ex
Normal file
43
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/stream.ex
Normal file
@@ -0,0 +1,43 @@
|
||||
defmodule Ecto.Adapters.SQL.Stream do
|
||||
@moduledoc false
|
||||
|
||||
defstruct [:meta, :statement, :params, :opts]
|
||||
|
||||
def build(meta, statement, params, opts) do
|
||||
%__MODULE__{meta: meta, statement: statement, params: params, opts: opts}
|
||||
end
|
||||
end
|
||||
|
||||
alias Ecto.Adapters.SQL.Stream
|
||||
|
||||
defimpl Enumerable, for: Stream do
|
||||
def count(_), do: {:error, __MODULE__}
|
||||
|
||||
def member?(_, _), do: {:error, __MODULE__}
|
||||
|
||||
def slice(_), do: {:error, __MODULE__}
|
||||
|
||||
def reduce(stream, acc, fun) do
|
||||
%Stream{meta: meta, statement: statement, params: params, opts: opts} = stream
|
||||
Ecto.Adapters.SQL.reduce(meta, statement, params, opts, acc, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Stream do
|
||||
def into(stream) do
|
||||
%Stream{meta: meta, statement: statement, params: params, opts: opts} = stream
|
||||
{state, fun} = Ecto.Adapters.SQL.into(meta, statement, params, opts)
|
||||
{state, make_into(fun, stream)}
|
||||
end
|
||||
|
||||
defp make_into(fun, stream) do
|
||||
fn
|
||||
state, :done ->
|
||||
fun.(state, :done)
|
||||
stream
|
||||
|
||||
state, acc ->
|
||||
fun.(state, acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
296
phoenix/deps/ecto_sql/lib/ecto/adapters/tds.ex
Normal file
296
phoenix/deps/ecto_sql/lib/ecto/adapters/tds.ex
Normal file
@@ -0,0 +1,296 @@
|
||||
defmodule Ecto.Adapters.Tds do
|
||||
@moduledoc """
|
||||
Adapter module for MSSQL Server using the TDS protocol.
|
||||
|
||||
## Options
|
||||
|
||||
Tds options split in different categories described
|
||||
below. All options can be given via the repository
|
||||
configuration.
|
||||
|
||||
### Connection options
|
||||
|
||||
* `:hostname` - Server hostname
|
||||
* `:port` - Server port (default: 1433)
|
||||
* `:username` - Username
|
||||
* `:password` - User password
|
||||
* `:database` - the database to connect to
|
||||
* `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
|
||||
* `:ssl` - Set to true if ssl should be used (default: false)
|
||||
* `:ssl_opts` - A list of ssl options, see Erlang's `ssl` docs
|
||||
* `:show_sensitive_data_on_connection_error` - show connection data and
|
||||
configuration whenever there is an error attempting to connect to the
|
||||
database
|
||||
|
||||
We also recommend developers to consult the `Tds.start_link/1` documentation
|
||||
for a complete list of all supported options for driver.
|
||||
|
||||
### Storage options
|
||||
|
||||
* `:collation` - the database collation. Used during database creation but
|
||||
it is ignored later
|
||||
|
||||
If you need collation other than Latin1, add `tds_encoding` as dependency to
|
||||
your project `mix.exs` file then amend `config/config.ex` by adding:
|
||||
|
||||
config :tds, :text_encoder, Tds.Encoding
|
||||
|
||||
This should give you extended set of most encoding. For complete list check
|
||||
`Tds.Encoding` [documentation](https://hexdocs.pm/tds_encoding).
|
||||
|
||||
### After connect flags
|
||||
|
||||
After connecting to MSSQL server, TDS will check if there are any flags set in
|
||||
connection options that should affect connection session behaviour. All flags are
|
||||
MSSQL standard *SET* options. The following flags are currently supported:
|
||||
|
||||
* `:set_language` - sets session language (consult stored procedure output
|
||||
`exec sp_helplanguage` for valid values)
|
||||
* `:set_datefirst` - number in range 1..7
|
||||
* `:set_dateformat` - atom, one of `:mdy | :dmy | :ymd | :ydm | :myd | :dym`
|
||||
* `:set_deadlock_priority` - atom, one of `:low | :high | :normal | -10..10`
|
||||
* `:set_lock_timeout` - number in milliseconds > 0
|
||||
* `:set_remote_proc_transactions` - atom, one of `:on | :off`
|
||||
* `:set_implicit_transactions` - atom, one of `:on | :off`
|
||||
* `:set_allow_snapshot_isolation` - atom, one of `:on | :off`
|
||||
(required if `Repo.transaction(fn -> ... end, isolation_level: :snapshot)` is used)
|
||||
* `:set_read_committed_snapshot` - atom, one of `:on | :off`
|
||||
|
||||
## Limitations
|
||||
|
||||
### UUIDs
|
||||
|
||||
MSSQL server has slightly different binary storage format for UUIDs (`uniqueidentifier`).
|
||||
If you use `:binary_id`, the proper choice is made. Otherwise you must use the `Tds.Ecto.UUID`
|
||||
type. Avoid using `Ecto.UUID` since it may cause unpredictable application behaviour.
|
||||
|
||||
### SQL `Char`, `VarChar` and `Text` types
|
||||
|
||||
When working with binaries and strings,there are some limitations you should be aware of:
|
||||
|
||||
- Strings that should be stored in mentioned sql types must be encoded to column
|
||||
codepage (defined in collation). If collation is different than database collation,
|
||||
it is not possible to store correct value into database since the connection
|
||||
respects the database collation. Ecto does not provide way to override parameter
|
||||
codepage.
|
||||
|
||||
- If you need other than Latin1 or other than your database default collation, as
|
||||
mentioned in "Storage Options" section, then manually encode strings using
|
||||
`Tds.Encoding.encode/2` into desired codepage and then tag parameter as `:binary`.
|
||||
Please be aware that queries that use this approach in where clauses can be 10x slower
|
||||
due increased logical reads in database.
|
||||
|
||||
- You can't store VarChar codepoints encoded in one collation/codepage to column that
|
||||
is encoded in different collation/codepage. You will always get wrong result. This is
|
||||
not adapter or driver limitation but rather how string encoding works for single byte
|
||||
encoded strings in MSSQL server. Don't be confused if you are always seeing latin1 chars,
|
||||
they are simply in each codepoint table.
|
||||
|
||||
In particular, if a field has the type `:text`, only raw binaries will be allowed.
|
||||
To avoid above limitations always use `:string` (NVarChar) type for text if possible.
|
||||
If you really need to use VarChar's column type, you can use the `Tds.Ecto.VarChar`
|
||||
Ecto type.
|
||||
|
||||
### JSON support
|
||||
|
||||
Even though the adapter will convert `:map` fields into JSON back and forth,
|
||||
actual value is stored in NVarChar column.
|
||||
|
||||
### Query hints and table hints
|
||||
|
||||
MSSQL supports both query hints and table hints: https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query
|
||||
|
||||
For Ecto compatibility, the query hints must be given via the `lock` option, and they
|
||||
will be translated to MSSQL's "OPTION". If you need to pass multiple options, you
|
||||
can separate them by comma:
|
||||
|
||||
from query, lock: "HASH GROUP, FAST 10"
|
||||
|
||||
Table hints are specified as a list alongside a `from` or `join`:
|
||||
|
||||
from query, hints: ["INDEX (IX_Employee_ManagerID)"]
|
||||
|
||||
The `:migration_lock` will be treated as a table hint and defaults to "UPDLOCK".
|
||||
|
||||
### Multi Repo calls in transactions
|
||||
|
||||
To avoid deadlocks in your app, we exposed `:isolation_level` repo transaction option.
|
||||
This will tell to SQL Server Transaction Manager how to begin transaction.
|
||||
By default, if this option is omitted, isolation level is set to `:read_committed`.
|
||||
|
||||
Any attempt to manually set the transaction isolation via queries, such as
|
||||
|
||||
Ecto.Adapter.SQL.query("SET TRANSACTION ISOLATION LEVEL XYZ")
|
||||
|
||||
will fail once explicit transaction is started using `c:Ecto.Repo.transaction/2`
|
||||
and reset back to :read_committed.
|
||||
|
||||
There is `Ecto.Query.lock/3` function can help by setting it to `WITH(NOLOCK)`.
|
||||
This should allow you to do eventually consistent reads and avoid locks on given
|
||||
table if you don't need to write to database.
|
||||
|
||||
NOTE: after explicit transaction ends (commit or rollback) implicit transactions
|
||||
will run as READ_COMMITTED.
|
||||
"""
|
||||
|
||||
use Ecto.Adapters.SQL,
|
||||
driver: :tds
|
||||
|
||||
require Logger
|
||||
require Ecto.Query
|
||||
|
||||
@behaviour Ecto.Adapter.Storage
|
||||
|
||||
@doc false
|
||||
def autogenerate(:binary_id), do: Tds.Ecto.UUID.bingenerate()
|
||||
def autogenerate(:embed_id), do: Tds.Ecto.UUID.generate()
|
||||
def autogenerate(type), do: super(type)
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def loaders({:map, _}, type), do: [&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
|
||||
def loaders(:map, type), do: [&json_decode/1, type]
|
||||
def loaders(:boolean, type), do: [&bool_decode/1, type]
|
||||
def loaders(:binary_id, type), do: [Tds.Ecto.UUID, type]
|
||||
def loaders(_, type), do: [type]
|
||||
|
||||
@impl true
|
||||
def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
|
||||
def dumpers(:binary_id, type), do: [type, Tds.Ecto.UUID]
|
||||
def dumpers(_, type), do: [type]
|
||||
|
||||
defp bool_decode(<<0>>), do: {:ok, false}
|
||||
defp bool_decode(<<1>>), do: {:ok, true}
|
||||
defp bool_decode(0), do: {:ok, false}
|
||||
defp bool_decode(1), do: {:ok, true}
|
||||
defp bool_decode(x), do: {:ok, x}
|
||||
|
||||
defp json_decode(x) when is_binary(x), do: {:ok, Tds.json_library().decode!(x)}
|
||||
defp json_decode(x), do: {:ok, x}
|
||||
|
||||
# Storage API
|
||||
@doc false
|
||||
@impl true
|
||||
def storage_up(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
command =
|
||||
~s(CREATE DATABASE [#{database}])
|
||||
|> concat_if(opts[:collation], &"COLLATE=#{&1}")
|
||||
|
||||
case run_query(Keyword.put(opts, :database, "master"), command) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mssql: %{number: 1801}}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
|
||||
defp concat_if(content, nil, _fun), do: content
|
||||
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def storage_down(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
case run_query(Keyword.put(opts, :database, "master"), "DROP DATABASE [#{database}]") do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mssql: %{number: 3701}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ecto.Adapter.Storage
|
||||
def storage_status(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.put(opts, :database, "master")
|
||||
|
||||
check_database_query =
|
||||
"SELECT [name] FROM [master].[sys].[databases] WHERE [name] = '#{database}'"
|
||||
|
||||
case run_query(opts, check_database_query) do
|
||||
{:ok, %{num_rows: 0}} -> :down
|
||||
{:ok, %{num_rows: _}} -> :up
|
||||
other -> {:error, other}
|
||||
end
|
||||
end
|
||||
|
||||
defp run_query(opts, sql_command) do
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
{:ok, _} = Application.ensure_all_started(:tds)
|
||||
|
||||
timeout = Keyword.get(opts, :timeout, 15_000)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.drop([:name, :log, :pool, :pool_size])
|
||||
|> Keyword.put(:backoff_type, :stop)
|
||||
|> Keyword.put(:max_restarts, 0)
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(pid, fn ->
|
||||
{:ok, conn} = Tds.start_link(opts)
|
||||
value = Ecto.Adapters.Tds.Connection.execute(conn, sql_command, [], opts)
|
||||
GenServer.stop(conn)
|
||||
value
|
||||
end)
|
||||
|
||||
case Task.yield(task, timeout) || Task.shutdown(task) do
|
||||
{:ok, {:ok, result}} ->
|
||||
{:ok, result}
|
||||
|
||||
{:ok, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, {%{__struct__: struct} = error, _}}
|
||||
when struct in [Tds.Error, DBConnection.Error] ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, reason} ->
|
||||
{:error, RuntimeError.exception(Exception.format_exit(reason))}
|
||||
|
||||
nil ->
|
||||
{:error, RuntimeError.exception("command timed out")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def supports_ddl_transaction? do
|
||||
true
|
||||
end
|
||||
|
||||
@impl true
|
||||
def lock_for_migrations(meta, opts, fun) do
|
||||
%{opts: adapter_opts, repo: repo} = meta
|
||||
|
||||
if Keyword.fetch(adapter_opts, :pool_size) == {:ok, 1} do
|
||||
Ecto.Adapters.SQL.raise_migration_pool_size_error()
|
||||
end
|
||||
|
||||
opts = Keyword.merge(opts, timeout: :infinity, telemetry_options: [schema_migration: true])
|
||||
|
||||
{:ok, result} =
|
||||
transaction(meta, opts, fn ->
|
||||
query =
|
||||
"exec sp_getapplock @Resource = 'ecto_#{inspect(repo)}', @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = -1"
|
||||
|
||||
Ecto.Adapters.SQL.query!(meta, query, [], opts)
|
||||
fun.()
|
||||
end)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
1987
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/connection.ex
Normal file
1987
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
293
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/types.ex
Normal file
293
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/types.ex
Normal file
@@ -0,0 +1,293 @@
|
||||
if Code.ensure_loaded?(Tds) do
|
||||
defmodule Tds.Ecto.UUID do
|
||||
@moduledoc """
|
||||
A TDS adapter type for UUIDs strings.
|
||||
|
||||
If you are using Tds adapter and UUIDs in your project, instead of `Ecto.UUID`
|
||||
you should use Tds.Ecto.UUID to generate correct bytes that should be stored
|
||||
in database.
|
||||
"""
|
||||
|
||||
use Ecto.Type
|
||||
|
||||
@typedoc """
|
||||
A hex-encoded UUID string.
|
||||
"""
|
||||
@type t :: <<_::288>>
|
||||
|
||||
@typedoc """
|
||||
A raw binary representation of a UUID.
|
||||
"""
|
||||
@type raw :: <<_::128>>
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def type(), do: :uuid
|
||||
|
||||
@doc """
|
||||
Casts to UUID.
|
||||
"""
|
||||
@impl true
|
||||
@spec cast(t | raw | any) :: {:ok, t} | :error
|
||||
def cast(
|
||||
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2,
|
||||
d3, d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
|
||||
) do
|
||||
<<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4),
|
||||
?-, c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3),
|
||||
c(e4), c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
casted -> {:ok, casted}
|
||||
end
|
||||
|
||||
def cast(<<bin::binary-size(16)>>), do: encode(bin)
|
||||
def cast(_), do: :error
|
||||
|
||||
@doc """
|
||||
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
|
||||
"""
|
||||
def cast!(value) do
|
||||
case cast(value) do
|
||||
{:ok, uuid} -> uuid
|
||||
:error -> raise Ecto.CastError, type: __MODULE__, value: value
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, c: 1}
|
||||
|
||||
defp c(?0), do: ?0
|
||||
defp c(?1), do: ?1
|
||||
defp c(?2), do: ?2
|
||||
defp c(?3), do: ?3
|
||||
defp c(?4), do: ?4
|
||||
defp c(?5), do: ?5
|
||||
defp c(?6), do: ?6
|
||||
defp c(?7), do: ?7
|
||||
defp c(?8), do: ?8
|
||||
defp c(?9), do: ?9
|
||||
defp c(?A), do: ?a
|
||||
defp c(?B), do: ?b
|
||||
defp c(?C), do: ?c
|
||||
defp c(?D), do: ?d
|
||||
defp c(?E), do: ?e
|
||||
defp c(?F), do: ?f
|
||||
defp c(?a), do: ?a
|
||||
defp c(?b), do: ?b
|
||||
defp c(?c), do: ?c
|
||||
defp c(?d), do: ?d
|
||||
defp c(?e), do: ?e
|
||||
defp c(?f), do: ?f
|
||||
defp c(_), do: throw(:error)
|
||||
|
||||
@doc """
|
||||
Converts a string representing a UUID into a binary.
|
||||
"""
|
||||
@impl true
|
||||
@spec dump(t | any) :: {:ok, raw} | :error
|
||||
def dump(
|
||||
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2,
|
||||
d3, d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
|
||||
) do
|
||||
try do
|
||||
<<d(a7)::4, d(a8)::4, d(a5)::4, d(a6)::4, d(a3)::4, d(a4)::4, d(a1)::4, d(a2)::4,
|
||||
d(b3)::4, d(b4)::4, d(b1)::4, d(b2)::4, d(c3)::4, d(c4)::4, d(c1)::4, d(c2)::4,
|
||||
d(d1)::4, d(d2)::4, d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4,
|
||||
d(e5)::4, d(e6)::4, d(e7)::4, d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
binary ->
|
||||
{:ok, binary}
|
||||
end
|
||||
end
|
||||
|
||||
def dump(_), do: :error
|
||||
|
||||
def dump!(value) do
|
||||
case dump(value) do
|
||||
{:ok, binary} -> binary
|
||||
:error -> raise ArgumentError, "Invalid uuid value #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, d: 1}
|
||||
|
||||
defp d(?0), do: 0
|
||||
defp d(?1), do: 1
|
||||
defp d(?2), do: 2
|
||||
defp d(?3), do: 3
|
||||
defp d(?4), do: 4
|
||||
defp d(?5), do: 5
|
||||
defp d(?6), do: 6
|
||||
defp d(?7), do: 7
|
||||
defp d(?8), do: 8
|
||||
defp d(?9), do: 9
|
||||
defp d(?A), do: 10
|
||||
defp d(?B), do: 11
|
||||
defp d(?C), do: 12
|
||||
defp d(?D), do: 13
|
||||
defp d(?E), do: 14
|
||||
defp d(?F), do: 15
|
||||
defp d(?a), do: 10
|
||||
defp d(?b), do: 11
|
||||
defp d(?c), do: 12
|
||||
defp d(?d), do: 13
|
||||
defp d(?e), do: 14
|
||||
defp d(?f), do: 15
|
||||
defp d(_), do: throw(:error)
|
||||
|
||||
@doc """
|
||||
Converts a binary UUID into a string.
|
||||
"""
|
||||
@impl true
|
||||
@spec load(raw | any) :: {:ok, t} | :error
|
||||
def load(<<_::128>> = uuid) do
|
||||
encode(uuid)
|
||||
end
|
||||
|
||||
def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
|
||||
raise ArgumentError,
|
||||
"trying to load string UUID as Tds.Ecto.UUID: #{inspect(string)}. " <>
|
||||
"Maybe you wanted to declare :uuid as your database field?"
|
||||
end
|
||||
|
||||
def load(_), do: :error
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID.
|
||||
"""
|
||||
@spec generate() :: t
|
||||
def generate do
|
||||
{:ok, uuid} = encode(bingenerate())
|
||||
uuid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID in the binary format.
|
||||
"""
|
||||
@spec bingenerate() :: raw
|
||||
def bingenerate do
|
||||
<<u0::56, u1::36, u2::28>> = :crypto.strong_rand_bytes(15)
|
||||
<<u0::56, 4::4, u1::36, 2::4, u2::28>>
|
||||
end
|
||||
|
||||
# Callback invoked by autogenerate fields.
|
||||
@impl true
|
||||
def autogenerate, do: generate()
|
||||
|
||||
defp encode(
|
||||
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
|
||||
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
|
||||
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
|
||||
) do
|
||||
<<e(a7), e(a8), e(a5), e(a6), e(a3), e(a4), e(a1), e(a2), ?-, e(b3), e(b4), e(b1), e(b2),
|
||||
?-, e(c3), e(c4), e(c1), e(c2), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3),
|
||||
e(e4), e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
encoded -> {:ok, encoded}
|
||||
end
|
||||
|
||||
@compile {:inline, e: 1}
|
||||
|
||||
defp e(0), do: ?0
|
||||
defp e(1), do: ?1
|
||||
defp e(2), do: ?2
|
||||
defp e(3), do: ?3
|
||||
defp e(4), do: ?4
|
||||
defp e(5), do: ?5
|
||||
defp e(6), do: ?6
|
||||
defp e(7), do: ?7
|
||||
defp e(8), do: ?8
|
||||
defp e(9), do: ?9
|
||||
defp e(10), do: ?a
|
||||
defp e(11), do: ?b
|
||||
defp e(12), do: ?c
|
||||
defp e(13), do: ?d
|
||||
defp e(14), do: ?e
|
||||
defp e(15), do: ?f
|
||||
end
|
||||
|
||||
defmodule Tds.Ecto.VarChar do
|
||||
@moduledoc """
|
||||
A Tds adapter Ecto Type that wraps erlang string into tuple so TDS driver
|
||||
can understand if erlang string should be encoded as NVarChar or Varchar.
|
||||
|
||||
Due to some limitations in Ecto and Tds driver, it is not possible to
|
||||
support collations other than the one set on connection during login.
|
||||
Please be aware of this limitation if you plan to store varchar values in
|
||||
your database using Ecto since you will probably lose some codepoints in
|
||||
the value during encoding. Instead use `tds_encoding` library and first
|
||||
encode value and then annotate it as `:binary` by calling `Ecto.Query.API.type/2`
|
||||
in your query. This way all codepoints will be properly preserved during
|
||||
insert to database.
|
||||
"""
|
||||
use Ecto.Type
|
||||
|
||||
@typedoc """
|
||||
An erlang string
|
||||
"""
|
||||
@type t :: String.t()
|
||||
|
||||
@typedoc """
|
||||
A value annotated as varchar.
|
||||
"""
|
||||
@type varchar :: {String.t(), :varchar}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def type(), do: :varchar
|
||||
|
||||
@doc """
|
||||
Casts to string.
|
||||
"""
|
||||
@spec cast(t | varchar | any) :: {:ok, t} | :error
|
||||
@impl true
|
||||
def cast({value, :varchar}) do
|
||||
# In case we get already dumped value
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
def cast(value) when is_binary(value) do
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
@doc """
|
||||
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
|
||||
"""
|
||||
@spec cast!(t | varchar | any) :: t
|
||||
def cast!(value) do
|
||||
case cast(value) do
|
||||
{:ok, uuid} -> uuid
|
||||
:error -> raise Ecto.CastError, type: __MODULE__, value: value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the DB type as is.
|
||||
"""
|
||||
@impl true
|
||||
@spec load(t | any) :: {:ok, t} | :error
|
||||
def load(value) do
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string representing a VarChar into a tuple `{value, :varchar}`.
|
||||
|
||||
Returns `:error` if value is not binary.
|
||||
"""
|
||||
@impl true
|
||||
@spec dump(t | any) :: {:ok, varchar} | :error
|
||||
def dump(value) when is_binary(value) do
|
||||
{:ok, {value, :varchar}}
|
||||
end
|
||||
|
||||
def dump(_), do: :error
|
||||
end
|
||||
end
|
||||
1778
phoenix/deps/ecto_sql/lib/ecto/migration.ex
Normal file
1778
phoenix/deps/ecto_sql/lib/ecto/migration.ex
Normal file
File diff suppressed because it is too large
Load Diff
503
phoenix/deps/ecto_sql/lib/ecto/migration/runner.ex
Normal file
503
phoenix/deps/ecto_sql/lib/ecto/migration/runner.ex
Normal file
@@ -0,0 +1,503 @@
|
||||
defmodule Ecto.Migration.Runner do
|
||||
@moduledoc false
|
||||
use Agent, restart: :temporary
|
||||
|
||||
require Logger
|
||||
|
||||
alias Ecto.Migration.Table
|
||||
alias Ecto.Migration.Index
|
||||
alias Ecto.Migration.Constraint
|
||||
alias Ecto.Migration.Command
|
||||
|
||||
@doc """
|
||||
Runs the given migration.
|
||||
"""
|
||||
def run(repo, config, version, module, direction, operation, migrator_direction, opts) do
|
||||
level = Keyword.get(opts, :log, :info)
|
||||
sql = Keyword.get(opts, :log_migrations_sql, false)
|
||||
log = %{level: level, sql: sql}
|
||||
args = {self(), repo, config, module, direction, migrator_direction, log}
|
||||
|
||||
{:ok, runner} = DynamicSupervisor.start_child(Ecto.MigratorSupervisor, {__MODULE__, args})
|
||||
metadata(runner, opts)
|
||||
|
||||
log(level, "== Running #{version} #{inspect(module)}.#{operation}/0 #{direction}")
|
||||
{time, _} = :timer.tc(fn -> perform_operation(repo, module, operation) end)
|
||||
log(level, "== Migrated #{version} in #{inspect(div(time, 100_000) / 10)}s")
|
||||
after
|
||||
stop()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the runner metadata.
|
||||
"""
|
||||
def metadata(runner, opts) do
|
||||
prefix = opts[:prefix]
|
||||
Process.put(:ecto_migration, %{runner: runner, prefix: prefix && to_string(prefix)})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts the runner for the specified repo.
|
||||
"""
|
||||
def start_link({parent, repo, config, module, direction, migrator_direction, log}) do
|
||||
Agent.start_link(fn ->
|
||||
Process.link(parent)
|
||||
|
||||
%{
|
||||
direction: direction,
|
||||
repo: repo,
|
||||
migration: module,
|
||||
migrator_direction: migrator_direction,
|
||||
command: nil,
|
||||
subcommands: [],
|
||||
log: log,
|
||||
commands: [],
|
||||
config: config
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the runner.
|
||||
"""
|
||||
def stop() do
|
||||
Agent.stop(runner())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Accesses the given repository configuration.
|
||||
"""
|
||||
def repo_config(key, default) do
|
||||
Agent.get(runner(), &Keyword.get(&1.config, key, default))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the migrator command (up or down).
|
||||
|
||||
* forward + up: up
|
||||
* forward + down: down
|
||||
* forward + change: up
|
||||
* backward + change: down
|
||||
|
||||
"""
|
||||
def migrator_direction do
|
||||
Agent.get(runner(), & &1.migrator_direction)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the repo for this migration
|
||||
"""
|
||||
def repo do
|
||||
Agent.get(runner(), & &1.repo)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the prefix for this migration
|
||||
"""
|
||||
def prefix do
|
||||
case Process.get(:ecto_migration) do
|
||||
%{prefix: prefix} -> prefix
|
||||
_ -> raise "could not find migration runner process for #{inspect(self())}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes queue migration commands.
|
||||
|
||||
Reverses the order commands are executed when doing a rollback
|
||||
on a change/0 function and resets commands queue.
|
||||
"""
|
||||
def flush do
|
||||
%{commands: commands, direction: direction, repo: repo, log: log, migration: migration} =
|
||||
Agent.get_and_update(runner(), fn state -> {state, %{state | commands: []}} end)
|
||||
|
||||
commands = if direction == :backward, do: commands, else: Enum.reverse(commands)
|
||||
|
||||
for command <- commands do
|
||||
execute_in_direction(repo, migration, direction, log, command)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Queues command tuples or strings for execution.
|
||||
|
||||
Ecto.MigrationError will be raised when the server
|
||||
is in `:backward` direction and `command` is irreversible.
|
||||
"""
|
||||
def execute(command) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:ok, %{state | subcommands: [], commands: [command | state.commands]}}
|
||||
|
||||
%{command: _} = state ->
|
||||
{:error, %{state | command: nil}}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, "cannot execute nested commands"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a command.
|
||||
"""
|
||||
def start_command(command) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:ok, %{state | command: command}}
|
||||
|
||||
%{command: _} = state ->
|
||||
{:error, %{state | command: command}}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, "cannot execute nested commands"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Queues and clears current command. Must call `start_command/1` first.
|
||||
"""
|
||||
def end_command do
|
||||
Agent.update(runner(), fn state ->
|
||||
{operation, object} = state.command
|
||||
command = {operation, object, Enum.reverse(state.subcommands)}
|
||||
%{state | command: nil, subcommands: [], commands: [command | state.commands]}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a subcommand to the current command. Must call `start_command/1` first.
|
||||
"""
|
||||
def subcommand(subcommand) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:error, state}
|
||||
|
||||
state ->
|
||||
{:ok, update_in(state.subcommands, &[subcommand | &1])}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, message: "cannot execute command outside of block"
|
||||
end
|
||||
end
|
||||
|
||||
## Execute
|
||||
|
||||
defp execute_in_direction(repo, migration, :forward, log, %Command{up: up}) do
|
||||
log_and_execute_ddl(repo, migration, log, up)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :forward, log, command) do
|
||||
log_and_execute_ddl(repo, migration, log, command)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :backward, log, %Command{down: down}) do
|
||||
log_and_execute_ddl(repo, migration, log, down)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :backward, log, command) do
|
||||
if reversed = reverse(command) do
|
||||
log_and_execute_ddl(repo, migration, log, reversed)
|
||||
else
|
||||
raise Ecto.MigrationError,
|
||||
message:
|
||||
"cannot reverse migration command: #{command(command)}. " <>
|
||||
"You will need to explicitly define up/0 and down/0 in your migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp reverse({:create, %Index{} = index}),
|
||||
do: {:drop, index, :restrict}
|
||||
|
||||
defp reverse({:create_if_not_exists, %Index{} = index}),
|
||||
do: {:drop_if_exists, index, :restrict}
|
||||
|
||||
defp reverse({:drop, %Index{} = index, _}),
|
||||
do: {:create, index}
|
||||
|
||||
defp reverse({:drop_if_exists, %Index{} = index, _}),
|
||||
do: {:create_if_not_exists, index}
|
||||
|
||||
defp reverse({:rename, %Index{} = index, new_name}),
|
||||
do: {:rename, %{index | name: new_name}, index.name}
|
||||
|
||||
defp reverse({:create, %Table{} = table, _columns}),
|
||||
do: {:drop, table, :restrict}
|
||||
|
||||
defp reverse({:create_if_not_exists, %Table{} = table, _columns}),
|
||||
do: {:drop_if_exists, table, :restrict}
|
||||
|
||||
defp reverse({:rename, %Table{} = table_current, %Table{} = table_new}),
|
||||
do: {:rename, table_new, table_current}
|
||||
|
||||
defp reverse({:rename, %Table{} = table, current_column, new_column}),
|
||||
do: {:rename, table, new_column, current_column}
|
||||
|
||||
defp reverse({:alter, %Table{} = table, changes}) do
|
||||
if reversed = table_reverse(changes, []) do
|
||||
{:alter, table, reversed}
|
||||
end
|
||||
end
|
||||
|
||||
# It is not a good idea to reverse constraints because
|
||||
# we can't guarantee data integrity when applying them back.
|
||||
defp reverse({:create_if_not_exists, %Constraint{} = constraint}),
|
||||
do: {:drop_if_exists, constraint, :restrict}
|
||||
|
||||
defp reverse({:create, %Constraint{} = constraint}),
|
||||
do: {:drop, constraint, :restrict}
|
||||
|
||||
defp reverse(_command), do: false
|
||||
|
||||
defp table_reverse([{:remove, name, type, opts} | t], acc) do
|
||||
table_reverse(t, [{:add, name, type, opts} | acc])
|
||||
end
|
||||
|
||||
defp table_reverse([{:modify, name, type, opts} | t], acc) do
|
||||
case opts[:from] do
|
||||
nil ->
|
||||
false
|
||||
|
||||
{reverse_type, from_opts} when is_list(from_opts) ->
|
||||
reverse_from = {type, Keyword.delete(opts, :from)}
|
||||
reverse_opts = Keyword.put(from_opts, :from, reverse_from)
|
||||
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
|
||||
|
||||
reverse_type ->
|
||||
reverse_opts = Keyword.put(opts, :from, type)
|
||||
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp table_reverse([{:add, name, type, _opts} | t], acc) do
|
||||
table_reverse(t, [{:remove, name, type, []} | acc])
|
||||
end
|
||||
|
||||
defp table_reverse([_ | _], _acc) do
|
||||
false
|
||||
end
|
||||
|
||||
defp table_reverse([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp perform_operation(repo, module, operation) do
|
||||
if function_exported?(repo, :in_transaction?, 0) and repo.in_transaction?() do
|
||||
if function_exported?(module, :after_begin, 0) do
|
||||
module.after_begin()
|
||||
flush()
|
||||
end
|
||||
|
||||
apply(module, operation, [])
|
||||
flush()
|
||||
|
||||
if function_exported?(module, :before_commit, 0) do
|
||||
module.before_commit()
|
||||
flush()
|
||||
end
|
||||
else
|
||||
apply(module, operation, [])
|
||||
flush()
|
||||
end
|
||||
end
|
||||
|
||||
defp runner do
|
||||
case Process.get(:ecto_migration) do
|
||||
%{runner: runner} -> runner
|
||||
_ -> raise "could not find migration runner process for #{inspect(self())}"
|
||||
end
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, migration, log, {instruction, %Index{} = index}) do
|
||||
maybe_warn_index_ddl_transaction(index, migration)
|
||||
maybe_warn_index_migration_lock(index, repo, migration)
|
||||
log_and_execute_ddl(repo, log, {instruction, index})
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, _migration, log, command) do
|
||||
log_and_execute_ddl(repo, log, command)
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(_repo, _log, func) when is_function(func, 0) do
|
||||
func.()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, %{level: level, sql: sql}, command) do
|
||||
log(level, command(command))
|
||||
meta = Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
|
||||
{:ok, logs} = repo.__adapter__().execute_ddl(meta, command, timeout: :infinity, log: sql)
|
||||
|
||||
Enum.each(logs, fn {ddl_log_level, message, metadata} ->
|
||||
ddl_log(ddl_log_level, level, message, metadata)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ddl_log(_level, false, _msg, _metadata), do: :ok
|
||||
defp ddl_log(level, _, msg, metadata), do: log(level, msg, metadata)
|
||||
|
||||
defp log(level, msg, metadata \\ [])
|
||||
defp log(false, _msg, _metadata), do: :ok
|
||||
defp log(true, msg, metadata), do: Logger.log(:info, msg, metadata)
|
||||
defp log(level, msg, metadata), do: Logger.log(level, msg, metadata)
|
||||
|
||||
defp maybe_warn_index_ddl_transaction(%{concurrently: true} = index, migration) do
|
||||
migration_config = migration.__migration__()
|
||||
|
||||
if not migration_config[:disable_ddl_transaction] do
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable ddl transaction. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_ddl_transaction true
|
||||
""",
|
||||
[]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_warn_index_ddl_transaction(_index, _migration), do: :ok
|
||||
|
||||
defp maybe_warn_index_migration_lock(%{concurrently: true} = index, repo, migration) do
|
||||
migration_lock_disabled = migration.__migration__()[:disable_migration_lock]
|
||||
lock_strategy = repo.config()[:migration_lock]
|
||||
adapter = repo.__adapter__()
|
||||
|
||||
case {migration_lock_disabled, adapter, lock_strategy} do
|
||||
{false, Ecto.Adapters.Postgres, :pg_advisory_lock} ->
|
||||
:ok
|
||||
|
||||
{false, Ecto.Adapters.Postgres, _} ->
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable migration lock. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_migration_lock true
|
||||
|
||||
Alternatively, consider using advisory locks during migrations in the \
|
||||
repo configuration:
|
||||
|
||||
config #{inspect(repo)}, migration_lock: :pg_advisory_lock
|
||||
""",
|
||||
[]
|
||||
)
|
||||
|
||||
{false, _adapter, _migration_lock} ->
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable migration lock. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_migration_lock true
|
||||
""",
|
||||
[]
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_warn_index_migration_lock(_index, _repo, _migration), do: :ok
|
||||
|
||||
defp command(ddl) when is_binary(ddl) or is_list(ddl),
|
||||
do: "execute #{inspect(ddl)}"
|
||||
|
||||
defp command({:create, %Table{} = table, _}),
|
||||
do: "create table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:create_if_not_exists, %Table{} = table, _}),
|
||||
do: "create table if not exists #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:alter, %Table{} = table, _}),
|
||||
do: "alter table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:drop, %Table{} = table, mode}),
|
||||
do: "drop table #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:drop_if_exists, %Table{} = table, mode}),
|
||||
do: "drop table if exists #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:create, %Index{} = index}),
|
||||
do: "create index #{quote_name(index.prefix, index.name)}"
|
||||
|
||||
defp command({:create_if_not_exists, %Index{} = index}),
|
||||
do: "create index if not exists #{quote_name(index.prefix, index.name)}"
|
||||
|
||||
defp command({:drop, %Index{} = index, mode}),
|
||||
do: "drop index #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:drop_if_exists, %Index{} = index, mode}),
|
||||
do: "drop index if exists #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:rename, %Index{} = index_current, new_name}),
|
||||
do: "rename index #{quote_name(index_current.prefix, index_current.name)} to #{new_name}"
|
||||
|
||||
defp command({:rename, %Table{} = current_table, %Table{} = new_table}),
|
||||
do:
|
||||
"rename table #{quote_name(current_table.prefix, current_table.name)} to #{quote_name(new_table.prefix, new_table.name)}"
|
||||
|
||||
defp command({:rename, %Table{} = table, current_column, new_column}),
|
||||
do:
|
||||
"rename column #{current_column} to #{new_column} on table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:create, %Constraint{check: nil, exclude: nil}}),
|
||||
do: raise(ArgumentError, "a constraint must have either a check or exclude option")
|
||||
|
||||
defp command({:create, %Constraint{check: check, exclude: exclude}})
|
||||
when is_binary(check) and is_binary(exclude),
|
||||
do: raise(ArgumentError, "a constraint must not have both check and exclude options")
|
||||
|
||||
defp command({:create, %Constraint{check: check} = constraint}) when is_binary(check),
|
||||
do:
|
||||
"create check constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:create, %Constraint{exclude: exclude} = constraint}) when is_binary(exclude),
|
||||
do:
|
||||
"create exclude constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:drop, %Constraint{} = constraint, _}),
|
||||
do:
|
||||
"drop constraint #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:drop_if_exists, %Constraint{} = constraint, _}),
|
||||
do:
|
||||
"drop constraint if exists #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp drop_mode(:restrict), do: ""
|
||||
defp drop_mode(:cascade), do: " cascade"
|
||||
|
||||
defp quote_name(nil, name), do: quote_name(name)
|
||||
defp quote_name(prefix, name), do: quote_name(prefix) <> "." <> quote_name(name)
|
||||
defp quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name))
|
||||
defp quote_name(name), do: name
|
||||
end
|
||||
81
phoenix/deps/ecto_sql/lib/ecto/migration/schema_migration.ex
Normal file
81
phoenix/deps/ecto_sql/lib/ecto/migration/schema_migration.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule Ecto.Migration.SchemaMigration do
|
||||
# Defines a schema that works with a table that tracks schema migrations.
|
||||
# The table name defaults to `schema_migrations`.
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
@primary_key false
|
||||
schema "schema_migrations" do
|
||||
field :version, :integer, primary_key: true
|
||||
timestamps updated_at: false
|
||||
end
|
||||
|
||||
# The migration flag is used to signal to the repository
|
||||
# we are in a migration operation.
|
||||
@default_opts [
|
||||
timeout: :infinity,
|
||||
log: false,
|
||||
# Keep schema_migration for backwards compatibility
|
||||
schema_migration: true,
|
||||
ecto_query: :schema_migration,
|
||||
telemetry_options: [schema_migration: true]
|
||||
]
|
||||
|
||||
def ensure_schema_migrations_table!(repo, config, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
table_name = String.to_atom(source)
|
||||
table = %Ecto.Migration.Table{name: table_name, prefix: opts[:prefix]}
|
||||
meta = Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
|
||||
|
||||
commands = [
|
||||
{:add, :version, :bigint, primary_key: true},
|
||||
{:add, :inserted_at, :naive_datetime, []}
|
||||
]
|
||||
|
||||
repo.__adapter__().execute_ddl(meta, {:create_if_not_exists, table, commands}, @default_opts)
|
||||
end
|
||||
|
||||
def versions(repo, config, prefix) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
from_opts = [prefix: prefix] ++ @default_opts
|
||||
|
||||
query =
|
||||
if Keyword.get(config, :migration_cast_version_column, false) do
|
||||
from(m in source, select: type(m.version, :integer))
|
||||
else
|
||||
from(m in source, select: m.version)
|
||||
end
|
||||
|
||||
{repo, query, from_opts}
|
||||
end
|
||||
|
||||
def up(repo, config, version, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
|
||||
%__MODULE__{version: version}
|
||||
|> Ecto.put_meta(source: source)
|
||||
|> repo.insert(default_opts(opts))
|
||||
end
|
||||
|
||||
def down(repo, config, version, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
|
||||
from(m in source, where: m.version == type(^version, :integer))
|
||||
|> repo.delete_all(default_opts(opts))
|
||||
end
|
||||
|
||||
def get_repo_and_source(repo, config) do
|
||||
{Keyword.get(config, :migration_repo, repo),
|
||||
Keyword.get(config, :migration_source, "schema_migrations")}
|
||||
end
|
||||
|
||||
defp default_opts(opts) do
|
||||
Keyword.merge(
|
||||
@default_opts,
|
||||
prefix: opts[:prefix],
|
||||
log: Keyword.get(opts, :log_migrator_sql, false)
|
||||
)
|
||||
end
|
||||
end
|
||||
861
phoenix/deps/ecto_sql/lib/ecto/migrator.ex
Normal file
861
phoenix/deps/ecto_sql/lib/ecto/migrator.ex
Normal file
@@ -0,0 +1,861 @@
|
||||
defmodule Ecto.Migrator do
|
||||
@moduledoc """
|
||||
Lower level API for managing migrations.
|
||||
|
||||
EctoSQL provides three mix tasks for running and managing migrations:
|
||||
|
||||
* `mix ecto.migrate` - migrates a repository
|
||||
* `mix ecto.rollback` - rolls back a particular migration
|
||||
* `mix ecto.migrations` - shows all migrations and their status
|
||||
|
||||
Those tasks are built on top of the functions in this module.
|
||||
While the tasks above cover most use cases, it may be necessary
|
||||
from time to time to jump into the lower level API. For example,
|
||||
if you are assembling an Elixir release, Mix is not available,
|
||||
so this module provides a nice complement to still migrate your
|
||||
system.
|
||||
|
||||
To learn more about migrations in general, see `Ecto.Migration`.
|
||||
|
||||
## Example: Running an individual migration
|
||||
|
||||
Imagine you have this migration:
|
||||
|
||||
defmodule MyApp.MigrationExample do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute "CREATE TABLE users(id serial PRIMARY_KEY, username text)"
|
||||
end
|
||||
|
||||
def down do
|
||||
execute "DROP TABLE users"
|
||||
end
|
||||
end
|
||||
|
||||
You can execute it manually with:
|
||||
|
||||
Ecto.Migrator.up(Repo, 20080906120000, MyApp.MigrationExample)
|
||||
|
||||
## Example: Running migrations in a release
|
||||
|
||||
Elixir v1.9 introduces `mix release`, which generates a self-contained
|
||||
directory that consists of your application code, all of its dependencies,
|
||||
plus the whole Erlang Virtual Machine (VM) and runtime.
|
||||
|
||||
When a release is assembled, Mix is no longer available inside a release
|
||||
and therefore none of the Mix tasks. Users may still need a mechanism to
|
||||
migrate their databases. This can be achieved with using the `Ecto.Migrator`
|
||||
module:
|
||||
|
||||
defmodule MyApp.Release do
|
||||
@app :my_app
|
||||
|
||||
def migrate do
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
def rollback(repo, version) do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||
end
|
||||
|
||||
defp repos do
|
||||
Application.load(@app)
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
end
|
||||
|
||||
The example above uses `with_repo/3` to make sure the repository is
|
||||
started and then runs all migrations up or a given migration down.
|
||||
Note you will have to replace `MyApp` and `:my_app` on the first two
|
||||
lines by your actual application name. Once the file above is added
|
||||
to your application, you can assemble a new release and invoke the
|
||||
commands above in the release root like this:
|
||||
|
||||
$ bin/my_app eval "MyApp.Release.migrate"
|
||||
$ bin/my_app eval "MyApp.Release.rollback(MyApp.Repo, 20190417140000)"
|
||||
|
||||
## Example: Running migrations on application startup
|
||||
|
||||
Add the following to the top of your application children spec:
|
||||
|
||||
{Ecto.Migrator,
|
||||
repos: Application.fetch_env!(:my_app, :ecto_repos),
|
||||
skip: System.get_env("SKIP_MIGRATIONS") == "true"}
|
||||
|
||||
To skip migrations you can also pass `skip: true` or as in the example
|
||||
set the environment variable `SKIP_MIGRATIONS` to a truthy value.
|
||||
|
||||
And all other options described in `up/4` are allowed,
|
||||
for example if you want to log the SQL commands,
|
||||
and run migrations in a prefix:
|
||||
|
||||
{Ecto.Migrator,
|
||||
repos: Application.fetch_env!(:my_app, :ecto_repos),
|
||||
log_migrator_sql: true,
|
||||
prefix: "my_app"}
|
||||
|
||||
To roll back you'd do it normally:
|
||||
|
||||
$ mix ecto.rollback
|
||||
|
||||
"""
|
||||
|
||||
require Logger
|
||||
require Ecto.Query
|
||||
|
||||
alias Ecto.Migration.Runner
|
||||
alias Ecto.Migration.SchemaMigration
|
||||
|
||||
@doc """
|
||||
Ensures the repo is started to perform migration operations.
|
||||
|
||||
All of the application required to run the repo will be started
|
||||
before hand with chosen mode. If the repo has not yet been started,
|
||||
it is manually started, with a `:pool_size` of 2, before the given
|
||||
function is executed, and the repo is then terminated. If the repo
|
||||
was already started, then the function is directly executed, without
|
||||
terminating the repo afterwards.
|
||||
|
||||
Although this function was designed to start repositories for running
|
||||
migrations, it can be used by any code, Mix task, or release tooling
|
||||
that needs to briefly start a repository to perform a certain operation
|
||||
and then terminate.
|
||||
|
||||
The repo may also configure a `:start_apps_before_migration` option
|
||||
which is a list of applications to be started before the migration
|
||||
runs.
|
||||
|
||||
It returns `{:ok, fun_return, apps}`, with all apps that have been
|
||||
started, or `{:error, term}`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:pool_size` - The pool size to start the repo for migrations.
|
||||
Defaults to 2.
|
||||
* `:mode` - The mode to start all applications.
|
||||
Defaults to `:permanent`.
|
||||
|
||||
## Examples
|
||||
|
||||
{:ok, _, _} =
|
||||
Ecto.Migrator.with_repo(repo, fn repo ->
|
||||
Ecto.Migrator.run(repo, :up, all: true)
|
||||
end)
|
||||
|
||||
"""
|
||||
def with_repo(repo, fun, opts \\ []) do
|
||||
config = repo.config()
|
||||
mode = Keyword.get(opts, :mode, :permanent)
|
||||
apps = [:ecto_sql | config[:start_apps_before_migration] || []]
|
||||
|
||||
extra_started =
|
||||
Enum.flat_map(apps, fn app ->
|
||||
{:ok, started} = Application.ensure_all_started(app, mode)
|
||||
started
|
||||
end)
|
||||
|
||||
{:ok, repo_started} = repo.__adapter__().ensure_all_started(config, mode)
|
||||
started = extra_started ++ repo_started
|
||||
pool_size = Keyword.get(opts, :pool_size, 2)
|
||||
migration_repo = config[:migration_repo] || repo
|
||||
|
||||
case ensure_repo_started(repo, pool_size) do
|
||||
{:ok, repo_after} ->
|
||||
case ensure_migration_repo_started(migration_repo, repo) do
|
||||
{:ok, migration_repo_after} ->
|
||||
try do
|
||||
{:ok, fun.(repo), started}
|
||||
after
|
||||
after_action(repo, repo_after)
|
||||
after_action(migration_repo, migration_repo_after)
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
after_action(repo, repo_after)
|
||||
error
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the migrations path from a repository.
|
||||
|
||||
This function accepts an optional second parameter to customize the
|
||||
migrations directory. This can be used to specify a custom migrations
|
||||
path.
|
||||
"""
|
||||
@spec migrations_path(Ecto.Repo.t(), String.t()) :: String.t()
|
||||
def migrations_path(repo, directory \\ "migrations") do
|
||||
config = repo.config()
|
||||
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
|
||||
app = Keyword.fetch!(config, :otp_app)
|
||||
Application.app_dir(app, Path.join(priv, directory))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all migrated versions.
|
||||
|
||||
This function ensures the migration table exists
|
||||
if no table has been defined yet.
|
||||
|
||||
## Options
|
||||
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
* `:skip_table_creation` - skips any attempt to create the migration table
|
||||
Useful for situations where user needs to check migrations but has
|
||||
insufficient permissions to create the table. Note that migrations
|
||||
commands may fail if this is set to true. Defaults to `false`. Accepts a
|
||||
boolean.
|
||||
"""
|
||||
@spec migrated_versions(Ecto.Repo.t(), Keyword.t()) :: [integer]
|
||||
def migrated_versions(repo, opts \\ []) do
|
||||
lock_for_migrations(true, repo, opts, fn _config, versions -> versions end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs an up migration on the given repository.
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - the level to use for logging of migration instructions.
|
||||
Defaults to `:info`. Can be any of `Logger.level/0` values or a boolean.
|
||||
If `false`, it also avoids logging messages from the database.
|
||||
* `:log_migrations_sql` - the level to use for logging of SQL commands
|
||||
generated by migrations. Can be any of the `Logger.level/0` values
|
||||
or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:log_migrator_sql` - the level to use for logging of SQL commands emitted
|
||||
by the migrator, such as transactions, locks, etc. Can be any of the `Logger.level/0`
|
||||
values or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
* `:strict_version_order` - abort when applying a migration with old timestamp
|
||||
(otherwise it emits a warning)
|
||||
"""
|
||||
@spec up(Ecto.Repo.t(), integer, module, Keyword.t()) :: :ok | :already_up
|
||||
def up(repo, version, module, opts \\ []) do
|
||||
conditional_lock_for_migrations(module, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
:already_up
|
||||
else
|
||||
result = do_up(repo, config, version, module, opts)
|
||||
|
||||
if version != Enum.max([version | versions]) do
|
||||
latest = Enum.max(versions)
|
||||
|
||||
message = """
|
||||
You are running migration #{version} but an older \
|
||||
migration with version #{latest} has already run.
|
||||
|
||||
This can be an issue if you have already ran #{latest} in production \
|
||||
because a new deployment may migrate #{version} but a rollback command \
|
||||
would revert #{latest} instead of #{version}.
|
||||
|
||||
If this can be an issue, we recommend to rollback #{version} and change \
|
||||
it to a version later than #{latest}.
|
||||
"""
|
||||
|
||||
if opts[:strict_version_order] do
|
||||
raise Ecto.MigrationError, message
|
||||
else
|
||||
Logger.warning(message)
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_up(repo, config, version, module, opts) do
|
||||
async_migrate_maybe_in_transaction(repo, config, version, module, :up, opts, fn ->
|
||||
attempt(repo, config, version, module, :forward, :up, :up, opts) ||
|
||||
attempt(repo, config, version, module, :forward, :change, :up, opts) ||
|
||||
{:error,
|
||||
Ecto.MigrationError.exception(
|
||||
"#{inspect(module)} does not implement a `up/0` or `change/0` function"
|
||||
)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs a down migration on the given repository.
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - the level to use for logging of migration commands. Defaults to `:info`.
|
||||
Can be any of `Logger.level/0` values or a boolean.
|
||||
* `:log_migrations_sql` - the level to use for logging of SQL commands
|
||||
generated by migrations. Can be any of the `Logger.level/0` values
|
||||
or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:log_migrator_sql` - the level to use for logging of SQL commands emitted
|
||||
by the migrator, such as transactions, locks, etc. Can be any of the `Logger.level/0`
|
||||
values or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
|
||||
"""
|
||||
@spec down(Ecto.Repo.t(), integer, module) :: :ok | :already_down
|
||||
def down(repo, version, module, opts \\ []) do
|
||||
conditional_lock_for_migrations(module, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
do_down(repo, config, version, module, opts)
|
||||
else
|
||||
:already_down
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_down(repo, config, version, module, opts) do
|
||||
async_migrate_maybe_in_transaction(repo, config, version, module, :down, opts, fn ->
|
||||
attempt(repo, config, version, module, :forward, :down, :down, opts) ||
|
||||
attempt(repo, config, version, module, :backward, :change, :down, opts) ||
|
||||
{:error,
|
||||
Ecto.MigrationError.exception(
|
||||
"#{inspect(module)} does not implement a `down/0` or `change/0` function"
|
||||
)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp async_migrate_maybe_in_transaction(repo, config, version, module, direction, opts, fun) do
|
||||
dynamic_repo = repo.get_dynamic_repo()
|
||||
|
||||
fun_with_status = fn ->
|
||||
result = fun.()
|
||||
apply(SchemaMigration, direction, [repo, config, version, opts])
|
||||
result
|
||||
end
|
||||
|
||||
fn -> run_maybe_in_transaction(repo, dynamic_repo, module, fun_with_status, opts) end
|
||||
|> Task.async()
|
||||
|> Task.await(:infinity)
|
||||
end
|
||||
|
||||
defp run_maybe_in_transaction(repo, dynamic_repo, module, fun, opts) do
|
||||
repo.put_dynamic_repo(dynamic_repo)
|
||||
|
||||
if module.__migration__()[:disable_ddl_transaction] ||
|
||||
not repo.__adapter__().supports_ddl_transaction?() do
|
||||
fun.()
|
||||
else
|
||||
{:ok, result} = repo.transaction(fun, log: migrator_log(opts), timeout: :infinity)
|
||||
|
||||
result
|
||||
end
|
||||
catch
|
||||
kind, reason ->
|
||||
{kind, reason, __STACKTRACE__}
|
||||
end
|
||||
|
||||
defp attempt(repo, config, version, module, direction, operation, reference, opts) do
|
||||
if Code.ensure_loaded?(module) and function_exported?(module, operation, 0) do
|
||||
Runner.run(repo, config, version, module, direction, operation, reference, opts)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs migrations for the given repository.
|
||||
|
||||
Equivalent to:
|
||||
|
||||
Ecto.Migrator.run(repo, [Ecto.Migrator.migrations_path(repo)], direction, opts)
|
||||
|
||||
See `run/4` for more information.
|
||||
"""
|
||||
@spec run(Ecto.Repo.t(), atom, Keyword.t()) :: [integer]
|
||||
def run(repo, direction, opts) do
|
||||
run(repo, [migrations_path(repo)], direction, opts)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Apply migrations to a repository with a given strategy.
|
||||
|
||||
The second argument identifies where the migrations are sourced from.
|
||||
A binary representing directory (or a list of binaries representing
|
||||
directories) may be passed, in which case we will load all files
|
||||
following the "#{VERSION}_#{NAME}.exs" schema. The `migration_source`
|
||||
may also be a list of tuples that identify the version number and
|
||||
migration modules to be run, for example:
|
||||
|
||||
Ecto.Migrator.run(Repo, [{0, MyApp.Migration1}, {1, MyApp.Migration2}, ...], :up, opts)
|
||||
|
||||
A strategy (which is one of `:all`, `:step`, `:to`, or `:to_exclusive`) must be given as
|
||||
an option.
|
||||
|
||||
## Execution model
|
||||
|
||||
In order to run migrations, at least two database connections are
|
||||
necessary. One is used to lock the "schema_migrations" table and
|
||||
the other one to effectively run the migrations. This allows multiple
|
||||
nodes to run migrations at the same time, but guarantee that only one
|
||||
of them will effectively migrate the database.
|
||||
|
||||
A downside of this approach is that migrations cannot run dynamically
|
||||
during test under the `Ecto.Adapters.SQL.Sandbox`, as the sandbox has
|
||||
to share a single connection across processes to guarantee the changes
|
||||
can be reverted.
|
||||
|
||||
## Options
|
||||
|
||||
* `:all` - runs all available if `true`
|
||||
|
||||
* `:step` - runs the specific number of migrations
|
||||
|
||||
* `:to` - runs all until the supplied version is reached
|
||||
(including the version given in `:to`)
|
||||
|
||||
* `:to_exclusive` - runs all until the supplied version is reached
|
||||
(excluding the version given in `:to_exclusive`)
|
||||
|
||||
Plus all other options described in `up/4`.
|
||||
"""
|
||||
@spec run(Ecto.Repo.t(), String.t() | [String.t()] | [{integer, module}], atom, Keyword.t()) ::
|
||||
[integer]
|
||||
def run(repo, migration_source, direction, opts) do
|
||||
migration_source = List.wrap(migration_source)
|
||||
|
||||
pending =
|
||||
lock_for_migrations(true, repo, opts, fn _config, versions ->
|
||||
cond do
|
||||
opts[:all] ->
|
||||
pending_all(versions, migration_source, direction)
|
||||
|
||||
to = opts[:to] ->
|
||||
pending_to(versions, migration_source, direction, to)
|
||||
|
||||
to_exclusive = opts[:to_exclusive] ->
|
||||
pending_to_exclusive(versions, migration_source, direction, to_exclusive)
|
||||
|
||||
step = opts[:step] ->
|
||||
pending_step(versions, migration_source, direction, step)
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
ArgumentError.exception(
|
||||
"expected one of :all, :to, :to_exclusive, or :step strategies"
|
||||
)}
|
||||
end
|
||||
end)
|
||||
|
||||
# The lock above already created the table, so we can now skip it.
|
||||
opts = Keyword.put(opts, :skip_table_creation, true)
|
||||
|
||||
ensure_no_duplication!(pending)
|
||||
migrate(Enum.map(pending, &load_migration!/1), direction, repo, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an array of tuples as the migration status of the given repo,
|
||||
without actually running any migrations.
|
||||
|
||||
Equivalent to:
|
||||
|
||||
Ecto.Migrator.migrations(repo, [Ecto.Migrator.migrations_path(repo)])
|
||||
|
||||
"""
|
||||
@spec migrations(Ecto.Repo.t()) :: [{:up | :down, id :: integer(), name :: String.t()}]
|
||||
def migrations(repo) do
|
||||
migrations(repo, [migrations_path(repo)])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an array of tuples as the migration status of the given repo,
|
||||
without actually running any migrations.
|
||||
"""
|
||||
@spec migrations(Ecto.Repo.t(), String.t() | [String.t()], Keyword.t()) ::
|
||||
[{:up | :down, id :: integer(), name :: String.t()}]
|
||||
def migrations(repo, directories, opts \\ []) do
|
||||
directories = List.wrap(directories)
|
||||
|
||||
repo
|
||||
|> migrated_versions(opts)
|
||||
|> collect_migrations(directories)
|
||||
|> Enum.sort_by(fn {_, version, _} -> version end)
|
||||
end
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc """
|
||||
Runs migrations as part of your supervision tree.
|
||||
|
||||
## Options
|
||||
|
||||
* `:repos` - Required option to tell the migrator which Repo's to
|
||||
migrate. Example: `repos: [MyApp.Repo]`
|
||||
|
||||
* `:skip` - Option to skip migrations. Defaults to `false`.
|
||||
|
||||
Plus all other options described in `up/4`.
|
||||
|
||||
See "Example: Running migrations on application startup" for more info.
|
||||
"""
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{repos, opts} = Keyword.pop!(opts, :repos)
|
||||
{skip?, opts} = Keyword.pop(opts, :skip, false)
|
||||
{migrator, opts} = Keyword.pop(opts, :migrator, &Ecto.Migrator.run/3)
|
||||
opts = Keyword.put(opts, :all, true)
|
||||
|
||||
unless skip? do
|
||||
for repo <- repos do
|
||||
{:ok, _, _} = with_repo(repo, &migrator.(&1, :up, opts))
|
||||
end
|
||||
end
|
||||
|
||||
:ignore
|
||||
end
|
||||
|
||||
defp collect_migrations(versions, migration_source) do
|
||||
ups_with_file =
|
||||
versions
|
||||
|> pending_in_direction(migration_source, :down)
|
||||
|> Enum.map(fn {version, name, _} -> {:up, version, name} end)
|
||||
|
||||
ups_without_file =
|
||||
versions
|
||||
|> versions_without_file(migration_source)
|
||||
|> Enum.map(fn version -> {:up, version, "** FILE NOT FOUND **"} end)
|
||||
|
||||
downs =
|
||||
versions
|
||||
|> pending_in_direction(migration_source, :up)
|
||||
|> Enum.map(fn {version, name, _} -> {:down, version, name} end)
|
||||
|
||||
ups_with_file ++ ups_without_file ++ downs
|
||||
end
|
||||
|
||||
defp versions_without_file(versions, migration_source) do
|
||||
versions_with_file =
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.map(fn {version, _, _} -> version end)
|
||||
|
||||
versions -- versions_with_file
|
||||
end
|
||||
|
||||
defp lock_for_migrations(lock_or_migration_number, repo, opts, fun) do
|
||||
dynamic_repo = Keyword.get(opts, :dynamic_repo, repo.get_dynamic_repo())
|
||||
skip_table_creation = Keyword.get(opts, :skip_table_creation, false)
|
||||
previous_dynamic_repo = repo.put_dynamic_repo(dynamic_repo)
|
||||
|
||||
try do
|
||||
config = repo.config()
|
||||
|
||||
unless skip_table_creation do
|
||||
verbose_schema_migration(repo, "create schema migrations table", fn ->
|
||||
SchemaMigration.ensure_schema_migrations_table!(repo, config, opts)
|
||||
end)
|
||||
end
|
||||
|
||||
{migration_repo, query, all_opts} = SchemaMigration.versions(repo, config, opts[:prefix])
|
||||
|
||||
migration_lock? =
|
||||
Keyword.get(opts, :migration_lock, Keyword.get(config, :migration_lock, true))
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.put(:migration_source, config[:migration_source] || "schema_migrations")
|
||||
|> Keyword.put(:log, migrator_log(opts))
|
||||
|
||||
result =
|
||||
if lock_or_migration_number && migration_lock? do
|
||||
# If there is a migration_repo, it wins over dynamic_repo,
|
||||
# otherwise the dynamic_repo is the one locked in migrations.
|
||||
meta_repo = if migration_repo != repo, do: migration_repo, else: dynamic_repo
|
||||
meta = Ecto.Adapter.lookup_meta(meta_repo)
|
||||
|
||||
migration_repo.__adapter__().lock_for_migrations(meta, opts, fn ->
|
||||
fun.(config, migration_repo.all(query, all_opts))
|
||||
end)
|
||||
else
|
||||
fun.(config, migration_repo.all(query, all_opts))
|
||||
end
|
||||
|
||||
case result do
|
||||
{kind, reason, stacktrace} ->
|
||||
:erlang.raise(kind, reason, stacktrace)
|
||||
|
||||
{:error, error} ->
|
||||
raise error
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
after
|
||||
repo.put_dynamic_repo(previous_dynamic_repo)
|
||||
end
|
||||
end
|
||||
|
||||
defp conditional_lock_for_migrations(module, version, repo, opts, fun) do
|
||||
lock = if module.__migration__()[:disable_migration_lock], do: false, else: version
|
||||
lock_for_migrations(lock, repo, opts, fun)
|
||||
end
|
||||
|
||||
defp pending_to(versions, migration_source, direction, target) when is_integer(target) do
|
||||
within_target_version? = fn
|
||||
{version, _, _}, target, :up ->
|
||||
version <= target
|
||||
|
||||
{version, _, _}, target, :down ->
|
||||
version >= target
|
||||
end
|
||||
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take_while(&within_target_version?.(&1, target, direction))
|
||||
end
|
||||
|
||||
defp pending_to_exclusive(versions, migration_source, direction, target)
|
||||
when is_integer(target) do
|
||||
within_target_version? = fn
|
||||
{version, _, _}, target, :up ->
|
||||
version < target
|
||||
|
||||
{version, _, _}, target, :down ->
|
||||
version > target
|
||||
end
|
||||
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take_while(&within_target_version?.(&1, target, direction))
|
||||
end
|
||||
|
||||
defp pending_step(versions, migration_source, direction, count) do
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take(count)
|
||||
end
|
||||
|
||||
defp pending_all(versions, migration_source, direction) do
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
end
|
||||
|
||||
defp pending_in_direction(versions, migration_source, :up) do
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.filter(fn {version, _name, _file} -> version not in versions end)
|
||||
end
|
||||
|
||||
defp pending_in_direction(versions, migration_source, :down) do
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.filter(fn {version, _name, _file} -> version in versions end)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp migrations_for(migration_source) when is_list(migration_source) do
|
||||
migration_source
|
||||
|> Enum.flat_map(fn
|
||||
directory when is_binary(directory) ->
|
||||
Path.join([directory, "**", "*.{ex,exs}"])
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&extract_migration_info/1)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
{version, module} ->
|
||||
[{version, module, module}]
|
||||
end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp extract_migration_info(file) do
|
||||
base = Path.basename(file)
|
||||
|
||||
case Integer.parse(Path.rootname(base)) do
|
||||
{integer, "_" <> name} ->
|
||||
if Path.extname(base) == ".ex" do
|
||||
# See: https://github.com/elixir-ecto/ecto_sql/issues/599
|
||||
IO.warn(
|
||||
"""
|
||||
file looks like a migration but ends in .ex. \
|
||||
Migration files should end in .exs. Use "mix ecto.gen.migration" to generate \
|
||||
migration files with the correct extension.\
|
||||
""",
|
||||
stacktrace_info(file: file)
|
||||
)
|
||||
|
||||
nil
|
||||
else
|
||||
{integer, name, file}
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove when we require Elixir 1.14
|
||||
if Version.match?(System.version(), ">= 1.14.0") do
|
||||
defp stacktrace_info(info), do: info
|
||||
else
|
||||
defp stacktrace_info(_info), do: []
|
||||
end
|
||||
|
||||
defp ensure_no_duplication!([{version, name, _} | t]) do
|
||||
cond do
|
||||
List.keyfind(t, version, 0) ->
|
||||
raise Ecto.MigrationError,
|
||||
"migrations can't be executed, migration version #{version} is duplicated"
|
||||
|
||||
List.keyfind(t, name, 1) ->
|
||||
raise Ecto.MigrationError,
|
||||
"migrations can't be executed, migration name #{name} is duplicated"
|
||||
|
||||
true ->
|
||||
ensure_no_duplication!(t)
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_no_duplication!([]), do: :ok
|
||||
|
||||
defp load_migration!({version, _, mod}) when is_atom(mod) do
|
||||
if migration?(mod) do
|
||||
{version, mod}
|
||||
else
|
||||
raise Ecto.MigrationError, "module #{inspect(mod)} is not an Ecto.Migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp load_migration!({version, _, file}) when is_binary(file) do
|
||||
loaded_modules = file |> Code.compile_file() |> Enum.map(&elem(&1, 0))
|
||||
|
||||
if mod = Enum.find(loaded_modules, &migration?/1) do
|
||||
{version, mod}
|
||||
else
|
||||
raise Ecto.MigrationError,
|
||||
"file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp migration?(mod) do
|
||||
Code.ensure_loaded?(mod) and function_exported?(mod, :__migration__, 0)
|
||||
end
|
||||
|
||||
defp migrate([], direction, _repo, opts) do
|
||||
level = Keyword.get(opts, :log, :info)
|
||||
log(level, "Migrations already #{direction}")
|
||||
[]
|
||||
end
|
||||
|
||||
defp migrate(migrations, direction, repo, opts) do
|
||||
for {version, mod} <- migrations,
|
||||
do_direction(direction, repo, version, mod, opts),
|
||||
do: version
|
||||
end
|
||||
|
||||
defp do_direction(:up, repo, version, mod, opts) do
|
||||
conditional_lock_for_migrations(mod, version, repo, opts, fn config, versions ->
|
||||
unless version in versions do
|
||||
do_up(repo, config, version, mod, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_direction(:down, repo, version, mod, opts) do
|
||||
conditional_lock_for_migrations(mod, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
do_down(repo, config, version, mod, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp verbose_schema_migration(repo, reason, fun) do
|
||||
try do
|
||||
fun.()
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("""
|
||||
Could not #{reason}. This error usually happens due to the following:
|
||||
|
||||
* The database does not exist
|
||||
* The "schema_migrations" table, which Ecto uses for managing
|
||||
migrations, was defined by another library
|
||||
* There is a deadlock while migrating (such as using concurrent
|
||||
indexes with a migration_lock)
|
||||
|
||||
To fix the first issue, run "mix ecto.create" for the desired MIX_ENV.
|
||||
|
||||
To address the second, you can run "mix ecto.drop" followed by
|
||||
"mix ecto.create", both for the desired MIX_ENV. Alternatively you may
|
||||
configure Ecto to use another table and/or repository for managing
|
||||
migrations:
|
||||
|
||||
config #{inspect(repo.config()[:otp_app])}, #{inspect(repo)},
|
||||
migration_source: "some_other_table_for_schema_migrations",
|
||||
migration_repo: AnotherRepoForSchemaMigrations
|
||||
|
||||
The full error report is shown below.
|
||||
""")
|
||||
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
defp log(false, _msg), do: :ok
|
||||
defp log(true, msg), do: Logger.info(msg)
|
||||
defp log(level, msg), do: Logger.log(level, msg)
|
||||
|
||||
defp migrator_log(opts) do
|
||||
Keyword.get(opts, :log_migrator_sql, false)
|
||||
end
|
||||
|
||||
defp ensure_repo_started(repo, pool_size) do
|
||||
case repo.start_link(pool_size: pool_size) do
|
||||
{:ok, _} ->
|
||||
{:ok, :stop}
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
{:ok, :restart}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_migration_repo_started(repo, repo) do
|
||||
{:ok, :noop}
|
||||
end
|
||||
|
||||
defp ensure_migration_repo_started(migration_repo, _repo) do
|
||||
case migration_repo.start_link() do
|
||||
{:ok, _} ->
|
||||
{:ok, :stop}
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
{:ok, :noop}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp after_action(repo, :restart) do
|
||||
if Process.whereis(repo) do
|
||||
%{pid: pid} = Ecto.Adapter.lookup_meta(repo)
|
||||
Supervisor.restart_child(repo, pid)
|
||||
end
|
||||
end
|
||||
|
||||
defp after_action(repo, :stop) do
|
||||
repo.stop()
|
||||
end
|
||||
|
||||
defp after_action(_repo, :noop) do
|
||||
:noop
|
||||
end
|
||||
end
|
||||
45
phoenix/deps/ecto_sql/lib/mix/ecto_sql.ex
Normal file
45
phoenix/deps/ecto_sql/lib/mix/ecto_sql.ex
Normal file
@@ -0,0 +1,45 @@
|
||||
defmodule Mix.EctoSQL do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Ensures the given repository's migrations paths exists on the file system.
|
||||
"""
|
||||
@spec ensure_migrations_paths(Ecto.Repo.t(), Keyword.t()) :: [String.t()]
|
||||
def ensure_migrations_paths(repo, opts) do
|
||||
paths = Keyword.get_values(opts, :migrations_path)
|
||||
paths = if paths == [], do: [Path.join(source_repo_priv(repo), "migrations")], else: paths
|
||||
|
||||
if not Mix.Project.umbrella?() do
|
||||
for path <- paths, not File.dir?(path) do
|
||||
raise_missing_migrations(Path.relative_to_cwd(path), repo)
|
||||
end
|
||||
end
|
||||
|
||||
paths
|
||||
end
|
||||
|
||||
defp raise_missing_migrations(path, repo) do
|
||||
Mix.raise("""
|
||||
Could not find migrations directory #{inspect(path)}
|
||||
for repo #{inspect(repo)}.
|
||||
|
||||
This may be because you are in a new project and the
|
||||
migration directory has not been created yet. Creating an
|
||||
empty directory at the path above will fix this error.
|
||||
|
||||
If you expected existing migrations to be found, please
|
||||
make sure your repository has been properly configured
|
||||
and the configured path exists.
|
||||
""")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the private repository path relative to the source.
|
||||
"""
|
||||
def source_repo_priv(repo) do
|
||||
config = repo.config()
|
||||
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
|
||||
app = Keyword.fetch!(config, :otp_app)
|
||||
Path.join(Mix.Project.deps_paths()[app] || File.cwd!(), priv)
|
||||
end
|
||||
end
|
||||
115
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.dump.ex
Normal file
115
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.dump.ex
Normal file
@@ -0,0 +1,115 @@
|
||||
defmodule Mix.Tasks.Ecto.Dump do
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Dumps the repository database structure"
|
||||
@default_opts [quiet: false]
|
||||
|
||||
@aliases [
|
||||
d: :dump_path,
|
||||
q: :quiet,
|
||||
r: :repo
|
||||
]
|
||||
|
||||
@switches [
|
||||
dump_path: :string,
|
||||
quiet: :boolean,
|
||||
repo: [:string, :keep],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
prefix: [:string, :keep]
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Dumps the current environment's database structure for the
|
||||
given repository into a structure file.
|
||||
|
||||
The repository must be set under `:ecto_repos` in the
|
||||
current app configuration or given via the `-r` option.
|
||||
|
||||
This task needs some shell utility to be present on the machine
|
||||
running the task.
|
||||
|
||||
Database | Utility needed
|
||||
:--------- | :-------------
|
||||
PostgreSQL | pg_dump
|
||||
MySQL | mysqldump
|
||||
|
||||
## Example
|
||||
|
||||
$ mix ecto.dump
|
||||
|
||||
## Command line options
|
||||
|
||||
* `-r`, `--repo` - the repo to load the structure info from
|
||||
* `-d`, `--dump-path` - the path of the dump file to create
|
||||
* `-q`, `--quiet` - run the command quietly
|
||||
* `--no-compile` - does not compile applications before dumping
|
||||
* `--no-deps-check` - does not check dependencies before dumping
|
||||
* `--prefix` - prefix that will be included in the structure dump.
|
||||
Can include multiple prefixes (ex. `--prefix foo --prefix bar`) with
|
||||
PostgreSQL but not MySQL. When specified, the prefixes will have
|
||||
their definitions dumped along with the data in their migration table.
|
||||
The default behavior is dependent on the adapter for backwards compatibility
|
||||
reasons. For PostgreSQL, the configured database has the definitions dumped
|
||||
from all of its schemas but only the data from the migration table
|
||||
from the `public` schema is included. For MySQL, only the configured
|
||||
database and its migration table are dumped.
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
dump_prefixes =
|
||||
case Keyword.get_values(opts, :prefix) do
|
||||
[_ | _] = prefixes -> prefixes
|
||||
[] -> nil
|
||||
end
|
||||
|
||||
opts =
|
||||
@default_opts
|
||||
|> Keyword.merge(opts)
|
||||
|> Keyword.put(:dump_prefixes, dump_prefixes)
|
||||
|
||||
Enum.each(parse_repo(args), fn repo ->
|
||||
ensure_repo(repo, args)
|
||||
|
||||
ensure_implements(
|
||||
repo.__adapter__(),
|
||||
Ecto.Adapter.Structure,
|
||||
"dump structure for #{inspect(repo)}"
|
||||
)
|
||||
|
||||
migration_repo = repo.config()[:migration_repo] || repo
|
||||
|
||||
for repo <- Enum.uniq([repo, migration_repo]) do
|
||||
config = Keyword.merge(repo.config(), opts)
|
||||
start_time = System.system_time()
|
||||
|
||||
case repo.__adapter__().structure_dump(source_repo_priv(repo), config) do
|
||||
{:ok, location} ->
|
||||
unless opts[:quiet] do
|
||||
elapsed =
|
||||
System.convert_time_unit(System.system_time() - start_time, :native, :microsecond)
|
||||
|
||||
Mix.shell().info(
|
||||
"The structure for #{inspect(repo)} has been dumped to #{location} in #{format_time(elapsed)}"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, term} when is_binary(term) ->
|
||||
Mix.raise("The structure for #{inspect(repo)} couldn't be dumped: #{term}")
|
||||
|
||||
{:error, term} ->
|
||||
Mix.raise("The structure for #{inspect(repo)} couldn't be dumped: #{inspect(term)}")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_time(microsec) when microsec < 1_000, do: "#{microsec} μs"
|
||||
defp format_time(microsec) when microsec < 1_000_000, do: "#{div(microsec, 1_000)} ms"
|
||||
defp format_time(microsec), do: "#{Float.round(microsec / 1_000_000.0)} s"
|
||||
end
|
||||
130
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.gen.migration.ex
Normal file
130
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.gen.migration.ex
Normal file
@@ -0,0 +1,130 @@
|
||||
defmodule Mix.Tasks.Ecto.Gen.Migration do
|
||||
use Mix.Task
|
||||
|
||||
import Macro, only: [camelize: 1, underscore: 1]
|
||||
import Mix.Generator
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Generates a new migration for the repo"
|
||||
|
||||
@aliases [
|
||||
r: :repo
|
||||
]
|
||||
|
||||
@switches [
|
||||
change: :string,
|
||||
repo: [:string, :keep],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
migrations_path: :string
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Generates a migration.
|
||||
|
||||
The repository must be set under `:ecto_repos` in the
|
||||
current app configuration or given via the `-r` option.
|
||||
|
||||
## Examples
|
||||
|
||||
$ mix ecto.gen.migration add_posts_table
|
||||
$ mix ecto.gen.migration add_posts_table -r Custom.Repo
|
||||
|
||||
The generated migration filename will be prefixed with the current
|
||||
timestamp in UTC which is used for versioning and ordering.
|
||||
|
||||
By default, the migration will be generated to the
|
||||
"priv/YOUR_REPO/migrations" directory of the current application
|
||||
but it can be configured to be any subdirectory of `priv` by
|
||||
specifying the `:priv` key under the repository configuration.
|
||||
|
||||
This generator will automatically open the generated file if
|
||||
you have `ECTO_EDITOR` set in your environment variable.
|
||||
|
||||
## Command line options
|
||||
|
||||
* `-r`, `--repo` - the repo to generate migration for
|
||||
* `--no-compile` - does not compile applications before running
|
||||
* `--no-deps-check` - does not check dependencies before running
|
||||
* `--migrations-path` - the path to run the migrations from, defaults to `priv/repo/migrations`
|
||||
|
||||
## Configuration
|
||||
|
||||
If the current app configuration specifies a custom migration module
|
||||
the generated migration code will use that rather than the default
|
||||
`Ecto.Migration`:
|
||||
|
||||
config :ecto_sql, migration_module: MyApplication.CustomMigrationModule
|
||||
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
repos = parse_repo(args)
|
||||
|
||||
Enum.map(repos, fn repo ->
|
||||
case OptionParser.parse!(args, strict: @switches, aliases: @aliases) do
|
||||
{opts, [name]} ->
|
||||
ensure_repo(repo, args)
|
||||
path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
|
||||
base_name = "#{underscore(name)}.exs"
|
||||
file = Path.join(path, "#{timestamp()}_#{base_name}")
|
||||
unless File.dir?(path), do: create_directory(path)
|
||||
|
||||
fuzzy_path = Path.join(path, "*_#{base_name}")
|
||||
|
||||
if Path.wildcard(fuzzy_path) != [] do
|
||||
Mix.raise(
|
||||
"migration can't be created, there is already a migration file with name #{name}."
|
||||
)
|
||||
end
|
||||
|
||||
# The :change option may be used by other tasks but not the CLI
|
||||
assigns = [
|
||||
mod: Module.concat([repo, Migrations, camelize(name)]),
|
||||
change: opts[:change]
|
||||
]
|
||||
|
||||
create_file(file, migration_template(assigns))
|
||||
|
||||
if open?(file) and Mix.shell().yes?("Do you want to run this migration?") do
|
||||
Mix.Task.run("ecto.migrate", ["-r", inspect(repo), "--migrations-path", path])
|
||||
end
|
||||
|
||||
file
|
||||
|
||||
{_, _} ->
|
||||
Mix.raise(
|
||||
"expected ecto.gen.migration to receive the migration file name, " <>
|
||||
"got: #{inspect(Enum.join(args, " "))}"
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp timestamp do
|
||||
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
|
||||
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
|
||||
end
|
||||
|
||||
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
|
||||
defp pad(i), do: to_string(i)
|
||||
|
||||
defp migration_module do
|
||||
case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
|
||||
migration_module when is_atom(migration_module) -> migration_module
|
||||
other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
|
||||
end
|
||||
end
|
||||
|
||||
embed_template(:migration, """
|
||||
defmodule <%= inspect @mod %> do
|
||||
use <%= inspect migration_module() %>
|
||||
|
||||
def change do
|
||||
<%= @change %>
|
||||
end
|
||||
end
|
||||
""")
|
||||
end
|
||||
146
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.load.ex
Normal file
146
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.load.ex
Normal file
@@ -0,0 +1,146 @@
|
||||
defmodule Mix.Tasks.Ecto.Load do
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Loads previously dumped database structure"
|
||||
@default_opts [force: false, quiet: false]
|
||||
|
||||
@aliases [
|
||||
d: :dump_path,
|
||||
f: :force,
|
||||
q: :quiet,
|
||||
r: :repo
|
||||
]
|
||||
|
||||
@switches [
|
||||
dump_path: :string,
|
||||
force: :boolean,
|
||||
quiet: :boolean,
|
||||
repo: [:string, :keep],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
skip_if_loaded: :boolean
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Loads the current environment's database structure for the
|
||||
given repository from a previously dumped structure file.
|
||||
|
||||
The repository must be set under `:ecto_repos` in the
|
||||
current app configuration or given via the `-r` option.
|
||||
|
||||
This task needs some shell utility to be present on the machine
|
||||
running the task.
|
||||
|
||||
Database | Utility needed
|
||||
:--------- | :-------------
|
||||
PostgreSQL | psql
|
||||
MySQL | mysql
|
||||
|
||||
## Example
|
||||
|
||||
$ mix ecto.load
|
||||
|
||||
## Command line options
|
||||
|
||||
* `-r`, `--repo` - the repo to load the structure info into
|
||||
* `-d`, `--dump-path` - the path of the dump file to load from
|
||||
* `-q`, `--quiet` - run the command quietly
|
||||
* `-f`, `--force` - do not ask for confirmation when loading data.
|
||||
Configuration is asked only when `:start_permanent` is set to true
|
||||
(typically in production)
|
||||
* `--no-compile` - does not compile applications before loading
|
||||
* `--no-deps-check` - does not check dependencies before loading
|
||||
* `--skip-if-loaded` - does not load the dump file if the repo has the migrations table up
|
||||
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args, table_exists? \\ &Ecto.Adapters.SQL.table_exists?/3) do
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
opts = Keyword.merge(@default_opts, opts)
|
||||
opts = if opts[:quiet], do: Keyword.put(opts, :log, false), else: opts
|
||||
|
||||
Enum.each(parse_repo(args), fn repo ->
|
||||
ensure_repo(repo, args)
|
||||
|
||||
ensure_implements(
|
||||
repo.__adapter__(),
|
||||
Ecto.Adapter.Structure,
|
||||
"load structure for #{inspect(repo)}"
|
||||
)
|
||||
|
||||
{migration_repo, source} =
|
||||
Ecto.Migration.SchemaMigration.get_repo_and_source(repo, repo.config())
|
||||
|
||||
{:ok, loaded?, _} =
|
||||
Ecto.Migrator.with_repo(migration_repo, table_exists_closure(table_exists?, source, opts))
|
||||
|
||||
for repo <- Enum.uniq([repo, migration_repo]) do
|
||||
cond do
|
||||
loaded? and opts[:skip_if_loaded] ->
|
||||
:ok
|
||||
|
||||
(skip_safety_warnings?() and not loaded?) or opts[:force] or confirm_load(repo, loaded?) ->
|
||||
load_structure(repo, opts)
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp table_exists_closure(fun, source, opts) when is_function(fun, 3) do
|
||||
&fun.(&1, source, opts)
|
||||
end
|
||||
|
||||
defp table_exists_closure(fun, source, _opts) when is_function(fun, 2) do
|
||||
&fun.(&1, source)
|
||||
end
|
||||
|
||||
defp skip_safety_warnings? do
|
||||
Mix.Project.config()[:start_permanent] != true
|
||||
end
|
||||
|
||||
defp confirm_load(repo, false) do
|
||||
Mix.shell().yes?(
|
||||
"Are you sure you want to load a new structure for #{inspect(repo)}? Any existing data in this repo may be lost."
|
||||
)
|
||||
end
|
||||
|
||||
defp confirm_load(repo, true) do
|
||||
Mix.shell().yes?("""
|
||||
It looks like a structure was already loaded for #{inspect(repo)}. Any attempt to load it again might fail.
|
||||
Are you sure you want to proceed?
|
||||
""")
|
||||
end
|
||||
|
||||
defp load_structure(repo, opts) do
|
||||
config = Keyword.merge(repo.config(), opts)
|
||||
start_time = System.system_time()
|
||||
|
||||
case repo.__adapter__().structure_load(source_repo_priv(repo), config) do
|
||||
{:ok, location} ->
|
||||
unless opts[:quiet] do
|
||||
elapsed =
|
||||
System.convert_time_unit(System.system_time() - start_time, :native, :microsecond)
|
||||
|
||||
Mix.shell().info(
|
||||
"The structure for #{inspect(repo)} has been loaded from #{location} in #{format_time(elapsed)}"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, term} when is_binary(term) ->
|
||||
Mix.raise("The structure for #{inspect(repo)} couldn't be loaded: #{term}")
|
||||
|
||||
{:error, term} ->
|
||||
Mix.raise("The structure for #{inspect(repo)} couldn't be loaded: #{inspect(term)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp format_time(microsec) when microsec < 1_000, do: "#{microsec} μs"
|
||||
defp format_time(microsec) when microsec < 1_000_000, do: "#{div(microsec, 1_000)} ms"
|
||||
defp format_time(microsec), do: "#{Float.round(microsec / 1_000_000.0)} s"
|
||||
end
|
||||
162
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.migrate.ex
Normal file
162
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.migrate.ex
Normal file
@@ -0,0 +1,162 @@
|
||||
defmodule Mix.Tasks.Ecto.Migrate do
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Runs the repository migrations"
|
||||
|
||||
@aliases [
|
||||
n: :step,
|
||||
r: :repo
|
||||
]
|
||||
|
||||
@switches [
|
||||
all: :boolean,
|
||||
step: :integer,
|
||||
to: :integer,
|
||||
to_exclusive: :integer,
|
||||
quiet: :boolean,
|
||||
prefix: :string,
|
||||
pool_size: :integer,
|
||||
log_level: :string,
|
||||
log_migrations_sql: :boolean,
|
||||
log_migrator_sql: :boolean,
|
||||
strict_version_order: :boolean,
|
||||
repo: [:keep, :string],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
migrations_path: :keep
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Runs the pending migrations for the given repository.
|
||||
|
||||
Migrations are expected at "priv/YOUR_REPO/migrations" directory
|
||||
of the current application, where "YOUR_REPO" is the last segment
|
||||
in your repository name. For example, the repository `MyApp.Repo`
|
||||
will use "priv/repo/migrations". The repository `Whatever.MyRepo`
|
||||
will use "priv/my_repo/migrations".
|
||||
|
||||
You can configure a repository to use another directory by specifying
|
||||
the `:priv` key under the repository configuration. The "migrations"
|
||||
part will be automatically appended to it. For instance, to use
|
||||
"priv/custom_repo/migrations":
|
||||
|
||||
config :my_app, MyApp.Repo, priv: "priv/custom_repo"
|
||||
|
||||
This task runs all pending migrations by default. To migrate up to a
|
||||
specific version number, supply `--to version_number`. To migrate a
|
||||
specific number of times, use `--step n`.
|
||||
|
||||
The repositories to migrate are the ones specified under the
|
||||
`:ecto_repos` option in the current app configuration. However,
|
||||
if the `-r` option is given, it replaces the `:ecto_repos` config.
|
||||
|
||||
Since Ecto tasks can only be executed once, if you need to migrate
|
||||
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
|
||||
flag multiple times.
|
||||
|
||||
If a repository has not yet been started, one will be started outside
|
||||
your application supervision tree and shutdown afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
$ mix ecto.migrate
|
||||
$ mix ecto.migrate -r Custom.Repo
|
||||
|
||||
$ mix ecto.migrate -n 3
|
||||
$ mix ecto.migrate --step 3
|
||||
|
||||
$ mix ecto.migrate --to 20080906120000
|
||||
|
||||
## Command line options
|
||||
|
||||
* `--all` - run all pending migrations
|
||||
|
||||
* `--log-migrations-sql` - log SQL generated by migration commands
|
||||
|
||||
* `--log-migrator-sql` - log SQL generated by the migrator, such as
|
||||
transactions, table locks, etc
|
||||
|
||||
* `--log-level` (since v3.11.0) - the level to set for `Logger`. This task
|
||||
does not start your application, so whatever level you have configured in
|
||||
your config files will not be used. If this is not provided, no level
|
||||
will be set, so that if you set it yourself before calling this task
|
||||
then this won't interfere. Can be any of the `t:Logger.level/0` levels
|
||||
|
||||
* `--migrations-path` - the path to load the migrations from, defaults to
|
||||
`"priv/repo/migrations"`. This option may be given multiple times in which
|
||||
case the migrations are loaded from all the given directories and sorted
|
||||
as if they were in the same one
|
||||
|
||||
* `--no-compile` - does not compile applications before migrating
|
||||
|
||||
* `--no-deps-check` - does not check dependencies before migrating
|
||||
|
||||
* `--pool-size` - the pool size if the repository is started
|
||||
only for the task (defaults to 2)
|
||||
|
||||
* `--prefix` - the prefix to run migrations on
|
||||
|
||||
* `--quiet` - do not log migration commands
|
||||
|
||||
* `-r`, `--repo` - the repo to migrate
|
||||
|
||||
* `--step`, `-n` - run n number of pending migrations
|
||||
|
||||
* `--strict-version-order` - abort when applying a migration with old
|
||||
timestamp (otherwise it emits a warning)
|
||||
|
||||
* `--to` - run all migrations up to and including version
|
||||
|
||||
* `--to-exclusive` - run all migrations up to and excluding version
|
||||
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args, migrator \\ &Ecto.Migrator.run/4) do
|
||||
repos = parse_repo(args)
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
opts =
|
||||
if opts[:to] || opts[:to_exclusive] || opts[:step] || opts[:all],
|
||||
do: opts,
|
||||
else: Keyword.put(opts, :all, true)
|
||||
|
||||
opts =
|
||||
if opts[:quiet],
|
||||
do: Keyword.merge(opts, log: false, log_migrations_sql: false, log_migrator_sql: false),
|
||||
else: opts
|
||||
|
||||
if log_level = opts[:log_level] do
|
||||
Logger.configure(level: String.to_existing_atom(log_level))
|
||||
end
|
||||
|
||||
# Start ecto_sql explicitly before as we don't need
|
||||
# to restart those apps if migrated.
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
|
||||
for repo <- repos do
|
||||
ensure_repo(repo, args)
|
||||
paths = ensure_migrations_paths(repo, opts)
|
||||
pool = repo.config()[:pool]
|
||||
|
||||
fun =
|
||||
if Code.ensure_loaded?(pool) and function_exported?(pool, :unboxed_run, 2) do
|
||||
&pool.unboxed_run(&1, fn -> migrator.(&1, paths, :up, opts) end)
|
||||
else
|
||||
&migrator.(&1, paths, :up, opts)
|
||||
end
|
||||
|
||||
case Ecto.Migrator.with_repo(repo, fun, [mode: :temporary] ++ opts) do
|
||||
{:ok, _migrated, _apps} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
97
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.migrations.ex
Normal file
97
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.migrations.ex
Normal file
@@ -0,0 +1,97 @@
|
||||
defmodule Mix.Tasks.Ecto.Migrations do
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Displays the repository migration status"
|
||||
|
||||
@aliases [
|
||||
r: :repo
|
||||
]
|
||||
|
||||
@switches [
|
||||
repo: [:keep, :string],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
migrations_path: :keep,
|
||||
prefix: :string
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Displays the up / down migration status for the given repository.
|
||||
|
||||
The repository must be set under `:ecto_repos` in the
|
||||
current app configuration or given via the `-r` option.
|
||||
|
||||
By default, migrations are expected at "priv/YOUR_REPO/migrations"
|
||||
directory of the current application but it can be configured
|
||||
by specifying the `:priv` key under the repository configuration.
|
||||
|
||||
If the repository has not been started yet, one will be
|
||||
started outside our application supervision tree and shutdown
|
||||
afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
$ mix ecto.migrations
|
||||
$ mix ecto.migrations -r Custom.Repo
|
||||
|
||||
## Command line options
|
||||
|
||||
* `--migrations-path` - the path to load the migrations from, defaults to
|
||||
`"priv/repo/migrations"`. This option may be given multiple times in which
|
||||
case the migrations are loaded from all the given directories and sorted as
|
||||
if they were in the same one.
|
||||
|
||||
Note, if you have previously run migrations from paths `a/` and `b/`, and now
|
||||
run `mix ecto.migrations --migrations-path a/` (omitting path `b/`), the
|
||||
migrations from the path `b/` will be shown in the output as `** FILE NOT FOUND **`.
|
||||
|
||||
* `--no-compile` - does not compile applications before running
|
||||
|
||||
* `--no-deps-check` - does not check dependencies before running
|
||||
|
||||
* `--prefix` - the prefix to check migrations on
|
||||
|
||||
* `-r`, `--repo` - the repo to obtain the status for
|
||||
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args, migrations \\ &Ecto.Migrator.migrations/3, puts \\ &IO.puts/1) do
|
||||
repos = parse_repo(args)
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
for repo <- repos do
|
||||
ensure_repo(repo, args)
|
||||
paths = ensure_migrations_paths(repo, opts)
|
||||
|
||||
case Ecto.Migrator.with_repo(repo, &migrations.(&1, paths, opts), mode: :temporary) do
|
||||
{:ok, repo_status, _} ->
|
||||
puts.(
|
||||
"""
|
||||
|
||||
Repo: #{inspect(repo)}
|
||||
|
||||
Status Migration ID Migration Name
|
||||
--------------------------------------------------
|
||||
""" <>
|
||||
Enum.map_join(repo_status, "\n", fn {status, number, description} ->
|
||||
" #{format(status, 10)}#{format(number, 16)}#{description}"
|
||||
end) <> "\n"
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp format(content, pad) do
|
||||
content
|
||||
|> to_string
|
||||
|> String.pad_trailing(pad)
|
||||
end
|
||||
end
|
||||
158
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.rollback.ex
Normal file
158
phoenix/deps/ecto_sql/lib/mix/tasks/ecto.rollback.ex
Normal file
@@ -0,0 +1,158 @@
|
||||
defmodule Mix.Tasks.Ecto.Rollback do
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
import Mix.EctoSQL
|
||||
|
||||
@shortdoc "Rolls back the repository migrations"
|
||||
|
||||
@aliases [
|
||||
r: :repo,
|
||||
n: :step
|
||||
]
|
||||
|
||||
@switches [
|
||||
all: :boolean,
|
||||
step: :integer,
|
||||
to: :integer,
|
||||
to_exclusive: :integer,
|
||||
quiet: :boolean,
|
||||
prefix: :string,
|
||||
pool_size: :integer,
|
||||
log_level: :string,
|
||||
log_migrations_sql: :boolean,
|
||||
log_migrator_sql: :boolean,
|
||||
repo: [:keep, :string],
|
||||
no_compile: :boolean,
|
||||
no_deps_check: :boolean,
|
||||
migrations_path: :keep
|
||||
]
|
||||
|
||||
@moduledoc """
|
||||
Reverts applied migrations in the given repository.
|
||||
|
||||
Migrations are expected at "priv/YOUR_REPO/migrations" directory
|
||||
of the current application, where "YOUR_REPO" is the last segment
|
||||
in your repository name. For example, the repository `MyApp.Repo`
|
||||
will use "priv/repo/migrations". The repository `Whatever.MyRepo`
|
||||
will use "priv/my_repo/migrations".
|
||||
|
||||
You can configure a repository to use another directory by specifying
|
||||
the `:priv` key under the repository configuration. The "migrations"
|
||||
part will be automatically appended to it. For instance, to use
|
||||
"priv/custom_repo/migrations":
|
||||
|
||||
config :my_app, MyApp.Repo, priv: "priv/custom_repo"
|
||||
|
||||
This task rolls back the last applied migration by default. To roll
|
||||
back to a version number, supply `--to version_number`. To roll
|
||||
back a specific number of times, use `--step n`. To undo all applied
|
||||
migrations, provide `--all`.
|
||||
|
||||
The repositories to rollback are the ones specified under the
|
||||
`:ecto_repos` option in the current app configuration. However,
|
||||
if the `-r` option is given, it replaces the `:ecto_repos` config.
|
||||
|
||||
If a repository has not yet been started, one will be started outside
|
||||
your application supervision tree and shutdown afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
$ mix ecto.rollback
|
||||
$ mix ecto.rollback -r Custom.Repo
|
||||
|
||||
$ mix ecto.rollback -n 3
|
||||
$ mix ecto.rollback --step 3
|
||||
|
||||
$ mix ecto.rollback --to 20080906120000
|
||||
|
||||
## Command line options
|
||||
|
||||
* `--all` - run all pending migrations
|
||||
|
||||
* `--log-migrations-sql` - log SQL generated by migration commands
|
||||
|
||||
* `--log-migrator-sql` - log SQL generated by the migrator, such as
|
||||
transactions, table locks, etc
|
||||
|
||||
* `--log-level` (since v3.11.0) - the level to set for `Logger`. This task
|
||||
does not start your application, so whatever level you have configured in
|
||||
your config files will not be used. If this is not provided, no level
|
||||
will be set, so that if you set it yourself before calling this task
|
||||
then this won't interfere. Can be any of the `t:Logger.level/0` levels
|
||||
|
||||
* `--migrations-path` - the path to load the migrations from, defaults to
|
||||
`"priv/repo/migrations"`. This option may be given multiple times in which
|
||||
case the migrations are loaded from all the given directories and sorted
|
||||
as if they were in the same one
|
||||
|
||||
* `--no-compile` - does not compile applications before migrating
|
||||
|
||||
* `--no-deps-check` - does not check dependencies before migrating
|
||||
|
||||
* `--pool-size` - the pool size if the repository is started
|
||||
only for the task (defaults to 2)
|
||||
|
||||
* `--prefix` - the prefix to run migrations on
|
||||
|
||||
* `--quiet` - do not log migration commands
|
||||
|
||||
* `-r`, `--repo` - the repo to migrate
|
||||
|
||||
* `--step`, `-n` - revert n migrations
|
||||
|
||||
* `--strict-version-order` - abort when applying a migration with old
|
||||
timestamp (otherwise it emits a warning)
|
||||
|
||||
* `--to` - revert all migrations down to and including version
|
||||
|
||||
* `--to-exclusive` - revert all migrations down to and excluding version
|
||||
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def run(args, migrator \\ &Ecto.Migrator.run/4) do
|
||||
repos = parse_repo(args)
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
opts =
|
||||
if opts[:to] || opts[:to_exclusive] || opts[:step] || opts[:all],
|
||||
do: opts,
|
||||
else: Keyword.put(opts, :step, 1)
|
||||
|
||||
opts =
|
||||
if opts[:quiet],
|
||||
do: Keyword.merge(opts, log: false, log_migrations_sql: false, log_migrator_sql: false),
|
||||
else: opts
|
||||
|
||||
if log_level = opts[:log_level] do
|
||||
Logger.configure(level: String.to_existing_atom(log_level))
|
||||
end
|
||||
|
||||
# Start ecto_sql explicitly before as we don't need
|
||||
# to restart those apps if migrated.
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
|
||||
for repo <- repos do
|
||||
ensure_repo(repo, args)
|
||||
paths = ensure_migrations_paths(repo, opts)
|
||||
pool = repo.config()[:pool]
|
||||
|
||||
fun =
|
||||
if Code.ensure_loaded?(pool) and function_exported?(pool, :unboxed_run, 2) do
|
||||
&pool.unboxed_run(&1, fn -> migrator.(&1, paths, :down, opts) end)
|
||||
else
|
||||
&migrator.(&1, paths, :down, opts)
|
||||
end
|
||||
|
||||
case Ecto.Migrator.with_repo(repo, fun, [mode: :temporary] ++ opts) do
|
||||
{:ok, _migrated, _apps} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
207
phoenix/deps/ecto_sql/mix.exs
Normal file
207
phoenix/deps/ecto_sql/mix.exs
Normal file
@@ -0,0 +1,207 @@
|
||||
defmodule EctoSQL.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/elixir-ecto/ecto_sql"
|
||||
@version "3.13.5"
|
||||
@adapters ~w(pg myxql tds)
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :ecto_sql,
|
||||
version: @version,
|
||||
elixir: "~> 1.14",
|
||||
deps: deps(),
|
||||
test_paths: test_paths(System.get_env("ECTO_ADAPTER")),
|
||||
xref: [
|
||||
exclude: [
|
||||
MyXQL,
|
||||
Ecto.Adapters.MyXQL.Connection,
|
||||
Postgrex,
|
||||
Ecto.Adapters.Postgres.Connection,
|
||||
Tds,
|
||||
Tds.Ecto.UUID,
|
||||
Ecto.Adapters.Tds.Connection
|
||||
]
|
||||
],
|
||||
|
||||
# Custom testing
|
||||
aliases: [
|
||||
"test.all": ["test", "test.adapters", "test.as_a_dep"],
|
||||
"test.adapters": &test_adapters/1,
|
||||
"test.as_a_dep": &test_as_a_dep/1
|
||||
],
|
||||
preferred_cli_env: ["test.all": :test, "test.adapters": :test],
|
||||
|
||||
# Hex
|
||||
description: "SQL-based adapters for Ecto and database migrations",
|
||||
package: package(),
|
||||
|
||||
# Docs
|
||||
name: "Ecto SQL",
|
||||
docs: docs()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :eex],
|
||||
env: [postgres_map_type: "jsonb"],
|
||||
mod: {Ecto.Adapters.SQL.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
ecto_dep(),
|
||||
{:telemetry, "~> 0.4.0 or ~> 1.0"},
|
||||
|
||||
# Drivers
|
||||
{:db_connection, "~> 2.5 or ~> 2.4.1"},
|
||||
postgrex_dep(),
|
||||
myxql_dep(),
|
||||
tds_dep(),
|
||||
|
||||
# Bring something in for JSON during tests
|
||||
{:jason, ">= 0.0.0", only: [:test, :docs]},
|
||||
|
||||
# Docs
|
||||
{:ex_doc, "~> 0.21", only: :docs},
|
||||
|
||||
# Benchmarks
|
||||
{:benchee, "~> 1.0", only: :bench}
|
||||
]
|
||||
end
|
||||
|
||||
defp ecto_dep do
|
||||
if path = System.get_env("ECTO_PATH") do
|
||||
{:ecto, path: path}
|
||||
else
|
||||
{:ecto, "~> 3.13.0"}
|
||||
end
|
||||
end
|
||||
|
||||
defp postgrex_dep do
|
||||
if path = System.get_env("POSTGREX_PATH") do
|
||||
{:postgrex, path: path}
|
||||
else
|
||||
{:postgrex, "~> 0.19 or ~> 1.0", optional: true}
|
||||
end
|
||||
end
|
||||
|
||||
defp myxql_dep do
|
||||
if path = System.get_env("MYXQL_PATH") do
|
||||
{:myxql, path: path}
|
||||
else
|
||||
{:myxql, "~> 0.7", optional: true}
|
||||
end
|
||||
end
|
||||
|
||||
defp tds_dep do
|
||||
if path = System.get_env("TDS_PATH") do
|
||||
{:tds, path: path}
|
||||
else
|
||||
{:tds, "~> 2.1.1 or ~> 2.2", optional: true}
|
||||
end
|
||||
end
|
||||
|
||||
defp test_paths(adapter) when adapter in @adapters, do: ["integration_test/#{adapter}"]
|
||||
defp test_paths(nil), do: ["test"]
|
||||
defp test_paths(other), do: raise("unknown adapter #{inspect(other)}")
|
||||
|
||||
defp package do
|
||||
[
|
||||
maintainers: ["Eric Meadows-Jönsson", "José Valim", "James Fish", "Michał Muskała"],
|
||||
licenses: ["Apache-2.0"],
|
||||
links: %{"GitHub" => @source_url},
|
||||
files:
|
||||
~w(.formatter.exs mix.exs README.md CHANGELOG.md lib) ++
|
||||
~w(integration_test/sql integration_test/support)
|
||||
]
|
||||
end
|
||||
|
||||
defp test_as_a_dep(args) do
|
||||
IO.puts("==> Compiling ecto_sql from a dependency")
|
||||
File.rm_rf!("tmp/as_a_dep")
|
||||
File.mkdir_p!("tmp/as_a_dep")
|
||||
|
||||
File.cd!("tmp/as_a_dep", fn ->
|
||||
File.write!("mix.exs", """
|
||||
defmodule DepsOnEctoSQL.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :deps_on_ecto_sql,
|
||||
version: "0.0.1",
|
||||
deps: [{:ecto_sql, path: "../.."}]
|
||||
]
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
mix_cmd_with_status_check(["do", "deps.get,", "compile", "--force" | args])
|
||||
end)
|
||||
end
|
||||
|
||||
defp test_adapters(args) do
|
||||
for adapter <- @adapters, do: env_run(adapter, args)
|
||||
end
|
||||
|
||||
defp env_run(adapter, args) do
|
||||
IO.puts("==> Running tests for ECTO_ADAPTER=#{adapter} mix test")
|
||||
|
||||
mix_cmd_with_status_check(
|
||||
["test", ansi_option() | args],
|
||||
env: [{"ECTO_ADAPTER", adapter}]
|
||||
)
|
||||
end
|
||||
|
||||
defp ansi_option do
|
||||
if IO.ANSI.enabled?(), do: "--color", else: "--no-color"
|
||||
end
|
||||
|
||||
defp mix_cmd_with_status_check(args, opts \\ []) do
|
||||
{_, res} = System.cmd("mix", args, [into: IO.binstream(:stdio, :line)] ++ opts)
|
||||
|
||||
if res > 0 do
|
||||
System.at_exit(fn _ -> exit({:shutdown, 1}) end)
|
||||
end
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
main: "Ecto.Adapters.SQL",
|
||||
source_ref: "v#{@version}",
|
||||
canonical: "http://hexdocs.pm/ecto_sql",
|
||||
source_url: @source_url,
|
||||
extras: ["CHANGELOG.md"],
|
||||
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
|
||||
groups_for_modules: [
|
||||
# Ecto.Adapters.SQL,
|
||||
# Ecto.Adapters.SQL.Sandbox,
|
||||
# Ecto.Migration,
|
||||
# Ecto.Migrator,
|
||||
|
||||
"Built-in adapters": [
|
||||
Ecto.Adapters.MyXQL,
|
||||
Ecto.Adapters.Tds,
|
||||
Ecto.Adapters.Postgres
|
||||
],
|
||||
"TDS Types": [
|
||||
Tds.Ecto.UUID,
|
||||
Tds.Ecto.VarChar
|
||||
],
|
||||
"Adapter specification": [
|
||||
Ecto.Adapter.Migration,
|
||||
Ecto.Adapter.Structure,
|
||||
Ecto.Adapters.SQL.Connection,
|
||||
Ecto.Migration.Command,
|
||||
Ecto.Migration.Constraint,
|
||||
Ecto.Migration.Index,
|
||||
Ecto.Migration.Reference,
|
||||
Ecto.Migration.Table
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user