mirror of
https://github.com/ash-project/ash_sqlite.git
synced 2024-09-19 21:03:01 +12:00
improvement: support Ash 3.0, leverage ash_sql
package
This commit is contained in:
parent
52a5051398
commit
c12be48a5b
63 changed files with 1469 additions and 4778 deletions
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Proposal
|
name: Proposal
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ''
|
title: ""
|
||||||
labels: enhancement, needs review
|
labels: enhancement, needs review
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
@ -29,7 +28,7 @@ For example
|
||||||
Or
|
Or
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
Api.read(:resource, bar: 10) # <- Adding `bar` here would cause <x>
|
Ash.read(:resource, bar: 10) # <- Adding `bar` here would cause <x>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
|
@ -27,7 +27,7 @@ Then, configure each of your `Ash.Resource` resources by adding `use Ash.Resourc
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.SomeResource do
|
defmodule MyApp.SomeResource do
|
||||||
use Ash.Resource, data_layer: AshSqlite.DataLayer
|
use Ash.Resource, domain: MyDomain, data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
repo MyApp.Repo
|
repo MyApp.Repo
|
||||||
|
|
|
@ -15,8 +15,8 @@ if Mix.env() == :dev do
|
||||||
end
|
end
|
||||||
|
|
||||||
if Mix.env() == :test do
|
if Mix.env() == :test do
|
||||||
config :ash, :validate_api_resource_inclusion?, false
|
config :ash, :validate_domain_resource_inclusion?, false
|
||||||
config :ash, :validate_api_config_inclusion?, false
|
config :ash, :validate_domain_config_inclusion?, false
|
||||||
|
|
||||||
config :ash_sqlite, AshSqlite.TestRepo,
|
config :ash_sqlite, AshSqlite.TestRepo,
|
||||||
database: Path.join(__DIR__, "../test/test.db"),
|
database: Path.join(__DIR__, "../test/test.db"),
|
||||||
|
@ -27,8 +27,8 @@ if Mix.env() == :test do
|
||||||
|
|
||||||
config :ash_sqlite,
|
config :ash_sqlite,
|
||||||
ecto_repos: [AshSqlite.TestRepo],
|
ecto_repos: [AshSqlite.TestRepo],
|
||||||
ash_apis: [
|
ash_domains: [
|
||||||
AshSqlite.Test.Api
|
AshSqlite.Test.Domain
|
||||||
]
|
]
|
||||||
|
|
||||||
config :logger, level: :warning
|
config :logger, level: :warning
|
||||||
|
|
File diff suppressed because it is too large
Load diff
280
documentation/dsls/DSL:-AshSqlite.DataLayer.md
Normal file
280
documentation/dsls/DSL:-AshSqlite.DataLayer.md
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
<!--
|
||||||
|
This file was generated by Spark. Do not edit it by hand.
|
||||||
|
-->
|
||||||
|
# DSL: AshSqlite.DataLayer
|
||||||
|
|
||||||
|
A sqlite data layer that leverages Ecto's sqlite capabilities.
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite
|
||||||
|
Sqlite data layer configuration
|
||||||
|
|
||||||
|
|
||||||
|
### Nested DSLs
|
||||||
|
* [custom_indexes](#sqlite-custom_indexes)
|
||||||
|
* index
|
||||||
|
* [custom_statements](#sqlite-custom_statements)
|
||||||
|
* statement
|
||||||
|
* [references](#sqlite-references)
|
||||||
|
* reference
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
sqlite do
|
||||||
|
repo MyApp.Repo
|
||||||
|
table "organizations"
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`repo`](#sqlite-repo){: #sqlite-repo .spark-required} | `atom` | | The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more |
|
||||||
|
| [`migrate?`](#sqlite-migrate?){: #sqlite-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` |
|
||||||
|
| [`migration_types`](#sqlite-migration_types){: #sqlite-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. |
|
||||||
|
| [`migration_defaults`](#sqlite-migration_defaults){: #sqlite-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. |
|
||||||
|
| [`base_filter_sql`](#sqlite-base_filter_sql){: #sqlite-base_filter_sql } | `String.t` | | A raw sql version of the base_filter, e.g `representative = true`. Required if trying to create a unique constraint on a resource with a base_filter |
|
||||||
|
| [`skip_unique_indexes`](#sqlite-skip_unique_indexes){: #sqlite-skip_unique_indexes } | `atom \| list(atom)` | `false` | Skip generating unique indexes when generating migrations |
|
||||||
|
| [`unique_index_names`](#sqlite-unique_index_names){: #sqlite-unique_index_names } | `list({list(atom), String.t} \| {list(atom), String.t, String.t})` | `[]` | A list of unique index names that could raise errors that are not configured in identities, or an mfa to a function that takes a changeset and returns the list. In the format `{[:affected, :keys], "name_of_constraint"}` or `{[:affected, :keys], "name_of_constraint", "custom error message"}` |
|
||||||
|
| [`exclusion_constraint_names`](#sqlite-exclusion_constraint_names){: #sqlite-exclusion_constraint_names } | `any` | `[]` | A list of exclusion constraint names that could raise errors. Must be in the format `{:affected_key, "name_of_constraint"}` or `{:affected_key, "name_of_constraint", "custom error message"}` |
|
||||||
|
| [`identity_index_names`](#sqlite-identity_index_names){: #sqlite-identity_index_names } | `any` | `[]` | A keyword list of identity names to the unique index name that they should use when being managed by the migration generator. |
|
||||||
|
| [`foreign_key_names`](#sqlite-foreign_key_names){: #sqlite-foreign_key_names } | `list({atom, String.t} \| {String.t, String.t})` | `[]` | A list of foreign keys that could raise errors, or an mfa to a function that takes a changeset and returns a list. In the format: `{:key, "name_of_constraint"}` or `{:key, "name_of_constraint", "custom error message"}` |
|
||||||
|
| [`migration_ignore_attributes`](#sqlite-migration_ignore_attributes){: #sqlite-migration_ignore_attributes } | `list(atom)` | `[]` | A list of attributes that will be ignored when generating migrations. |
|
||||||
|
| [`table`](#sqlite-table){: #sqlite-table } | `String.t` | | The table to store and read the resource from. If this is changed, the migration generator will not remove the old table. |
|
||||||
|
| [`polymorphic?`](#sqlite-polymorphic?){: #sqlite-polymorphic? } | `boolean` | `false` | Declares this resource as polymorphic. See the [polymorphic resources guide](/documentation/topics/polymorphic_resources.md) for more. |
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.custom_indexes
|
||||||
|
A section for configuring indexes to be created by the migration generator.
|
||||||
|
|
||||||
|
In general, prefer to use `identities` for simple unique constraints. This is a tool to allow
|
||||||
|
for declaring more complex indexes.
|
||||||
|
|
||||||
|
|
||||||
|
### Nested DSLs
|
||||||
|
* [index](#sqlite-custom_indexes-index)
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
custom_indexes do
|
||||||
|
index [:column1, :column2], unique: true, where: "thing = TRUE"
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.custom_indexes.index
|
||||||
|
```elixir
|
||||||
|
index fields
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Add an index to be managed by the migration generator.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
index ["column", "column2"], unique: true, where: "thing = TRUE"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`fields`](#sqlite-custom_indexes-index-fields){: #sqlite-custom_indexes-index-fields } | `atom \| String.t \| list(atom \| String.t)` | | The fields to include in the index. |
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`name`](#sqlite-custom_indexes-index-name){: #sqlite-custom_indexes-index-name } | `String.t` | | the name of the index. Defaults to "#{table}_#{column}_index". |
|
||||||
|
| [`unique`](#sqlite-custom_indexes-index-unique){: #sqlite-custom_indexes-index-unique } | `boolean` | `false` | indicates whether the index should be unique. |
|
||||||
|
| [`using`](#sqlite-custom_indexes-index-using){: #sqlite-custom_indexes-index-using } | `String.t` | | configures the index type. |
|
||||||
|
| [`where`](#sqlite-custom_indexes-index-where){: #sqlite-custom_indexes-index-where } | `String.t` | | specify conditions for a partial index. |
|
||||||
|
| [`message`](#sqlite-custom_indexes-index-message){: #sqlite-custom_indexes-index-message } | `String.t` | | A custom message to use for unique indexes that have been violated |
|
||||||
|
| [`include`](#sqlite-custom_indexes-index-include){: #sqlite-custom_indexes-index-include } | `list(String.t)` | | specify fields for a covering index. This is not supported by all databases. For more information on SQLite support, please read the official docs. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Introspection
|
||||||
|
|
||||||
|
Target: `AshSqlite.CustomIndex`
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.custom_statements
|
||||||
|
A section for configuring custom statements to be added to migrations.
|
||||||
|
|
||||||
|
Changing custom statements may require manual intervention, because Ash can't determine what order they should run
|
||||||
|
in (i.e if they depend on table structure that you've added, or vice versa). As such, any `down` statements we run
|
||||||
|
for custom statements happen first, and any `up` statements happen last.
|
||||||
|
|
||||||
|
Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate
|
||||||
|
the old structure down using the previously configured `down` and recreate it.
|
||||||
|
|
||||||
|
This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was
|
||||||
|
generated by the migration generator. As always: read your migrations after generating them!
|
||||||
|
|
||||||
|
|
||||||
|
### Nested DSLs
|
||||||
|
* [statement](#sqlite-custom_statements-statement)
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
custom_statements do
|
||||||
|
# the name is used to detect if you remove or modify the statement
|
||||||
|
statement :pgweb_idx do
|
||||||
|
up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));"
|
||||||
|
down "DROP INDEX pgweb_idx;"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.custom_statements.statement
|
||||||
|
```elixir
|
||||||
|
statement name
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Add a custom statement for migrations.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
statement :pgweb_idx do
|
||||||
|
up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));"
|
||||||
|
down "DROP INDEX pgweb_idx;"
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`name`](#sqlite-custom_statements-statement-name){: #sqlite-custom_statements-statement-name .spark-required} | `atom` | | The name of the statement, must be unique within the resource |
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`up`](#sqlite-custom_statements-statement-up){: #sqlite-custom_statements-statement-up .spark-required} | `String.t` | | How to create the structure of the statement |
|
||||||
|
| [`down`](#sqlite-custom_statements-statement-down){: #sqlite-custom_statements-statement-down .spark-required} | `String.t` | | How to tear down the structure of the statement |
|
||||||
|
| [`code?`](#sqlite-custom_statements-statement-code?){: #sqlite-custom_statements-statement-code? } | `boolean` | `false` | By default, we place the strings inside of ecto migration's `execute/1` function and assume they are sql. Use this option if you want to provide custom elixir code to be placed directly in the migrations |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Introspection
|
||||||
|
|
||||||
|
Target: `AshSqlite.Statement`
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.references
|
||||||
|
A section for configuring the references (foreign keys) in resource migrations.
|
||||||
|
|
||||||
|
This section is only relevant if you are using the migration generator with this resource.
|
||||||
|
Otherwise, it has no effect.
|
||||||
|
|
||||||
|
|
||||||
|
### Nested DSLs
|
||||||
|
* [reference](#sqlite-references-reference)
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
references do
|
||||||
|
reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey"
|
||||||
|
end
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`polymorphic_on_delete`](#sqlite-references-polymorphic_on_delete){: #sqlite-references-polymorphic_on_delete } | `:delete \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_delete behavior of the automatically generated foreign keys to source tables. |
|
||||||
|
| [`polymorphic_on_update`](#sqlite-references-polymorphic_on_update){: #sqlite-references-polymorphic_on_update } | `:update \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables. |
|
||||||
|
| [`polymorphic_name`](#sqlite-references-polymorphic_name){: #sqlite-references-polymorphic_name } | `:update \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## sqlite.references.reference
|
||||||
|
```elixir
|
||||||
|
reference relationship
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Configures the reference for a relationship in resource migrations.
|
||||||
|
|
||||||
|
Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys.
|
||||||
|
In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result
|
||||||
|
in an error, across this resource and any other resources that share a table with this one. For this reason,
|
||||||
|
instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that
|
||||||
|
is the default behavior if *no* relationship anywhere has configured the behavior of that reference.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`relationship`](#sqlite-references-reference-relationship){: #sqlite-references-reference-relationship .spark-required} | `atom` | | The relationship to be configured |
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`ignore?`](#sqlite-references-reference-ignore?){: #sqlite-references-reference-ignore? } | `boolean` | | If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way |
|
||||||
|
| [`on_delete`](#sqlite-references-reference-on_delete){: #sqlite-references-reference-on_delete } | `:delete \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced record of the *destination* resource is deleted. |
|
||||||
|
| [`on_update`](#sqlite-references-reference-on_update){: #sqlite-references-reference-on_update } | `:update \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. |
|
||||||
|
| [`deferrable`](#sqlite-references-reference-deferrable){: #sqlite-references-reference-deferrable } | `false \| true \| :initially` | `false` | Wether or not the constraint is deferrable. This only affects the migration generator. |
|
||||||
|
| [`name`](#sqlite-references-reference-name){: #sqlite-references-reference-name } | `String.t` | | The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Introspection
|
||||||
|
|
||||||
|
Target: `AshSqlite.Reference`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style type="text/css">.spark-required::after { content: "*"; color: red !important; }</style>
|
|
@ -86,17 +86,17 @@ defmodule MyApp.Release do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp repos do
|
defp repos do
|
||||||
apis()
|
domains()
|
||||||
|> Enum.flat_map(fn api ->
|
|> Enum.flat_map(fn domain ->
|
||||||
api
|
domain
|
||||||
|> Ash.Api.Info.resources()
|
|> Ash.Domain.Info.resources()
|
||||||
|> Enum.map(&AshSqlite.repo/1)
|
|> Enum.map(&AshSqlite.repo/1)
|
||||||
end)
|
end)
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apis do
|
defp domains do
|
||||||
Application.fetch_env!(:my_app, :ash_apis)
|
Application.fetch_env!(:my_app, :ash_domains)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_app do
|
defp load_app do
|
||||||
|
|
|
@ -5,6 +5,7 @@ To support leveraging the same resource backed by multiple tables (useful for th
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Reaction do
|
defmodule MyApp.Reaction do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: MyApp.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -24,6 +25,7 @@ Then, in your related resources, you set the table context like so:
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Post do
|
defmodule MyApp.Post do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: MyApp.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
...
|
...
|
||||||
|
@ -37,6 +39,7 @@ end
|
||||||
|
|
||||||
defmodule MyApp.Comment do
|
defmodule MyApp.Comment do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: MyApp.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -136,6 +136,7 @@ Now we can add the data layer to our resources. The basic configuration for a re
|
||||||
# in lib/helpdesk/support/resources/ticket.ex
|
# in lib/helpdesk/support/resources/ticket.ex
|
||||||
|
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: MyApp.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -148,6 +149,7 @@ Now we can add the data layer to our resources. The basic configuration for a re
|
||||||
# in lib/helpdesk/support/resources/representative.ex
|
# in lib/helpdesk/support/resources/representative.ex
|
||||||
|
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: MyApp.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
|
336
lib/aggregate.ex
336
lib/aggregate.ex
|
@ -1,336 +0,0 @@
|
||||||
defmodule AshSqlite.Aggregate do
|
|
||||||
@moduledoc false
|
|
||||||
|
|
||||||
require Ecto.Query
|
|
||||||
|
|
||||||
def add_subquery_aggregate_select(
|
|
||||||
query,
|
|
||||||
relationship_path,
|
|
||||||
%{kind: :first} = aggregate,
|
|
||||||
resource,
|
|
||||||
is_single?
|
|
||||||
) do
|
|
||||||
query = AshSqlite.DataLayer.default_bindings(query, aggregate.resource)
|
|
||||||
|
|
||||||
ref = %Ash.Query.Ref{
|
|
||||||
attribute: aggregate_field(aggregate, resource, relationship_path, query),
|
|
||||||
relationship_path: relationship_path,
|
|
||||||
resource: query.__ash_bindings__.resource
|
|
||||||
}
|
|
||||||
|
|
||||||
type = AshSqlite.Types.parameterized_type(aggregate.type, aggregate.constraints)
|
|
||||||
|
|
||||||
binding =
|
|
||||||
AshSqlite.DataLayer.get_binding(
|
|
||||||
query.__ash_bindings__.resource,
|
|
||||||
relationship_path,
|
|
||||||
query,
|
|
||||||
[:left, :inner, :root]
|
|
||||||
)
|
|
||||||
|
|
||||||
field = AshSqlite.Expr.dynamic_expr(query, ref, query.__ash_bindings__, false)
|
|
||||||
|
|
||||||
sorted =
|
|
||||||
if has_sort?(aggregate.query) do
|
|
||||||
{:ok, sort_expr} =
|
|
||||||
AshSqlite.Sort.sort(
|
|
||||||
query,
|
|
||||||
aggregate.query.sort,
|
|
||||||
Ash.Resource.Info.related(
|
|
||||||
query.__ash_bindings__.resource,
|
|
||||||
relationship_path
|
|
||||||
),
|
|
||||||
relationship_path,
|
|
||||||
binding,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
question_marks = Enum.map(sort_expr, fn _ -> " ? " end)
|
|
||||||
|
|
||||||
{:ok, expr} =
|
|
||||||
AshSqlite.Functions.Fragment.casted_new(
|
|
||||||
["array_agg(? ORDER BY #{question_marks})", field] ++ sort_expr
|
|
||||||
)
|
|
||||||
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, expr, query.__ash_bindings__, false)
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic(
|
|
||||||
[row],
|
|
||||||
fragment("array_agg(?)", ^field)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered = filter_field(sorted, query, aggregate, relationship_path, is_single?)
|
|
||||||
|
|
||||||
value = Ecto.Query.dynamic(fragment("(?)[1]", ^filtered))
|
|
||||||
|
|
||||||
with_default =
|
|
||||||
if aggregate.default_value do
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(coalesce(^value, type(^aggregate.default_value, ^type)))
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic(coalesce(^value, ^aggregate.default_value))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
|
|
||||||
casted =
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(type(^with_default, ^type))
|
|
||||||
else
|
|
||||||
with_default
|
|
||||||
end
|
|
||||||
|
|
||||||
select_or_merge(query, aggregate.name, casted)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_subquery_aggregate_select(
|
|
||||||
query,
|
|
||||||
relationship_path,
|
|
||||||
%{kind: :list} = aggregate,
|
|
||||||
resource,
|
|
||||||
is_single?
|
|
||||||
) do
|
|
||||||
query = AshSqlite.DataLayer.default_bindings(query, aggregate.resource)
|
|
||||||
type = AshSqlite.Types.parameterized_type(aggregate.type, aggregate.constraints)
|
|
||||||
|
|
||||||
binding =
|
|
||||||
AshSqlite.DataLayer.get_binding(
|
|
||||||
query.__ash_bindings__.resource,
|
|
||||||
relationship_path,
|
|
||||||
query,
|
|
||||||
[:left, :inner, :root]
|
|
||||||
)
|
|
||||||
|
|
||||||
ref = %Ash.Query.Ref{
|
|
||||||
attribute: aggregate_field(aggregate, resource, relationship_path, query),
|
|
||||||
relationship_path: relationship_path,
|
|
||||||
resource: query.__ash_bindings__.resource
|
|
||||||
}
|
|
||||||
|
|
||||||
field = AshSqlite.Expr.dynamic_expr(query, ref, query.__ash_bindings__, false)
|
|
||||||
|
|
||||||
sorted =
|
|
||||||
if has_sort?(aggregate.query) do
|
|
||||||
{:ok, sort_expr} =
|
|
||||||
AshSqlite.Sort.sort(
|
|
||||||
query,
|
|
||||||
aggregate.query.sort,
|
|
||||||
Ash.Resource.Info.related(
|
|
||||||
query.__ash_bindings__.resource,
|
|
||||||
relationship_path
|
|
||||||
),
|
|
||||||
relationship_path,
|
|
||||||
binding,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
question_marks = Enum.map(sort_expr, fn _ -> " ? " end)
|
|
||||||
|
|
||||||
distinct =
|
|
||||||
if Map.get(aggregate, :uniq?) do
|
|
||||||
"DISTINCT "
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, expr} =
|
|
||||||
AshSqlite.Functions.Fragment.casted_new(
|
|
||||||
["array_agg(#{distinct}? ORDER BY #{question_marks})", field] ++ sort_expr
|
|
||||||
)
|
|
||||||
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, expr, query.__ash_bindings__, false)
|
|
||||||
else
|
|
||||||
if Map.get(aggregate, :uniq?) do
|
|
||||||
Ecto.Query.dynamic(
|
|
||||||
[row],
|
|
||||||
fragment("array_agg(DISTINCT ?)", ^field)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic(
|
|
||||||
[row],
|
|
||||||
fragment("array_agg(?)", ^field)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered = filter_field(sorted, query, aggregate, relationship_path, is_single?)
|
|
||||||
|
|
||||||
with_default =
|
|
||||||
if aggregate.default_value do
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(coalesce(^filtered, type(^aggregate.default_value, ^type)))
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic(coalesce(^filtered, ^aggregate.default_value))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
filtered
|
|
||||||
end
|
|
||||||
|
|
||||||
cast =
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(type(^with_default, ^type))
|
|
||||||
else
|
|
||||||
with_default
|
|
||||||
end
|
|
||||||
|
|
||||||
select_or_merge(query, aggregate.name, cast)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_subquery_aggregate_select(
|
|
||||||
query,
|
|
||||||
relationship_path,
|
|
||||||
%{kind: kind} = aggregate,
|
|
||||||
resource,
|
|
||||||
is_single?
|
|
||||||
)
|
|
||||||
when kind in [:count, :sum, :avg, :max, :min, :custom] do
|
|
||||||
query = AshSqlite.DataLayer.default_bindings(query, aggregate.resource)
|
|
||||||
|
|
||||||
ref = %Ash.Query.Ref{
|
|
||||||
attribute: aggregate_field(aggregate, resource, relationship_path, query),
|
|
||||||
relationship_path: relationship_path,
|
|
||||||
resource: resource
|
|
||||||
}
|
|
||||||
|
|
||||||
field =
|
|
||||||
if kind == :custom do
|
|
||||||
# we won't use this if its custom so don't try to make one
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, ref, query.__ash_bindings__, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
type = AshSqlite.Types.parameterized_type(aggregate.type, aggregate.constraints)
|
|
||||||
|
|
||||||
binding =
|
|
||||||
AshSqlite.DataLayer.get_binding(
|
|
||||||
query.__ash_bindings__.resource,
|
|
||||||
relationship_path,
|
|
||||||
query,
|
|
||||||
[:left, :inner, :root]
|
|
||||||
)
|
|
||||||
|
|
||||||
field =
|
|
||||||
case kind do
|
|
||||||
:count ->
|
|
||||||
if Map.get(aggregate, :uniq?) do
|
|
||||||
Ecto.Query.dynamic([row], count(^field, :distinct))
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic([row], count(^field))
|
|
||||||
end
|
|
||||||
|
|
||||||
:sum ->
|
|
||||||
Ecto.Query.dynamic([row], sum(^field))
|
|
||||||
|
|
||||||
:avg ->
|
|
||||||
Ecto.Query.dynamic([row], avg(^field))
|
|
||||||
|
|
||||||
:max ->
|
|
||||||
Ecto.Query.dynamic([row], max(^field))
|
|
||||||
|
|
||||||
:min ->
|
|
||||||
Ecto.Query.dynamic([row], min(^field))
|
|
||||||
|
|
||||||
:custom ->
|
|
||||||
{module, opts} = aggregate.implementation
|
|
||||||
|
|
||||||
module.dynamic(opts, binding)
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered = filter_field(field, query, aggregate, relationship_path, is_single?)
|
|
||||||
|
|
||||||
with_default =
|
|
||||||
if aggregate.default_value do
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(coalesce(^filtered, type(^aggregate.default_value, ^type)))
|
|
||||||
else
|
|
||||||
Ecto.Query.dynamic(coalesce(^filtered, ^aggregate.default_value))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
filtered
|
|
||||||
end
|
|
||||||
|
|
||||||
cast =
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(type(^with_default, ^type))
|
|
||||||
else
|
|
||||||
with_default
|
|
||||||
end
|
|
||||||
|
|
||||||
select_or_merge(query, aggregate.name, cast)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp filter_field(field, _query, _aggregate, _relationship_path, true) do
|
|
||||||
field
|
|
||||||
end
|
|
||||||
|
|
||||||
defp filter_field(field, query, aggregate, relationship_path, _is_single?) do
|
|
||||||
if has_filter?(aggregate.query) do
|
|
||||||
filter =
|
|
||||||
Ash.Filter.move_to_relationship_path(
|
|
||||||
aggregate.query.filter,
|
|
||||||
relationship_path
|
|
||||||
)
|
|
||||||
|
|
||||||
expr =
|
|
||||||
AshSqlite.Expr.dynamic_expr(
|
|
||||||
query,
|
|
||||||
filter,
|
|
||||||
query.__ash_bindings__,
|
|
||||||
false,
|
|
||||||
AshSqlite.Types.parameterized_type(aggregate.type, aggregate.constraints)
|
|
||||||
)
|
|
||||||
|
|
||||||
Ecto.Query.dynamic(filter(^field, ^expr))
|
|
||||||
else
|
|
||||||
field
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp has_filter?(nil), do: false
|
|
||||||
defp has_filter?(%{filter: nil}), do: false
|
|
||||||
defp has_filter?(%{filter: %Ash.Filter{expression: nil}}), do: false
|
|
||||||
defp has_filter?(%{filter: %Ash.Filter{}}), do: true
|
|
||||||
defp has_filter?(_), do: false
|
|
||||||
|
|
||||||
defp has_sort?(nil), do: false
|
|
||||||
defp has_sort?(%{sort: nil}), do: false
|
|
||||||
defp has_sort?(%{sort: []}), do: false
|
|
||||||
defp has_sort?(%{sort: _}), do: true
|
|
||||||
defp has_sort?(_), do: false
|
|
||||||
|
|
||||||
defp select_or_merge(query, aggregate_name, casted) do
|
|
||||||
query =
|
|
||||||
if query.select do
|
|
||||||
query
|
|
||||||
else
|
|
||||||
Ecto.Query.select(query, %{})
|
|
||||||
end
|
|
||||||
|
|
||||||
Ecto.Query.select_merge(query, ^%{aggregate_name => casted})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp aggregate_field(aggregate, resource, _relationship_path, _query) do
|
|
||||||
case Ash.Resource.Info.field(
|
|
||||||
resource,
|
|
||||||
aggregate.field || List.first(Ash.Resource.Info.primary_key(resource))
|
|
||||||
) do
|
|
||||||
%Ash.Resource.Calculation{calculation: {module, opts}} = calculation ->
|
|
||||||
{:ok, query_calc} =
|
|
||||||
Ash.Query.Calculation.new(
|
|
||||||
calculation.name,
|
|
||||||
module,
|
|
||||||
opts,
|
|
||||||
calculation.type,
|
|
||||||
Map.get(aggregate, :context, %{})
|
|
||||||
)
|
|
||||||
|
|
||||||
query_calc
|
|
||||||
|
|
||||||
other ->
|
|
||||||
other
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,83 +0,0 @@
|
||||||
defmodule AshSqlite.Calculation do
|
|
||||||
@moduledoc false
|
|
||||||
|
|
||||||
require Ecto.Query
|
|
||||||
|
|
||||||
def add_calculations(query, [], _, _), do: {:ok, query}
|
|
||||||
|
|
||||||
def add_calculations(query, calculations, resource, _source_binding) do
|
|
||||||
query = AshSqlite.DataLayer.default_bindings(query, resource)
|
|
||||||
|
|
||||||
{:ok, query} =
|
|
||||||
AshSqlite.Join.join_all_relationships(
|
|
||||||
query,
|
|
||||||
%Ash.Filter{
|
|
||||||
resource: resource,
|
|
||||||
expression: Enum.map(calculations, &elem(&1, 1))
|
|
||||||
},
|
|
||||||
left_only?: true
|
|
||||||
)
|
|
||||||
|
|
||||||
query =
|
|
||||||
if query.select do
|
|
||||||
query
|
|
||||||
else
|
|
||||||
Ecto.Query.select_merge(query, %{})
|
|
||||||
end
|
|
||||||
|
|
||||||
dynamics =
|
|
||||||
Enum.map(calculations, fn {calculation, expression} ->
|
|
||||||
type =
|
|
||||||
AshSqlite.Types.parameterized_type(
|
|
||||||
calculation.type,
|
|
||||||
Map.get(calculation, :constraints, [])
|
|
||||||
)
|
|
||||||
|
|
||||||
expr =
|
|
||||||
AshSqlite.Expr.dynamic_expr(
|
|
||||||
query,
|
|
||||||
expression,
|
|
||||||
query.__ash_bindings__,
|
|
||||||
false,
|
|
||||||
type
|
|
||||||
)
|
|
||||||
|
|
||||||
expr =
|
|
||||||
if type do
|
|
||||||
Ecto.Query.dynamic(type(^expr, ^type))
|
|
||||||
else
|
|
||||||
expr
|
|
||||||
end
|
|
||||||
|
|
||||||
{calculation.load, calculation.name, expr}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, add_calculation_selects(query, dynamics)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp add_calculation_selects(query, dynamics) do
|
|
||||||
{in_calculations, in_body} =
|
|
||||||
Enum.split_with(dynamics, fn {load, _name, _dynamic} -> is_nil(load) end)
|
|
||||||
|
|
||||||
calcs =
|
|
||||||
in_body
|
|
||||||
|> Map.new(fn {load, _, dynamic} ->
|
|
||||||
{load, dynamic}
|
|
||||||
end)
|
|
||||||
|
|
||||||
calcs =
|
|
||||||
if Enum.empty?(in_calculations) do
|
|
||||||
calcs
|
|
||||||
else
|
|
||||||
Map.put(
|
|
||||||
calcs,
|
|
||||||
:calculations,
|
|
||||||
Map.new(in_calculations, fn {_, name, dynamic} ->
|
|
||||||
{name, dynamic}
|
|
||||||
end)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
Ecto.Query.select_merge(query, ^calcs)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -282,9 +282,6 @@ defmodule AshSqlite.DataLayer do
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
alias Ash.Filter
|
|
||||||
alias Ash.Query.{BooleanExpression, Not}
|
|
||||||
|
|
||||||
@behaviour Ash.DataLayer
|
@behaviour Ash.DataLayer
|
||||||
|
|
||||||
@sections [@sqlite]
|
@sections [@sqlite]
|
||||||
|
@ -380,6 +377,8 @@ defmodule AshSqlite.DataLayer do
|
||||||
def can?(_, {:aggregate_relationship, _}), do: false
|
def can?(_, {:aggregate_relationship, _}), do: false
|
||||||
|
|
||||||
def can?(_, :timeout), do: true
|
def can?(_, :timeout), do: true
|
||||||
|
def can?(_, {:filter_expr, %Ash.Query.Function.StringJoin{}}), do: false
|
||||||
|
def can?(_, {:filter_expr, %Ash.Query.Function.Contains{}}), do: false
|
||||||
def can?(_, {:filter_expr, _}), do: true
|
def can?(_, {:filter_expr, _}), do: true
|
||||||
def can?(_, :nested_expressions), do: true
|
def can?(_, :nested_expressions), do: true
|
||||||
def can?(_, {:query_aggregate, _}), do: true
|
def can?(_, {:query_aggregate, _}), do: true
|
||||||
|
@ -416,7 +415,13 @@ defmodule AshSqlite.DataLayer do
|
||||||
data_layer_query
|
data_layer_query
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, default_bindings(data_layer_query, resource, context)}
|
{:ok,
|
||||||
|
AshSql.Bindings.default_bindings(
|
||||||
|
data_layer_query,
|
||||||
|
resource,
|
||||||
|
AshSqlite.SqlImplementation,
|
||||||
|
context
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -433,10 +438,10 @@ defmodule AshSqlite.DataLayer do
|
||||||
@impl true
|
@impl true
|
||||||
def run_aggregate_query(query, aggregates, resource) do
|
def run_aggregate_query(query, aggregates, resource) do
|
||||||
{exists, aggregates} = Enum.split_with(aggregates, &(&1.kind == :exists))
|
{exists, aggregates} = Enum.split_with(aggregates, &(&1.kind == :exists))
|
||||||
query = default_bindings(query, resource)
|
query = AshSql.Bindings.default_bindings(query, resource, AshSqlite.SqlImplementation)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
if query.distinct || query.limit do
|
if query.limit do
|
||||||
query =
|
query =
|
||||||
query
|
query
|
||||||
|> Ecto.Query.exclude(:select)
|
|> Ecto.Query.exclude(:select)
|
||||||
|
@ -459,12 +464,13 @@ defmodule AshSqlite.DataLayer do
|
||||||
aggregates,
|
aggregates,
|
||||||
query,
|
query,
|
||||||
fn agg, query ->
|
fn agg, query ->
|
||||||
AshSqlite.Aggregate.add_subquery_aggregate_select(
|
AshSql.Aggregate.add_subquery_aggregate_select(
|
||||||
query,
|
query,
|
||||||
agg.relationship_path |> Enum.drop(1),
|
agg.relationship_path |> Enum.drop(1),
|
||||||
agg,
|
agg,
|
||||||
resource,
|
resource,
|
||||||
true
|
true,
|
||||||
|
Ash.Resource.Info.relationship(resource, agg.relationship_path |> Enum.at(1))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
@ -505,13 +511,11 @@ defmodule AshSqlite.DataLayer do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def run_query(query, resource) do
|
def run_query(query, resource) do
|
||||||
query = default_bindings(query, resource)
|
|
||||||
|
|
||||||
with_sort_applied =
|
with_sort_applied =
|
||||||
if query.__ash_bindings__[:sort_applied?] do
|
if query.__ash_bindings__[:sort_applied?] do
|
||||||
{:ok, query}
|
{:ok, query}
|
||||||
else
|
else
|
||||||
apply_sort(query, query.__ash_bindings__[:sort], resource)
|
AshSql.Sort.apply_sort(query, query.__ash_bindings__[:sort], resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
case with_sort_applied do
|
case with_sort_applied do
|
||||||
|
@ -568,7 +572,6 @@ defmodule AshSqlite.DataLayer do
|
||||||
@impl true
|
@impl true
|
||||||
def functions(_resource) do
|
def functions(_resource) do
|
||||||
[
|
[
|
||||||
AshSqlite.Functions.Fragment,
|
|
||||||
AshSqlite.Functions.Like,
|
AshSqlite.Functions.Like,
|
||||||
AshSqlite.Functions.ILike
|
AshSqlite.Functions.ILike
|
||||||
]
|
]
|
||||||
|
@ -601,22 +604,22 @@ defmodule AshSqlite.DataLayer do
|
||||||
if options[:upsert?] do
|
if options[:upsert?] do
|
||||||
# Ash groups changesets by atomics before dispatching them to the data layer
|
# Ash groups changesets by atomics before dispatching them to the data layer
|
||||||
# this means that all changesets have the same atomics
|
# this means that all changesets have the same atomics
|
||||||
%{atomics: atomics, filters: filters} = Enum.at(changesets, 0)
|
%{atomics: atomics, filter: filter} = Enum.at(changesets, 0)
|
||||||
|
|
||||||
query = from(row in resource, as: ^0)
|
query = from(row in resource, as: ^0)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
query
|
query
|
||||||
|> default_bindings(resource)
|
|> AshSql.Bindings.default_bindings(resource, AshSqlite.SqlImplementation)
|
||||||
|
|
||||||
upsert_set =
|
upsert_set =
|
||||||
upsert_set(resource, changesets, options)
|
upsert_set(resource, changesets, options)
|
||||||
|
|
||||||
on_conflict =
|
on_conflict =
|
||||||
case query_with_atomics(
|
case AshSql.Atomics.query_with_atomics(
|
||||||
resource,
|
resource,
|
||||||
query,
|
query,
|
||||||
filters,
|
filter,
|
||||||
atomics,
|
atomics,
|
||||||
%{},
|
%{},
|
||||||
upsert_set
|
upsert_set
|
||||||
|
@ -1292,13 +1295,17 @@ defmodule AshSqlite.DataLayer do
|
||||||
|
|
||||||
query =
|
query =
|
||||||
query
|
query
|
||||||
|> default_bindings(resource, changeset.context)
|
|> AshSql.Bindings.default_bindings(
|
||||||
|
resource,
|
||||||
|
AshSqlite.SqlImplementation,
|
||||||
|
changeset.context
|
||||||
|
)
|
||||||
|> Ecto.Query.select(^select)
|
|> Ecto.Query.select(^select)
|
||||||
|
|
||||||
case query_with_atomics(
|
case AshSql.Atomics.query_with_atomics(
|
||||||
resource,
|
resource,
|
||||||
query,
|
query,
|
||||||
ecto_changeset.filters,
|
changeset.filter,
|
||||||
changeset.atomics,
|
changeset.atomics,
|
||||||
ecto_changeset.changes,
|
ecto_changeset.changes,
|
||||||
[]
|
[]
|
||||||
|
@ -1324,7 +1331,7 @@ defmodule AshSqlite.DataLayer do
|
||||||
{:error,
|
{:error,
|
||||||
Ash.Error.Changes.StaleRecord.exception(
|
Ash.Error.Changes.StaleRecord.exception(
|
||||||
resource: resource,
|
resource: resource,
|
||||||
filters: ecto_changeset.filters
|
filters: changeset.filter
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{1, [result]} ->
|
{1, [result]} ->
|
||||||
|
@ -1345,101 +1352,6 @@ defmodule AshSqlite.DataLayer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp query_with_atomics(
|
|
||||||
resource,
|
|
||||||
query,
|
|
||||||
filters,
|
|
||||||
atomics,
|
|
||||||
updating_one_changes,
|
|
||||||
existing_set
|
|
||||||
) do
|
|
||||||
query =
|
|
||||||
Enum.reduce(filters, query, fn {key, value}, query ->
|
|
||||||
from(row in query,
|
|
||||||
where: field(row, ^key) == ^value
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
atomics_result =
|
|
||||||
Enum.reduce_while(atomics, {:ok, query, []}, fn {field, expr}, {:ok, query, set} ->
|
|
||||||
with {:ok, query} <-
|
|
||||||
AshSqlite.Join.join_all_relationships(
|
|
||||||
query,
|
|
||||||
%Ash.Filter{
|
|
||||||
resource: resource,
|
|
||||||
expression: expr
|
|
||||||
},
|
|
||||||
left_only?: true
|
|
||||||
),
|
|
||||||
dynamic <-
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, expr, query.__ash_bindings__) do
|
|
||||||
{:cont, {:ok, query, Keyword.put(set, field, dynamic)}}
|
|
||||||
else
|
|
||||||
other ->
|
|
||||||
{:halt, other}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
case atomics_result do
|
|
||||||
{:ok, query, dynamics} ->
|
|
||||||
{params, set, count} =
|
|
||||||
updating_one_changes
|
|
||||||
|> Map.to_list()
|
|
||||||
|> Enum.reduce({[], [], 0}, fn {key, value}, {params, set, count} ->
|
|
||||||
{[{value, {0, key}} | params], [{key, {:^, [], [count]}} | set], count + 1}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{params, set, _} =
|
|
||||||
Enum.reduce(
|
|
||||||
dynamics ++ existing_set,
|
|
||||||
{params, set, count},
|
|
||||||
fn {key, value}, {params, set, count} ->
|
|
||||||
case AshSqlite.Expr.dynamic_expr(query, value, query.__ash_bindings__) do
|
|
||||||
%Ecto.Query.DynamicExpr{} = dynamic ->
|
|
||||||
result =
|
|
||||||
Ecto.Query.Builder.Dynamic.partially_expand(
|
|
||||||
:select,
|
|
||||||
query,
|
|
||||||
dynamic,
|
|
||||||
params,
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
expr = elem(result, 0)
|
|
||||||
new_params = elem(result, 1)
|
|
||||||
|
|
||||||
new_count =
|
|
||||||
result |> Tuple.to_list() |> List.last()
|
|
||||||
|
|
||||||
{new_params, [{key, expr} | set], new_count}
|
|
||||||
|
|
||||||
other ->
|
|
||||||
{[{other, {0, key}} | params], [{key, {:^, [], [count]}} | set], count + 1}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
case set do
|
|
||||||
[] ->
|
|
||||||
:empty
|
|
||||||
|
|
||||||
set ->
|
|
||||||
{:ok,
|
|
||||||
Map.put(query, :updates, [
|
|
||||||
%Ecto.Query.QueryExpr{
|
|
||||||
# why do I have to reverse the `set`???
|
|
||||||
# it breaks if I don't
|
|
||||||
expr: [set: Enum.reverse(set)],
|
|
||||||
params: Enum.reverse(params)
|
|
||||||
}
|
|
||||||
])}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def destroy(resource, %{data: record} = changeset) do
|
def destroy(resource, %{data: record} = changeset) do
|
||||||
ecto_changeset = ecto_changeset(record, changeset, :delete)
|
ecto_changeset = ecto_changeset(record, changeset, :delete)
|
||||||
|
@ -1470,7 +1382,7 @@ defmodule AshSqlite.DataLayer do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def select(query, select, resource) do
|
def select(query, select, resource) do
|
||||||
query = default_bindings(query, resource)
|
query = AshSql.Bindings.default_bindings(query, resource, AshSqlite.SqlImplementation)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
from(row in query,
|
from(row in query,
|
||||||
|
@ -1478,64 +1390,27 @@ defmodule AshSqlite.DataLayer do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_sort(query, sort, _resource) when sort in [nil, []] do
|
|
||||||
{:ok, query |> set_sort_applied()}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_sort(query, sort, resource) do
|
|
||||||
query
|
|
||||||
|> AshSqlite.Sort.sort(sort, resource, [], 0)
|
|
||||||
|> case do
|
|
||||||
{:ok, query} ->
|
|
||||||
{:ok, query |> set_sort_applied()}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def unwrap_one([thing]), do: thing
|
def unwrap_one([thing]), do: thing
|
||||||
def unwrap_one([]), do: nil
|
def unwrap_one([]), do: nil
|
||||||
def unwrap_one(other), do: other
|
def unwrap_one(other), do: other
|
||||||
|
|
||||||
defp set_sort_applied(query) do
|
|
||||||
Map.update!(query, :__ash_bindings__, &Map.put(&1, :sort_applied?, true))
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def filter(query, filter, resource, opts \\ []) do
|
def filter(query, filter, _resource, opts \\ []) do
|
||||||
query = default_bindings(query, resource)
|
|
||||||
|
|
||||||
query
|
query
|
||||||
|> AshSqlite.Join.join_all_relationships(filter, opts)
|
|> AshSql.Join.join_all_relationships(filter, opts)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, query} ->
|
{:ok, query} ->
|
||||||
{:ok, add_filter_expression(query, filter)}
|
{:ok, AshSql.Filter.add_filter_expression(query, filter)}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
|
||||||
def default_bindings(query, resource, context \\ %{}) do
|
|
||||||
start_bindings = context[:data_layer][:start_bindings_at] || 0
|
|
||||||
|
|
||||||
Map.put_new(query, :__ash_bindings__, %{
|
|
||||||
resource: resource,
|
|
||||||
current: Enum.count(query.joins) + 1 + start_bindings,
|
|
||||||
in_group?: false,
|
|
||||||
calculations: %{},
|
|
||||||
parent_resources: [],
|
|
||||||
context: context,
|
|
||||||
bindings: %{start_bindings => %{path: [], type: :root, source: resource}}
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def add_calculations(query, calculations, resource) do
|
def add_calculations(query, calculations, resource) do
|
||||||
AshSqlite.Calculation.add_calculations(query, calculations, resource, 0)
|
AshSql.Calculation.add_calculations(query, calculations, resource, 0, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
@ -1569,40 +1444,6 @@ defmodule AshSqlite.DataLayer do
|
||||||
|
|
||||||
def get_binding(_, _, _, _, _), do: nil
|
def get_binding(_, _, _, _, _), do: nil
|
||||||
|
|
||||||
defp add_filter_expression(query, filter) do
|
|
||||||
filter
|
|
||||||
|> split_and_statements()
|
|
||||||
|> Enum.reduce(query, fn filter, query ->
|
|
||||||
dynamic = AshSqlite.Expr.dynamic_expr(query, filter, query.__ash_bindings__)
|
|
||||||
|
|
||||||
Ecto.Query.where(query, ^dynamic)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_and_statements(%Filter{expression: expression}) do
|
|
||||||
split_and_statements(expression)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_and_statements(%BooleanExpression{op: :and, left: left, right: right}) do
|
|
||||||
split_and_statements(left) ++ split_and_statements(right)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_and_statements(%Not{expression: %Not{expression: expression}}) do
|
|
||||||
split_and_statements(expression)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_and_statements(%Not{
|
|
||||||
expression: %BooleanExpression{op: :or, left: left, right: right}
|
|
||||||
}) do
|
|
||||||
split_and_statements(%BooleanExpression{
|
|
||||||
op: :and,
|
|
||||||
left: %Not{expression: left},
|
|
||||||
right: %Not{expression: right}
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_and_statements(other), do: [other]
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def add_binding(query, data, additional_bindings \\ 0) do
|
def add_binding(query, data, additional_bindings \\ 0) do
|
||||||
current = query.__ash_bindings__.current
|
current = query.__ash_bindings__.current
|
||||||
|
|
1366
lib/expr.ex
1366
lib/expr.ex
File diff suppressed because it is too large
Load diff
|
@ -1,72 +0,0 @@
|
||||||
defmodule AshSqlite.Functions.Fragment do
|
|
||||||
@moduledoc """
|
|
||||||
A function that maps to ecto's `fragment` function
|
|
||||||
|
|
||||||
https://hexdocs.pm/ecto/Ecto.Query.API.html#fragment/1
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Query.Function, name: :fragment
|
|
||||||
|
|
||||||
def private?, do: true
|
|
||||||
|
|
||||||
# Varargs is special, and should only be used in rare circumstances (like this one)
|
|
||||||
# no type casting or help can be provided for these functions.
|
|
||||||
def args, do: :var_args
|
|
||||||
|
|
||||||
def new([fragment | _]) when not is_binary(fragment) do
|
|
||||||
{:error, "First argument to `fragment` must be a string."}
|
|
||||||
end
|
|
||||||
|
|
||||||
def new([fragment | rest]) do
|
|
||||||
split = split_fragment(fragment)
|
|
||||||
|
|
||||||
if Enum.count(split, &(&1 == :slot)) != length(rest) do
|
|
||||||
{:error,
|
|
||||||
"fragment(...) expects extra arguments in the same amount of question marks in string. " <>
|
|
||||||
"It received #{Enum.count(split, &(&1 == :slot))} extra argument(s) but expected #{length(rest)}"}
|
|
||||||
else
|
|
||||||
{:ok, %__MODULE__{arguments: merge_fragment(split, rest)}}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def casted_new([fragment | _]) when not is_binary(fragment) do
|
|
||||||
{:error, "First argument to `fragment` must be a string."}
|
|
||||||
end
|
|
||||||
|
|
||||||
def casted_new([fragment | rest]) do
|
|
||||||
split = split_fragment(fragment)
|
|
||||||
|
|
||||||
if Enum.count(split, &(&1 == :slot)) != length(rest) do
|
|
||||||
{:error,
|
|
||||||
"fragment(...) expects extra arguments in the same amount of question marks in string. " <>
|
|
||||||
"It received #{Enum.count(split, &(&1 == :slot))} extra argument(s) but expected #{length(rest)}"}
|
|
||||||
else
|
|
||||||
{:ok, %__MODULE__{arguments: merge_fragment(split, rest, :casted_expr)}}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp merge_fragment(expr, args, tag \\ :expr)
|
|
||||||
defp merge_fragment([], [], _tag), do: []
|
|
||||||
|
|
||||||
defp merge_fragment([:slot | rest], [arg | rest_args], tag) do
|
|
||||||
[{tag, arg} | merge_fragment(rest, rest_args, tag)]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp merge_fragment([val | rest], rest_args, tag) do
|
|
||||||
[{:raw, val} | merge_fragment(rest, rest_args, tag)]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_fragment(frag, consumed \\ "")
|
|
||||||
|
|
||||||
defp split_fragment(<<>>, consumed),
|
|
||||||
do: [consumed]
|
|
||||||
|
|
||||||
defp split_fragment(<<??, rest::binary>>, consumed),
|
|
||||||
do: [consumed, :slot | split_fragment(rest, "")]
|
|
||||||
|
|
||||||
defp split_fragment(<<?\\, ??, rest::binary>>, consumed),
|
|
||||||
do: split_fragment(rest, consumed <> <<??>>)
|
|
||||||
|
|
||||||
defp split_fragment(<<first::utf8, rest::binary>>, consumed),
|
|
||||||
do: split_fragment(rest, consumed <> <<first::utf8>>)
|
|
||||||
end
|
|
734
lib/join.ex
734
lib/join.ex
|
@ -1,734 +0,0 @@
|
||||||
defmodule AshSqlite.Join do
|
|
||||||
@moduledoc false
|
|
||||||
import Ecto.Query, only: [from: 2]
|
|
||||||
|
|
||||||
alias Ash.Query.{BooleanExpression, Not, Ref}
|
|
||||||
|
|
||||||
@known_inner_join_operators [
|
|
||||||
Eq,
|
|
||||||
GreaterThan,
|
|
||||||
GreaterThanOrEqual,
|
|
||||||
In,
|
|
||||||
LessThanOrEqual,
|
|
||||||
LessThan,
|
|
||||||
NotEq
|
|
||||||
]
|
|
||||||
|> Enum.map(&Module.concat(Ash.Query.Operator, &1))
|
|
||||||
|
|
||||||
@known_inner_join_functions [
|
|
||||||
Ago,
|
|
||||||
Contains
|
|
||||||
]
|
|
||||||
|> Enum.map(&Module.concat(Ash.Query.Function, &1))
|
|
||||||
|
|
||||||
@known_inner_join_predicates @known_inner_join_functions ++ @known_inner_join_operators
|
|
||||||
|
|
||||||
def join_all_relationships(
|
|
||||||
query,
|
|
||||||
filter,
|
|
||||||
opts \\ [],
|
|
||||||
relationship_paths \\ nil,
|
|
||||||
path \\ [],
|
|
||||||
source \\ nil
|
|
||||||
) do
|
|
||||||
relationship_paths =
|
|
||||||
cond do
|
|
||||||
relationship_paths ->
|
|
||||||
relationship_paths
|
|
||||||
|
|
||||||
opts[:no_this?] ->
|
|
||||||
filter
|
|
||||||
|> Ash.Filter.map(fn
|
|
||||||
%Ash.Query.Parent{} ->
|
|
||||||
nil
|
|
||||||
|
|
||||||
other ->
|
|
||||||
other
|
|
||||||
end)
|
|
||||||
|> Ash.Filter.relationship_paths()
|
|
||||||
|> to_joins(filter)
|
|
||||||
|
|
||||||
true ->
|
|
||||||
filter
|
|
||||||
|> Ash.Filter.relationship_paths()
|
|
||||||
|> to_joins(filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
Enum.reduce_while(relationship_paths, {:ok, query}, fn
|
|
||||||
{_join_type, []}, {:ok, query} ->
|
|
||||||
{:cont, {:ok, query}}
|
|
||||||
|
|
||||||
{join_type, [relationship | rest_rels]}, {:ok, query} ->
|
|
||||||
source = source || relationship.source
|
|
||||||
|
|
||||||
current_path = path ++ [relationship]
|
|
||||||
|
|
||||||
current_join_type = join_type
|
|
||||||
|
|
||||||
look_for_join_types =
|
|
||||||
case join_type do
|
|
||||||
:left ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
:inner ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
other ->
|
|
||||||
[other]
|
|
||||||
end
|
|
||||||
|
|
||||||
case get_binding(source, Enum.map(current_path, & &1.name), query, look_for_join_types) do
|
|
||||||
binding when is_integer(binding) ->
|
|
||||||
case join_all_relationships(
|
|
||||||
query,
|
|
||||||
filter,
|
|
||||||
opts,
|
|
||||||
[{join_type, rest_rels}],
|
|
||||||
current_path,
|
|
||||||
source
|
|
||||||
) do
|
|
||||||
{:ok, query} ->
|
|
||||||
{:cont, {:ok, query}}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:halt, {:error, error}}
|
|
||||||
end
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
case join_relationship(
|
|
||||||
query,
|
|
||||||
relationship,
|
|
||||||
Enum.map(path, & &1.name),
|
|
||||||
current_join_type,
|
|
||||||
source,
|
|
||||||
filter
|
|
||||||
) do
|
|
||||||
{:ok, joined_query} ->
|
|
||||||
joined_query_with_distinct = add_distinct(relationship, join_type, joined_query)
|
|
||||||
|
|
||||||
case join_all_relationships(
|
|
||||||
joined_query_with_distinct,
|
|
||||||
filter,
|
|
||||||
opts,
|
|
||||||
[{join_type, rest_rels}],
|
|
||||||
current_path,
|
|
||||||
source
|
|
||||||
) do
|
|
||||||
{:ok, query} ->
|
|
||||||
{:cont, {:ok, query}}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:halt, {:error, error}}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:halt, {:error, error}}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_joins(paths, filter) do
|
|
||||||
paths
|
|
||||||
|> Enum.map(fn path ->
|
|
||||||
if can_inner_join?(path, filter) do
|
|
||||||
{:inner,
|
|
||||||
AshSqlite.Join.relationship_path_to_relationships(
|
|
||||||
filter.resource,
|
|
||||||
path
|
|
||||||
)}
|
|
||||||
else
|
|
||||||
{:left,
|
|
||||||
AshSqlite.Join.relationship_path_to_relationships(
|
|
||||||
filter.resource,
|
|
||||||
path
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def relationship_path_to_relationships(resource, path, acc \\ [])
|
|
||||||
def relationship_path_to_relationships(_resource, [], acc), do: Enum.reverse(acc)
|
|
||||||
|
|
||||||
def relationship_path_to_relationships(resource, [relationship | rest], acc) do
|
|
||||||
relationship = Ash.Resource.Info.relationship(resource, relationship)
|
|
||||||
|
|
||||||
relationship_path_to_relationships(relationship.destination, rest, [relationship | acc])
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_get_resource_query(
|
|
||||||
resource,
|
|
||||||
relationship,
|
|
||||||
root_query,
|
|
||||||
path \\ [],
|
|
||||||
bindings \\ nil,
|
|
||||||
start_binding \\ nil,
|
|
||||||
is_subquery? \\ true
|
|
||||||
) do
|
|
||||||
resource
|
|
||||||
|> Ash.Query.new(nil, base_filter?: false)
|
|
||||||
|> Ash.Query.set_context(%{data_layer: %{start_bindings_at: start_binding}})
|
|
||||||
|> Ash.Query.set_context((bindings || root_query.__ash_bindings__).context)
|
|
||||||
|> Ash.Query.set_context(relationship.context)
|
|
||||||
|> case do
|
|
||||||
%{valid?: true} = query ->
|
|
||||||
ash_query = query
|
|
||||||
|
|
||||||
initial_query = AshSqlite.DataLayer.resource_to_query(resource, nil)
|
|
||||||
|
|
||||||
case Ash.Query.data_layer_query(query,
|
|
||||||
initial_query: initial_query
|
|
||||||
) do
|
|
||||||
{:ok, query} ->
|
|
||||||
query =
|
|
||||||
query
|
|
||||||
|> do_base_filter(
|
|
||||||
root_query,
|
|
||||||
ash_query,
|
|
||||||
resource,
|
|
||||||
path,
|
|
||||||
bindings
|
|
||||||
)
|
|
||||||
|> do_relationship_filter(
|
|
||||||
relationship,
|
|
||||||
root_query,
|
|
||||||
ash_query,
|
|
||||||
resource,
|
|
||||||
path,
|
|
||||||
bindings,
|
|
||||||
is_subquery?
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
|
|
||||||
query ->
|
|
||||||
{:error, query}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_relationship_filter(query, %{filter: nil}, _, _, _, _, _, _), do: query
|
|
||||||
|
|
||||||
defp do_relationship_filter(
|
|
||||||
query,
|
|
||||||
relationship,
|
|
||||||
root_query,
|
|
||||||
ash_query,
|
|
||||||
resource,
|
|
||||||
path,
|
|
||||||
bindings,
|
|
||||||
is_subquery?
|
|
||||||
) do
|
|
||||||
context =
|
|
||||||
ash_query.context
|
|
||||||
|> Map.update(
|
|
||||||
:parent_stack,
|
|
||||||
[relationship.source],
|
|
||||||
&[&1 | relationship.source]
|
|
||||||
)
|
|
||||||
|> Map.put(:resource, relationship.destination)
|
|
||||||
|
|
||||||
filter =
|
|
||||||
resource
|
|
||||||
|> Ash.Filter.parse!(
|
|
||||||
relationship.filter,
|
|
||||||
%{},
|
|
||||||
context
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok, filter} = Ash.Filter.hydrate_refs(filter, context)
|
|
||||||
|
|
||||||
base_bindings = bindings || query.__ash_bindings__
|
|
||||||
|
|
||||||
parent_binding =
|
|
||||||
case :lists.droplast(path) do
|
|
||||||
[] ->
|
|
||||||
base_bindings.bindings
|
|
||||||
|> Enum.find_value(fn {key, %{type: type}} ->
|
|
||||||
if type == :root do
|
|
||||||
key
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
path ->
|
|
||||||
get_binding(
|
|
||||||
root_query.__ash_bindings__.resource,
|
|
||||||
path,
|
|
||||||
%{query | __ash_bindings__: base_bindings},
|
|
||||||
[
|
|
||||||
:inner,
|
|
||||||
:left
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
parent_bindings = %{
|
|
||||||
base_bindings
|
|
||||||
| resource: relationship.source,
|
|
||||||
calculations: %{},
|
|
||||||
parent_resources: [],
|
|
||||||
context: relationship.context,
|
|
||||||
current: parent_binding + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
parent_bindings =
|
|
||||||
if bindings do
|
|
||||||
Map.put(parent_bindings, :parent_is_parent_as?, !is_subquery?)
|
|
||||||
else
|
|
||||||
parent_bindings
|
|
||||||
|> Map.update!(:bindings, &Map.take(&1, [parent_binding]))
|
|
||||||
end
|
|
||||||
|
|
||||||
has_bindings? = not is_nil(bindings)
|
|
||||||
|
|
||||||
bindings =
|
|
||||||
base_bindings
|
|
||||||
|> Map.put(:parent_bindings, parent_bindings)
|
|
||||||
|> Map.put(:parent_resources, [
|
|
||||||
relationship.source | parent_bindings[:parent_resources] || []
|
|
||||||
])
|
|
||||||
|
|
||||||
dynamic =
|
|
||||||
if has_bindings? do
|
|
||||||
filter =
|
|
||||||
if is_subquery? do
|
|
||||||
Ash.Filter.move_to_relationship_path(filter, path)
|
|
||||||
else
|
|
||||||
filter
|
|
||||||
end
|
|
||||||
|
|
||||||
AshSqlite.Expr.dynamic_expr(root_query, filter, bindings, true)
|
|
||||||
else
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, filter, bindings, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, query} = join_all_relationships(query, filter)
|
|
||||||
from(row in query, where: ^dynamic)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_base_filter(query, root_query, ash_query, resource, path, bindings) do
|
|
||||||
case Ash.Resource.Info.base_filter(resource) do
|
|
||||||
nil ->
|
|
||||||
query
|
|
||||||
|
|
||||||
filter ->
|
|
||||||
filter =
|
|
||||||
resource
|
|
||||||
|> Ash.Filter.parse!(
|
|
||||||
filter,
|
|
||||||
ash_query.calculations,
|
|
||||||
ash_query.context
|
|
||||||
)
|
|
||||||
|
|
||||||
dynamic =
|
|
||||||
if bindings do
|
|
||||||
filter = Ash.Filter.move_to_relationship_path(filter, path)
|
|
||||||
|
|
||||||
AshSqlite.Expr.dynamic_expr(root_query, filter, bindings, true)
|
|
||||||
else
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, filter, query.__ash_bindings__, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
from(row in query, where: ^dynamic)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(path, expr, seen_an_or? \\ false)
|
|
||||||
|
|
||||||
defp can_inner_join?(path, %{expression: expr}, seen_an_or?),
|
|
||||||
do: can_inner_join?(path, expr, seen_an_or?)
|
|
||||||
|
|
||||||
defp can_inner_join?(_path, expr, _seen_an_or?) when expr in [nil, true, false], do: true
|
|
||||||
|
|
||||||
defp can_inner_join?(path, %BooleanExpression{op: :and, left: left, right: right}, seen_an_or?) do
|
|
||||||
can_inner_join?(path, left, seen_an_or?) || can_inner_join?(path, right, seen_an_or?)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(path, %BooleanExpression{op: :or, left: left, right: right}, _) do
|
|
||||||
can_inner_join?(path, left, true) && can_inner_join?(path, right, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(
|
|
||||||
_,
|
|
||||||
%Not{},
|
|
||||||
_
|
|
||||||
) do
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(
|
|
||||||
search_path,
|
|
||||||
%struct{__operator__?: true, left: %Ref{relationship_path: relationship_path}},
|
|
||||||
seen_an_or?
|
|
||||||
)
|
|
||||||
when search_path == relationship_path and struct in @known_inner_join_predicates do
|
|
||||||
not seen_an_or?
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(
|
|
||||||
search_path,
|
|
||||||
%struct{__operator__?: true, right: %Ref{relationship_path: relationship_path}},
|
|
||||||
seen_an_or?
|
|
||||||
)
|
|
||||||
when search_path == relationship_path and struct in @known_inner_join_predicates do
|
|
||||||
not seen_an_or?
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(
|
|
||||||
search_path,
|
|
||||||
%struct{__function__?: true, arguments: arguments},
|
|
||||||
seen_an_or?
|
|
||||||
)
|
|
||||||
when struct in @known_inner_join_predicates do
|
|
||||||
if Enum.any?(arguments, &match?(%Ref{relationship_path: ^search_path}, &1)) do
|
|
||||||
not seen_an_or?
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp can_inner_join?(_, _, _), do: false
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def get_binding(resource, candidate_path, %{__ash_bindings__: _} = query, types) do
|
|
||||||
types = List.wrap(types)
|
|
||||||
|
|
||||||
Enum.find_value(query.__ash_bindings__.bindings, fn
|
|
||||||
{binding, %{path: path, source: source, type: type}} ->
|
|
||||||
if type in types &&
|
|
||||||
Ash.SatSolver.synonymous_relationship_paths?(resource, path, candidate_path, source) do
|
|
||||||
binding
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_binding(_, _, _, _), do: nil
|
|
||||||
|
|
||||||
defp add_distinct(_relationship, _join_type, joined_query) do
|
|
||||||
# We can't do the same distincting that we do in ash_postgres
|
|
||||||
# This means that all filters that reference `has_many` relationships need
|
|
||||||
# to be rewritten to use `exists`, which will allow us to not need to do any distincting.
|
|
||||||
# in fact, we probably want to do that in `ash_postgres` automatically too?
|
|
||||||
# if !joined_query.__ash_bindings__.in_group? &&
|
|
||||||
# (relationship.cardinality == :many || Map.get(relationship, :from_many?)) &&
|
|
||||||
# !joined_query.distinct do
|
|
||||||
# from(row in joined_query,
|
|
||||||
# distinct:
|
|
||||||
# ^AshSqlite.DataLayer.unwrap_one(
|
|
||||||
# Ash.Resource.Info.primary_key(joined_query.__ash_bindings__.resource)
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
# else
|
|
||||||
joined_query
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp join_relationship(
|
|
||||||
query,
|
|
||||||
relationship,
|
|
||||||
path,
|
|
||||||
join_type,
|
|
||||||
source,
|
|
||||||
filter
|
|
||||||
) do
|
|
||||||
case Map.get(query.__ash_bindings__.bindings, path) do
|
|
||||||
%{type: existing_join_type} when join_type != existing_join_type ->
|
|
||||||
raise "unreachable?"
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
do_join_relationship(
|
|
||||||
query,
|
|
||||||
relationship,
|
|
||||||
path,
|
|
||||||
join_type,
|
|
||||||
source,
|
|
||||||
filter
|
|
||||||
)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:ok, query}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_join_relationship(
|
|
||||||
query,
|
|
||||||
%{manual: {module, opts}} = relationship,
|
|
||||||
path,
|
|
||||||
kind,
|
|
||||||
source,
|
|
||||||
_filter
|
|
||||||
) do
|
|
||||||
full_path = path ++ [relationship.name]
|
|
||||||
initial_ash_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
binding_data = %{type: kind, path: full_path, source: source}
|
|
||||||
|
|
||||||
query = AshSqlite.DataLayer.add_binding(query, binding_data)
|
|
||||||
|
|
||||||
root_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
case maybe_get_resource_query(
|
|
||||||
relationship.destination,
|
|
||||||
relationship,
|
|
||||||
query,
|
|
||||||
full_path,
|
|
||||||
root_bindings
|
|
||||||
) do
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
|
|
||||||
{:ok, relationship_destination} ->
|
|
||||||
relationship_destination =
|
|
||||||
relationship_destination
|
|
||||||
|> Ecto.Queryable.to_query()
|
|
||||||
|
|
||||||
binding_kinds =
|
|
||||||
case kind do
|
|
||||||
:left ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
:inner ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
other ->
|
|
||||||
[other]
|
|
||||||
end
|
|
||||||
|
|
||||||
current_binding =
|
|
||||||
Enum.find_value(initial_ash_bindings.bindings, 0, fn {binding, data} ->
|
|
||||||
if data.type in binding_kinds && data.path == path do
|
|
||||||
binding
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
module.ash_sqlite_join(
|
|
||||||
query,
|
|
||||||
opts,
|
|
||||||
current_binding,
|
|
||||||
initial_ash_bindings.current,
|
|
||||||
kind,
|
|
||||||
relationship_destination
|
|
||||||
)
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
e in UndefinedFunctionError ->
|
|
||||||
if e.function == :ash_sqlite_join do
|
|
||||||
reraise """
|
|
||||||
AshSqlite cannot join to a manual relationship #{inspect(module)} that does not implement the `AshSqlite.ManualRelationship` behaviour.
|
|
||||||
""",
|
|
||||||
__STACKTRACE__
|
|
||||||
else
|
|
||||||
reraise e, __STACKTRACE__
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_join_relationship(
|
|
||||||
query,
|
|
||||||
%{type: :many_to_many} = relationship,
|
|
||||||
path,
|
|
||||||
kind,
|
|
||||||
source,
|
|
||||||
_filter
|
|
||||||
) do
|
|
||||||
join_relationship =
|
|
||||||
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
|
|
||||||
|
|
||||||
join_path = path ++ [join_relationship.name]
|
|
||||||
|
|
||||||
full_path = path ++ [relationship.name]
|
|
||||||
|
|
||||||
initial_ash_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
binding_data = %{type: kind, path: full_path, source: source}
|
|
||||||
|
|
||||||
query =
|
|
||||||
query
|
|
||||||
|> AshSqlite.DataLayer.add_binding(%{
|
|
||||||
path: join_path,
|
|
||||||
type: :left,
|
|
||||||
source: source
|
|
||||||
})
|
|
||||||
|> AshSqlite.DataLayer.add_binding(binding_data)
|
|
||||||
|
|
||||||
root_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
with {:ok, relationship_through} <-
|
|
||||||
maybe_get_resource_query(
|
|
||||||
relationship.through,
|
|
||||||
join_relationship,
|
|
||||||
query,
|
|
||||||
join_path,
|
|
||||||
root_bindings
|
|
||||||
),
|
|
||||||
{:ok, relationship_destination} <-
|
|
||||||
maybe_get_resource_query(
|
|
||||||
relationship.destination,
|
|
||||||
relationship,
|
|
||||||
query,
|
|
||||||
path,
|
|
||||||
root_bindings
|
|
||||||
) do
|
|
||||||
relationship_through =
|
|
||||||
relationship_through
|
|
||||||
|> Ecto.Queryable.to_query()
|
|
||||||
|
|
||||||
relationship_destination =
|
|
||||||
relationship_destination
|
|
||||||
|> Ecto.Queryable.to_query()
|
|
||||||
|
|
||||||
binding_kinds =
|
|
||||||
case kind do
|
|
||||||
:left ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
:inner ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
other ->
|
|
||||||
[other]
|
|
||||||
end
|
|
||||||
|
|
||||||
current_binding =
|
|
||||||
Enum.find_value(initial_ash_bindings.bindings, 0, fn {binding, data} ->
|
|
||||||
if data.type in binding_kinds && data.path == path do
|
|
||||||
binding
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
query =
|
|
||||||
case kind do
|
|
||||||
:inner ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
join: through in ^relationship_through,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on:
|
|
||||||
field(row, ^relationship.source_attribute) ==
|
|
||||||
field(through, ^relationship.source_attribute_on_join_resource),
|
|
||||||
join: destination in ^relationship_destination,
|
|
||||||
as: ^(initial_ash_bindings.current + 1),
|
|
||||||
on:
|
|
||||||
field(destination, ^relationship.destination_attribute) ==
|
|
||||||
field(through, ^relationship.destination_attribute_on_join_resource)
|
|
||||||
)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
left_join: through in ^relationship_through,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on:
|
|
||||||
field(row, ^relationship.source_attribute) ==
|
|
||||||
field(through, ^relationship.source_attribute_on_join_resource),
|
|
||||||
left_join: destination in ^relationship_destination,
|
|
||||||
as: ^(initial_ash_bindings.current + 1),
|
|
||||||
on:
|
|
||||||
field(destination, ^relationship.destination_attribute) ==
|
|
||||||
field(through, ^relationship.destination_attribute_on_join_resource)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_join_relationship(
|
|
||||||
query,
|
|
||||||
relationship,
|
|
||||||
path,
|
|
||||||
kind,
|
|
||||||
source,
|
|
||||||
_filter
|
|
||||||
) do
|
|
||||||
full_path = path ++ [relationship.name]
|
|
||||||
initial_ash_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
binding_data = %{type: kind, path: full_path, source: source}
|
|
||||||
|
|
||||||
query = AshSqlite.DataLayer.add_binding(query, binding_data)
|
|
||||||
|
|
||||||
root_bindings = query.__ash_bindings__
|
|
||||||
|
|
||||||
case maybe_get_resource_query(
|
|
||||||
relationship.destination,
|
|
||||||
relationship,
|
|
||||||
query,
|
|
||||||
full_path,
|
|
||||||
root_bindings
|
|
||||||
) do
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
|
|
||||||
{:ok, relationship_destination} ->
|
|
||||||
relationship_destination =
|
|
||||||
relationship_destination
|
|
||||||
|> Ecto.Queryable.to_query()
|
|
||||||
|
|
||||||
binding_kinds =
|
|
||||||
case kind do
|
|
||||||
:left ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
:inner ->
|
|
||||||
[:left, :inner]
|
|
||||||
|
|
||||||
other ->
|
|
||||||
[other]
|
|
||||||
end
|
|
||||||
|
|
||||||
current_binding =
|
|
||||||
Enum.find_value(initial_ash_bindings.bindings, 0, fn {binding, data} ->
|
|
||||||
if data.type in binding_kinds && data.path == path do
|
|
||||||
binding
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
query =
|
|
||||||
case {kind, Map.get(relationship, :no_attributes?)} do
|
|
||||||
{:inner, true} ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
join: destination in ^relationship_destination,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on: true
|
|
||||||
)
|
|
||||||
|
|
||||||
{_, true} ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
left_join: destination in ^relationship_destination,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on: true
|
|
||||||
)
|
|
||||||
|
|
||||||
{:inner, _} ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
join: destination in ^relationship_destination,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on:
|
|
||||||
field(row, ^relationship.source_attribute) ==
|
|
||||||
field(
|
|
||||||
destination,
|
|
||||||
^relationship.destination_attribute
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
from([{row, current_binding}] in query,
|
|
||||||
left_join: destination in ^relationship_destination,
|
|
||||||
as: ^initial_ash_bindings.current,
|
|
||||||
on:
|
|
||||||
field(row, ^relationship.source_attribute) ==
|
|
||||||
field(
|
|
||||||
destination,
|
|
||||||
^relationship.destination_attribute
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -19,11 +19,11 @@ defmodule AshSqlite.MigrationGenerator do
|
||||||
check: false,
|
check: false,
|
||||||
drop_columns: false
|
drop_columns: false
|
||||||
|
|
||||||
def generate(apis, opts \\ []) do
|
def generate(domains, opts \\ []) do
|
||||||
apis = List.wrap(apis)
|
domains = List.wrap(domains)
|
||||||
opts = opts(opts)
|
opts = opts(opts)
|
||||||
|
|
||||||
all_resources = Enum.uniq(Enum.flat_map(apis, &Ash.Api.Info.resources/1))
|
all_resources = Enum.uniq(Enum.flat_map(domains, &Ash.Domain.Info.resources/1))
|
||||||
|
|
||||||
snapshots =
|
snapshots =
|
||||||
all_resources
|
all_resources
|
||||||
|
@ -49,8 +49,8 @@ defmodule AshSqlite.MigrationGenerator do
|
||||||
|
|
||||||
Does not support everything supported by the migration generator.
|
Does not support everything supported by the migration generator.
|
||||||
"""
|
"""
|
||||||
def take_snapshots(api, repo, only_resources \\ nil) do
|
def take_snapshots(domain, repo, only_resources \\ nil) do
|
||||||
all_resources = api |> Ash.Api.Info.resources() |> Enum.uniq()
|
all_resources = domain |> Ash.Domain.Info.resources() |> Enum.uniq()
|
||||||
|
|
||||||
all_resources
|
all_resources
|
||||||
|> Enum.filter(fn resource ->
|
|> Enum.filter(fn resource ->
|
||||||
|
@ -408,10 +408,7 @@ defmodule AshSqlite.MigrationGenerator do
|
||||||
|
|
||||||
attributes = Enum.flat_map(snapshots, & &1.attributes)
|
attributes = Enum.flat_map(snapshots, & &1.attributes)
|
||||||
|
|
||||||
count_with_create =
|
count_with_create = Enum.count(snapshots, & &1.has_create_action)
|
||||||
snapshots
|
|
||||||
|> Enum.filter(& &1.has_create_action)
|
|
||||||
|> Enum.count()
|
|
||||||
|
|
||||||
new_snapshot = %{
|
new_snapshot = %{
|
||||||
snapshot
|
snapshot
|
||||||
|
@ -2035,7 +2032,7 @@ defmodule AshSqlite.MigrationGenerator do
|
||||||
defp has_create_action?(resource) do
|
defp has_create_action?(resource) do
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.actions()
|
|> Ash.Resource.Info.actions()
|
||||||
|> Enum.any?(&(&1.type == :create))
|
|> Enum.any?(&(&1.type == :create && !&1.manual))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp custom_indexes(resource) do
|
defp custom_indexes(resource) do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.MixHelpers do
|
defmodule AshSqlite.MixHelpers do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
def apis!(opts, args) do
|
def domains!(opts, args) do
|
||||||
apps =
|
apps =
|
||||||
if apps_paths = Mix.Project.apps_paths() do
|
if apps_paths = Mix.Project.apps_paths() do
|
||||||
apps_paths |> Map.keys() |> Enum.sort()
|
apps_paths |> Map.keys() |> Enum.sort()
|
||||||
|
@ -8,46 +8,46 @@ defmodule AshSqlite.MixHelpers do
|
||||||
[Mix.Project.config()[:app]]
|
[Mix.Project.config()[:app]]
|
||||||
end
|
end
|
||||||
|
|
||||||
configured_apis = Enum.flat_map(apps, &Application.get_env(&1, :ash_apis, []))
|
configured_domains = Enum.flat_map(apps, &Application.get_env(&1, :ash_domains, []))
|
||||||
|
|
||||||
apis =
|
domains =
|
||||||
if opts[:apis] && opts[:apis] != "" do
|
if opts[:domains] && opts[:domains] != "" do
|
||||||
opts[:apis]
|
opts[:domains]
|
||||||
|> Kernel.||("")
|
|> Kernel.||("")
|
||||||
|> String.split(",")
|
|> String.split(",")
|
||||||
|> Enum.flat_map(fn
|
|> Enum.flat_map(fn
|
||||||
"" ->
|
"" ->
|
||||||
[]
|
[]
|
||||||
|
|
||||||
api ->
|
domain ->
|
||||||
[Module.concat([api])]
|
[Module.concat([domain])]
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
configured_apis
|
configured_domains
|
||||||
end
|
end
|
||||||
|
|
||||||
apis
|
domains
|
||||||
|> Enum.map(&ensure_compiled(&1, args))
|
|> Enum.map(&ensure_compiled(&1, args))
|
||||||
|> case do
|
|> case do
|
||||||
[] ->
|
[] ->
|
||||||
raise "must supply the --apis argument, or set `config :my_app, ash_apis: [...]` in config"
|
raise "must supply the --domains argument, or set `config :my_app, ash_domains: [...]` in config"
|
||||||
|
|
||||||
apis ->
|
domains ->
|
||||||
apis
|
domains
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def repos!(opts, args) do
|
def repos!(opts, args) do
|
||||||
apis = apis!(opts, args)
|
domains = domains!(opts, args)
|
||||||
|
|
||||||
resources =
|
resources =
|
||||||
apis
|
domains
|
||||||
|> Enum.flat_map(&Ash.Api.Info.resources/1)
|
|> Enum.flat_map(&Ash.Domain.Info.resources/1)
|
||||||
|> Enum.filter(&(Ash.DataLayer.data_layer(&1) == AshSqlite.DataLayer))
|
|> Enum.filter(&(Ash.DataLayer.data_layer(&1) == AshSqlite.DataLayer))
|
||||||
|> case do
|
|> case do
|
||||||
[] ->
|
[] ->
|
||||||
raise """
|
raise """
|
||||||
No resources with `data_layer: AshSqlite.DataLayer` found in the apis #{Enum.map_join(apis, ",", &inspect/1)}.
|
No resources with `data_layer: AshSqlite.DataLayer` found in the domains #{Enum.map_join(domains, ",", &inspect/1)}.
|
||||||
|
|
||||||
Must be able to find at least one resource with `data_layer: AshSqlite.DataLayer`.
|
Must be able to find at least one resource with `data_layer: AshSqlite.DataLayer`.
|
||||||
"""
|
"""
|
||||||
|
@ -62,7 +62,7 @@ defmodule AshSqlite.MixHelpers do
|
||||||
|> case do
|
|> case do
|
||||||
[] ->
|
[] ->
|
||||||
raise """
|
raise """
|
||||||
No repos could be found configured on the resources in the apis: #{Enum.map_join(apis, ",", &inspect/1)}
|
No repos could be found configured on the resources in the domains: #{Enum.map_join(domains, ",", &inspect/1)}
|
||||||
|
|
||||||
At least one resource must have a repo configured.
|
At least one resource must have a repo configured.
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ defmodule AshSqlite.MixHelpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_compiled(api, args) do
|
defp ensure_compiled(domain, args) do
|
||||||
if Code.ensure_loaded?(Mix.Tasks.App.Config) do
|
if Code.ensure_loaded?(Mix.Tasks.App.Config) do
|
||||||
Mix.Task.run("app.config", args)
|
Mix.Task.run("app.config", args)
|
||||||
else
|
else
|
||||||
|
@ -104,18 +104,18 @@ defmodule AshSqlite.MixHelpers do
|
||||||
"--no-compile" not in args && Mix.Task.run("compile", args)
|
"--no-compile" not in args && Mix.Task.run("compile", args)
|
||||||
end
|
end
|
||||||
|
|
||||||
case Code.ensure_compiled(api) do
|
case Code.ensure_compiled(domain) do
|
||||||
{:module, _} ->
|
{:module, _} ->
|
||||||
api
|
domain
|
||||||
|> Ash.Api.Info.resources()
|
|> Ash.Domain.Info.resources()
|
||||||
|> Enum.each(&Code.ensure_compiled/1)
|
|> Enum.each(&Code.ensure_compiled/1)
|
||||||
|
|
||||||
# TODO: We shouldn't need to make sure that the resources are compiled
|
# TODO: We shouldn't need to make sure that the resources are compiled
|
||||||
|
|
||||||
api
|
domain
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Mix.raise("Could not load #{inspect(api)}, error: #{inspect(error)}. ")
|
Mix.raise("Could not load #{inspect(domain)}, error: #{inspect(error)}. ")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ defmodule Mix.Tasks.AshSqlite.Create do
|
||||||
|
|
||||||
@switches [
|
@switches [
|
||||||
quiet: :boolean,
|
quiet: :boolean,
|
||||||
apis: :string,
|
domains: :string,
|
||||||
no_compile: :boolean,
|
no_compile: :boolean,
|
||||||
no_deps_check: :boolean
|
no_deps_check: :boolean
|
||||||
]
|
]
|
||||||
|
@ -15,16 +15,16 @@ defmodule Mix.Tasks.AshSqlite.Create do
|
||||||
]
|
]
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Create the storage for repos in all resources for the given (or configured) apis.
|
Create the storage for repos in all resources for the given (or configured) domains.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
mix ash_sqlite.create
|
mix ash_sqlite.create
|
||||||
mix ash_sqlite.create --apis MyApp.Api1,MyApp.Api2
|
mix ash_sqlite.create --domains MyApp.Domain1,MyApp.Domain2
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
|
|
||||||
* `--apis` - the apis who's repos you want to migrate.
|
* `--domains` - the domains who's repos you want to migrate.
|
||||||
* `--quiet` - do not log output
|
* `--quiet` - do not log output
|
||||||
* `--no-compile` - do not compile before creating
|
* `--no-compile` - do not compile before creating
|
||||||
* `--no-deps-check` - do not compile before creating
|
* `--no-deps-check` - do not compile before creating
|
||||||
|
@ -41,7 +41,7 @@ defmodule Mix.Tasks.AshSqlite.Create do
|
||||||
["-r", to_string(repo)]
|
["-r", to_string(repo)]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
rest_opts = AshSqlite.MixHelpers.delete_arg(args, "--apis")
|
rest_opts = AshSqlite.MixHelpers.delete_arg(args, "--domains")
|
||||||
|
|
||||||
Mix.Task.reenable("ecto.create")
|
Mix.Task.reenable("ecto.create")
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Mix.Tasks.AshSqlite.Drop do
|
defmodule Mix.Tasks.AshSqlite.Drop do
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
|
|
||||||
@shortdoc "Drops the repository storage for the repos in the specified (or configured) apis"
|
@shortdoc "Drops the repository storage for the repos in the specified (or configured) domains"
|
||||||
@default_opts [force: false, force_drop: false]
|
@default_opts [force: false, force_drop: false]
|
||||||
|
|
||||||
@aliases [
|
@aliases [
|
||||||
|
@ -13,7 +13,7 @@ defmodule Mix.Tasks.AshSqlite.Drop do
|
||||||
force: :boolean,
|
force: :boolean,
|
||||||
force_drop: :boolean,
|
force_drop: :boolean,
|
||||||
quiet: :boolean,
|
quiet: :boolean,
|
||||||
apis: :string,
|
domains: :string,
|
||||||
no_compile: :boolean,
|
no_compile: :boolean,
|
||||||
no_deps_check: :boolean
|
no_deps_check: :boolean
|
||||||
]
|
]
|
||||||
|
@ -24,11 +24,11 @@ defmodule Mix.Tasks.AshSqlite.Drop do
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
mix ash_sqlite.drop
|
mix ash_sqlite.drop
|
||||||
mix ash_sqlite.drop -r MyApp.Api1,MyApp.Api2
|
mix ash_sqlite.drop -r MyApp.Domain1,MyApp.Domain2
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
|
|
||||||
* `--apis` - the apis who's repos should be dropped
|
* `--doains` - the domains who's repos should be dropped
|
||||||
* `-q`, `--quiet` - run the command quietly
|
* `-q`, `--quiet` - run the command quietly
|
||||||
* `-f`, `--force` - do not ask for confirmation when dropping the database.
|
* `-f`, `--force` - do not ask for confirmation when dropping the database.
|
||||||
Configuration is asked only when `:start_permanent` is set to true
|
Configuration is asked only when `:start_permanent` is set to true
|
||||||
|
@ -49,7 +49,7 @@ defmodule Mix.Tasks.AshSqlite.Drop do
|
||||||
["-r", to_string(repo)]
|
["-r", to_string(repo)]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
rest_opts = AshSqlite.MixHelpers.delete_arg(args, "--apis")
|
rest_opts = AshSqlite.MixHelpers.delete_arg(args, "--domains")
|
||||||
|
|
||||||
Mix.Task.reenable("ecto.drop")
|
Mix.Task.reenable("ecto.drop")
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
* `apis` - a comma separated list of API modules, for which migrations will be generated
|
* `domains` - a comma separated list of domain modules, for which migrations will be generated
|
||||||
* `snapshot-path` - a custom path to store the snapshots, defaults to "priv/resource_snapshots"
|
* `snapshot-path` - a custom path to store the snapshots, defaults to "priv/resource_snapshots"
|
||||||
* `migration-path` - a custom path to store the migrations, defaults to "priv".
|
* `migration-path` - a custom path to store the migrations, defaults to "priv".
|
||||||
Migrations are stored in a folder for each repo, so `priv/repo_name/migrations`
|
Migrations are stored in a folder for each repo, so `priv/repo_name/migrations`
|
||||||
|
@ -71,7 +71,7 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
|
||||||
{opts, _} =
|
{opts, _} =
|
||||||
OptionParser.parse!(args,
|
OptionParser.parse!(args,
|
||||||
strict: [
|
strict: [
|
||||||
apis: :string,
|
domains: :string,
|
||||||
snapshot_path: :string,
|
snapshot_path: :string,
|
||||||
migration_path: :string,
|
migration_path: :string,
|
||||||
quiet: :boolean,
|
quiet: :boolean,
|
||||||
|
@ -83,13 +83,13 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
apis = AshSqlite.MixHelpers.apis!(opts, args)
|
domains = AshSqlite.MixHelpers.domains!(opts, args)
|
||||||
|
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Keyword.put(:format, !opts[:no_format])
|
|> Keyword.put(:format, !opts[:no_format])
|
||||||
|> Keyword.delete(:no_format)
|
|> Keyword.delete(:no_format)
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(apis, opts)
|
AshSqlite.MigrationGenerator.generate(domains, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
import AshSqlite.MixHelpers,
|
import AshSqlite.MixHelpers,
|
||||||
only: [migrations_path: 2]
|
only: [migrations_path: 2]
|
||||||
|
|
||||||
@shortdoc "Runs the repository migrations for all repositories in the provided (or congigured) apis"
|
@shortdoc "Runs the repository migrations for all repositories in the provided (or congigured) domains"
|
||||||
|
|
||||||
@aliases [
|
@aliases [
|
||||||
n: :step
|
n: :step
|
||||||
|
@ -18,7 +18,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
pool_size: :integer,
|
pool_size: :integer,
|
||||||
log_sql: :boolean,
|
log_sql: :boolean,
|
||||||
strict_version_order: :boolean,
|
strict_version_order: :boolean,
|
||||||
apis: :string,
|
domains: :string,
|
||||||
no_compile: :boolean,
|
no_compile: :boolean,
|
||||||
no_deps_check: :boolean,
|
no_deps_check: :boolean,
|
||||||
migrations_path: :keep
|
migrations_path: :keep
|
||||||
|
@ -37,7 +37,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
specific version number, supply `--to version_number`. To migrate a
|
specific version number, supply `--to version_number`. To migrate a
|
||||||
specific number of times, use `--step n`.
|
specific number of times, use `--step n`.
|
||||||
|
|
||||||
This is only really useful if your api or apis only use a single repo.
|
This is only really useful if your domain or domains only use a single repo.
|
||||||
If you have multiple repos and you want to run a single migration and/or
|
If you have multiple repos and you want to run a single migration and/or
|
||||||
migrate/roll them back to different points, you will need to use the
|
migrate/roll them back to different points, you will need to use the
|
||||||
ecto specific task, `mix ecto.migrate` and provide your repo name.
|
ecto specific task, `mix ecto.migrate` and provide your repo name.
|
||||||
|
@ -48,7 +48,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
mix ash_sqlite.migrate
|
mix ash_sqlite.migrate
|
||||||
mix ash_sqlite.migrate --apis MyApp.Api1,MyApp.Api2
|
mix ash_sqlite.migrate --domains MyApp.Domain1,MyApp.Domain2
|
||||||
|
|
||||||
mix ash_sqlite.migrate -n 3
|
mix ash_sqlite.migrate -n 3
|
||||||
mix ash_sqlite.migrate --step 3
|
mix ash_sqlite.migrate --step 3
|
||||||
|
@ -57,7 +57,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
|
|
||||||
* `--apis` - the apis who's repos should be migrated
|
* `--domains` - the domains who's repos should be migrated
|
||||||
|
|
||||||
* `--all` - run all pending migrations
|
* `--all` - run all pending migrations
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ defmodule Mix.Tasks.AshSqlite.Migrate do
|
||||||
|
|
||||||
rest_opts =
|
rest_opts =
|
||||||
args
|
args
|
||||||
|> AshSqlite.MixHelpers.delete_arg("--apis")
|
|> AshSqlite.MixHelpers.delete_arg("--domains")
|
||||||
|> AshSqlite.MixHelpers.delete_arg("--migrations-path")
|
|> AshSqlite.MixHelpers.delete_arg("--migrations-path")
|
||||||
|
|
||||||
Mix.Task.reenable("ecto.migrate")
|
Mix.Task.reenable("ecto.migrate")
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Mix.Tasks.AshSqlite.Rollback do
|
||||||
import AshSqlite.MixHelpers,
|
import AshSqlite.MixHelpers,
|
||||||
only: [migrations_path: 2]
|
only: [migrations_path: 2]
|
||||||
|
|
||||||
@shortdoc "Rolls back the repository migrations for all repositories in the provided (or configured) apis"
|
@shortdoc "Rolls back the repository migrations for all repositories in the provided (or configured) domains"
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Reverts applied migrations in the given repository.
|
Reverts applied migrations in the given repository.
|
||||||
|
@ -16,7 +16,7 @@ defmodule Mix.Tasks.AshSqlite.Rollback do
|
||||||
specific number of times, use `--step n`. To undo all applied
|
specific number of times, use `--step n`. To undo all applied
|
||||||
migrations, provide `--all`.
|
migrations, provide `--all`.
|
||||||
|
|
||||||
This is only really useful if your api or apis only use a single repo.
|
This is only really useful if your domain or domains only use a single repo.
|
||||||
If you have multiple repos and you want to run a single migration and/or
|
If you have multiple repos and you want to run a single migration and/or
|
||||||
migrate/roll them back to different points, you will need to use the
|
migrate/roll them back to different points, you will need to use the
|
||||||
ecto specific task, `mix ecto.migrate` and provide your repo name.
|
ecto specific task, `mix ecto.migrate` and provide your repo name.
|
||||||
|
@ -30,7 +30,7 @@ defmodule Mix.Tasks.AshSqlite.Rollback do
|
||||||
mix ash_sqlite.rollback --to 20080906120000
|
mix ash_sqlite.rollback --to 20080906120000
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
* `--apis` - the apis who's repos should be rolledback
|
* `--domains` - the domains who's repos should be rolledback
|
||||||
* `--all` - revert all applied migrations
|
* `--all` - revert all applied migrations
|
||||||
* `--step` / `-n` - revert n number of applied migrations
|
* `--step` / `-n` - revert n number of applied migrations
|
||||||
* `--to` / `-v` - revert all migrations down to and including version
|
* `--to` / `-v` - revert all migrations down to and including version
|
||||||
|
@ -64,7 +64,7 @@ defmodule Mix.Tasks.AshSqlite.Rollback do
|
||||||
|
|
||||||
rest_opts =
|
rest_opts =
|
||||||
args
|
args
|
||||||
|> AshSqlite.MixHelpers.delete_arg("--apis")
|
|> AshSqlite.MixHelpers.delete_arg("--domains")
|
||||||
|> AshSqlite.MixHelpers.delete_arg("--migrations-path")
|
|> AshSqlite.MixHelpers.delete_arg("--migrations-path")
|
||||||
|
|
||||||
Mix.Task.reenable("ecto.rollback")
|
Mix.Task.reenable("ecto.rollback")
|
||||||
|
|
161
lib/sort.ex
161
lib/sort.ex
|
@ -1,161 +0,0 @@
|
||||||
defmodule AshSqlite.Sort do
|
|
||||||
@moduledoc false
|
|
||||||
require Ecto.Query
|
|
||||||
|
|
||||||
def sort(
|
|
||||||
query,
|
|
||||||
sort,
|
|
||||||
resource,
|
|
||||||
relationship_path \\ [],
|
|
||||||
binding \\ 0,
|
|
||||||
return_order_by? \\ false
|
|
||||||
) do
|
|
||||||
query = AshSqlite.DataLayer.default_bindings(query, resource)
|
|
||||||
|
|
||||||
calcs =
|
|
||||||
Enum.flat_map(sort, fn
|
|
||||||
{%Ash.Query.Calculation{} = calculation, _} ->
|
|
||||||
[calculation]
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
[]
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, query} =
|
|
||||||
AshSqlite.Join.join_all_relationships(
|
|
||||||
query,
|
|
||||||
%Ash.Filter{
|
|
||||||
resource: resource,
|
|
||||||
expression: calcs
|
|
||||||
},
|
|
||||||
left_only?: true
|
|
||||||
)
|
|
||||||
|
|
||||||
sort
|
|
||||||
|> sanitize_sort()
|
|
||||||
|> Enum.reduce_while({:ok, []}, fn
|
|
||||||
{order, %Ash.Query.Calculation{} = calc}, {:ok, query_expr} ->
|
|
||||||
type =
|
|
||||||
if calc.type do
|
|
||||||
AshSqlite.Types.parameterized_type(calc.type, calc.constraints)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
calc.opts
|
|
||||||
|> calc.module.expression(calc.context)
|
|
||||||
|> Ash.Filter.hydrate_refs(%{
|
|
||||||
resource: resource,
|
|
||||||
public?: false
|
|
||||||
})
|
|
||||||
|> Ash.Filter.move_to_relationship_path(relationship_path)
|
|
||||||
|> case do
|
|
||||||
{:ok, expr} ->
|
|
||||||
expr =
|
|
||||||
AshSqlite.Expr.dynamic_expr(query, expr, query.__ash_bindings__, false, type)
|
|
||||||
|
|
||||||
{:cont, {:ok, query_expr ++ [{order, expr}]}}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:halt, {:error, error}}
|
|
||||||
end
|
|
||||||
|
|
||||||
{order, sort}, {:ok, query_expr} ->
|
|
||||||
expr =
|
|
||||||
Ecto.Query.dynamic(field(as(^binding), ^sort))
|
|
||||||
|
|
||||||
{:cont, {:ok, query_expr ++ [{order, expr}]}}
|
|
||||||
end)
|
|
||||||
|> case do
|
|
||||||
{:ok, []} ->
|
|
||||||
if return_order_by? do
|
|
||||||
{:ok, order_to_fragments([])}
|
|
||||||
else
|
|
||||||
{:ok, query}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, sort_exprs} ->
|
|
||||||
if return_order_by? do
|
|
||||||
{:ok, order_to_fragments(sort_exprs)}
|
|
||||||
else
|
|
||||||
new_query = Ecto.Query.order_by(query, ^sort_exprs)
|
|
||||||
|
|
||||||
sort_expr = List.last(new_query.order_bys)
|
|
||||||
|
|
||||||
new_query =
|
|
||||||
new_query
|
|
||||||
|> Map.update!(:windows, fn windows ->
|
|
||||||
order_by_expr = %{sort_expr | expr: [order_by: sort_expr.expr]}
|
|
||||||
Keyword.put(windows, :order, order_by_expr)
|
|
||||||
end)
|
|
||||||
|> Map.update!(:__ash_bindings__, &Map.put(&1, :__order__?, true))
|
|
||||||
|
|
||||||
{:ok, new_query}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def order_to_fragments([]), do: []
|
|
||||||
|
|
||||||
def order_to_fragments(order) when is_list(order) do
|
|
||||||
Enum.map(order, &do_order_to_fragments(&1))
|
|
||||||
end
|
|
||||||
|
|
||||||
def do_order_to_fragments({order, sort}) do
|
|
||||||
case order do
|
|
||||||
:asc ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? ASC", ^sort))
|
|
||||||
|
|
||||||
:desc ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? DESC", ^sort))
|
|
||||||
|
|
||||||
:asc_nulls_last ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? ASC NULLS LAST", ^sort))
|
|
||||||
|
|
||||||
:asc_nulls_first ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? ASC NULLS FIRST", ^sort))
|
|
||||||
|
|
||||||
:desc_nulls_first ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? DESC NULLS FIRST", ^sort))
|
|
||||||
|
|
||||||
:desc_nulls_last ->
|
|
||||||
Ecto.Query.dynamic([row], fragment("? DESC NULLS LAST", ^sort))
|
|
||||||
"DESC NULLS LAST"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def order_to_postgres_order(dir) do
|
|
||||||
case dir do
|
|
||||||
:asc -> nil
|
|
||||||
:asc_nils_last -> " ASC NULLS LAST"
|
|
||||||
:asc_nils_first -> " ASC NULLS FIRST"
|
|
||||||
:desc -> " DESC"
|
|
||||||
:desc_nils_last -> " DESC NULLS LAST"
|
|
||||||
:desc_nils_first -> " DESC NULLS FIRST"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sanitize_sort(sort) do
|
|
||||||
sort
|
|
||||||
|> List.wrap()
|
|
||||||
|> Enum.map(fn
|
|
||||||
{sort, {order, context}} ->
|
|
||||||
{ash_to_ecto_order(order), {sort, context}}
|
|
||||||
|
|
||||||
{sort, order} ->
|
|
||||||
{ash_to_ecto_order(order), sort}
|
|
||||||
|
|
||||||
sort ->
|
|
||||||
sort
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ash_to_ecto_order(:asc_nils_last), do: :asc_nulls_last
|
|
||||||
defp ash_to_ecto_order(:asc_nils_first), do: :asc_nulls_first
|
|
||||||
defp ash_to_ecto_order(:desc_nils_last), do: :desc_nulls_last
|
|
||||||
defp ash_to_ecto_order(:desc_nils_first), do: :desc_nulls_first
|
|
||||||
defp ash_to_ecto_order(other), do: other
|
|
||||||
end
|
|
443
lib/sql_implementation.ex
Normal file
443
lib/sql_implementation.ex
Normal file
|
@ -0,0 +1,443 @@
|
||||||
|
defmodule AshSqlite.SqlImplementation do
|
||||||
|
@moduledoc false
|
||||||
|
use AshSql.Implementation
|
||||||
|
|
||||||
|
require Ecto.Query
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def manual_relationship_function, do: :ash_sqlite_join
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def manual_relationship_subquery_function, do: :ash_sqlite_subquery
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def expr(
|
||||||
|
query,
|
||||||
|
%like{arguments: [arg1, arg2], embedded?: pred_embedded?},
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
acc,
|
||||||
|
type
|
||||||
|
)
|
||||||
|
when like in [AshSqlite.Functions.Like, AshSqlite.Functions.ILike] do
|
||||||
|
{arg1, acc} =
|
||||||
|
AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc)
|
||||||
|
|
||||||
|
{arg2, acc} =
|
||||||
|
AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc)
|
||||||
|
|
||||||
|
inner_dyn =
|
||||||
|
if like == AshSqlite.Functions.Like do
|
||||||
|
Ecto.Query.dynamic(like(^arg1, ^arg2))
|
||||||
|
else
|
||||||
|
Ecto.Query.dynamic(like(fragment("LOWER(?)", ^arg1), fragment("LOWER(?)", ^arg2)))
|
||||||
|
end
|
||||||
|
|
||||||
|
if type != Ash.Type.Boolean do
|
||||||
|
{:ok, inner_dyn, acc}
|
||||||
|
else
|
||||||
|
{:ok, Ecto.Query.dynamic(type(^inner_dyn, ^type)), acc}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expr(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.GetPath{
|
||||||
|
arguments: [%Ash.Query.Ref{attribute: %{type: type}}, right]
|
||||||
|
} = get_path,
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
acc,
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
when is_atom(type) and is_list(right) do
|
||||||
|
if Ash.Type.embedded_type?(type) do
|
||||||
|
type = determine_type_at_path(type, right)
|
||||||
|
|
||||||
|
do_get_path(query, get_path, bindings, embedded?, acc, type)
|
||||||
|
else
|
||||||
|
do_get_path(query, get_path, bindings, embedded?, acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expr(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.GetPath{
|
||||||
|
arguments: [%Ash.Query.Ref{attribute: %{type: {:array, type}}}, right]
|
||||||
|
} = get_path,
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
acc,
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
when is_atom(type) and is_list(right) do
|
||||||
|
if Ash.Type.embedded_type?(type) do
|
||||||
|
type = determine_type_at_path(type, right)
|
||||||
|
do_get_path(query, get_path, bindings, embedded?, acc, type)
|
||||||
|
else
|
||||||
|
do_get_path(query, get_path, bindings, embedded?, acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expr(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.GetPath{} = get_path,
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
acc,
|
||||||
|
type
|
||||||
|
) do
|
||||||
|
do_get_path(query, get_path, bindings, embedded?, acc, type)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def expr(
|
||||||
|
_query,
|
||||||
|
_expr,
|
||||||
|
_bindings,
|
||||||
|
_embedded?,
|
||||||
|
_acc,
|
||||||
|
_type
|
||||||
|
) do
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def type_expr(expr, nil), do: expr
|
||||||
|
|
||||||
|
def type_expr(expr, type) when is_atom(type) do
|
||||||
|
type = Ash.Type.get_type(type)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
!Ash.Type.ash_type?(type) ->
|
||||||
|
Ecto.Query.dynamic(type(^expr, ^type))
|
||||||
|
|
||||||
|
Ash.Type.storage_type(type, []) == :ci_string ->
|
||||||
|
Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr))
|
||||||
|
|
||||||
|
true ->
|
||||||
|
Ecto.Query.dynamic(type(^expr, ^Ash.Type.storage_type(type, [])))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_expr(expr, type) do
|
||||||
|
case type do
|
||||||
|
{:parameterized, inner_type, constraints} ->
|
||||||
|
if inner_type.type(constraints) == :ci_string do
|
||||||
|
Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr))
|
||||||
|
else
|
||||||
|
Ecto.Query.dynamic(type(^expr, ^type))
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
expr
|
||||||
|
|
||||||
|
type ->
|
||||||
|
Ecto.Query.dynamic(type(^expr, ^type))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def table(resource) do
|
||||||
|
AshSqlite.DataLayer.Info.table(resource)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def schema(_resource) do
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def repo(resource, _kind) do
|
||||||
|
AshSqlite.DataLayer.Info.repo(resource)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def multicolumn_distinct?, do: false
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def parameterized_type(type, constraints, no_maps? \\ false)
|
||||||
|
|
||||||
|
def parameterized_type({:parameterized, _, _} = type, _, _) do
|
||||||
|
type
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameterized_type({:in, type}, constraints, no_maps?) do
|
||||||
|
parameterized_type({:array, type}, constraints, no_maps?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameterized_type({:array, type}, constraints, no_maps?) do
|
||||||
|
case parameterized_type(type, constraints[:items] || [], no_maps?) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
type ->
|
||||||
|
{:array, type}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameterized_type(type, _constraints, _no_maps?)
|
||||||
|
when type in [Ash.Type.Map, Ash.Type.Map.EctoType],
|
||||||
|
do: nil
|
||||||
|
|
||||||
|
def parameterized_type(type, constraints, no_maps?) do
|
||||||
|
if Ash.Type.ash_type?(type) do
|
||||||
|
cast_in_query? =
|
||||||
|
if function_exported?(Ash.Type, :cast_in_query?, 2) do
|
||||||
|
Ash.Type.cast_in_query?(type, constraints)
|
||||||
|
else
|
||||||
|
Ash.Type.cast_in_query?(type)
|
||||||
|
end
|
||||||
|
|
||||||
|
if cast_in_query? do
|
||||||
|
parameterized_type(Ash.Type.ecto_type(type), constraints, no_maps?)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if is_atom(type) && :erlang.function_exported(type, :type, 1) do
|
||||||
|
{:parameterized, type, constraints || []}
|
||||||
|
else
|
||||||
|
type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def determine_types(mod, values) do
|
||||||
|
Code.ensure_compiled(mod)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
:erlang.function_exported(mod, :types, 0) ->
|
||||||
|
mod.types()
|
||||||
|
|
||||||
|
:erlang.function_exported(mod, :args, 0) ->
|
||||||
|
mod.args()
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[:any]
|
||||||
|
end
|
||||||
|
|> Enum.map(fn types ->
|
||||||
|
case types do
|
||||||
|
:same ->
|
||||||
|
types =
|
||||||
|
for _ <- values do
|
||||||
|
:same
|
||||||
|
end
|
||||||
|
|
||||||
|
closest_fitting_type(types, values)
|
||||||
|
|
||||||
|
:any ->
|
||||||
|
for _ <- values do
|
||||||
|
:any
|
||||||
|
end
|
||||||
|
|
||||||
|
types ->
|
||||||
|
closest_fitting_type(types, values)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(fn types ->
|
||||||
|
Enum.all?(types, &(vagueness(&1) == 0))
|
||||||
|
end)
|
||||||
|
|> case do
|
||||||
|
[type] ->
|
||||||
|
if type == :any || type == {:in, :any} do
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
type
|
||||||
|
end
|
||||||
|
|
||||||
|
# There are things we could likely do here
|
||||||
|
# We only say "we know what types these are" when we explicitly know
|
||||||
|
_ ->
|
||||||
|
Enum.map(values, fn _ -> nil end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp closest_fitting_type(types, values) do
|
||||||
|
types_with_values = Enum.zip(types, values)
|
||||||
|
|
||||||
|
types_with_values
|
||||||
|
|> fill_in_known_types()
|
||||||
|
|> clarify_types()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp clarify_types(types) do
|
||||||
|
basis =
|
||||||
|
types
|
||||||
|
|> Enum.map(&elem(&1, 0))
|
||||||
|
|> Enum.min_by(&vagueness(&1))
|
||||||
|
|
||||||
|
Enum.map(types, fn {type, _value} ->
|
||||||
|
replace_same(type, basis)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_same({:in, type}, basis) do
|
||||||
|
{:in, replace_same(type, basis)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_same(:same, :same) do
|
||||||
|
:any
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_same(:same, {:in, :same}) do
|
||||||
|
{:in, :any}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_same(:same, basis) do
|
||||||
|
basis
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_same(other, _basis) do
|
||||||
|
other
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fill_in_known_types(types) do
|
||||||
|
Enum.map(types, &fill_in_known_type/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fill_in_known_type(
|
||||||
|
{vague_type, %Ash.Query.Ref{attribute: %{type: type, constraints: constraints}}} = ref
|
||||||
|
)
|
||||||
|
when vague_type in [:any, :same] do
|
||||||
|
if Ash.Type.ash_type?(type) do
|
||||||
|
type = type |> parameterized_type(constraints, true) |> array_to_in()
|
||||||
|
|
||||||
|
{type || :any, ref}
|
||||||
|
else
|
||||||
|
type =
|
||||||
|
if is_atom(type) && :erlang.function_exported(type, :type, 1) do
|
||||||
|
{:parameterized, type, []} |> array_to_in()
|
||||||
|
else
|
||||||
|
type |> array_to_in()
|
||||||
|
end
|
||||||
|
|
||||||
|
{type, ref}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fill_in_known_type(
|
||||||
|
{{:array, type}, %Ash.Query.Ref{attribute: %{type: {:array, type}} = attribute} = ref}
|
||||||
|
) do
|
||||||
|
{:in, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fill_in_known_type({type, value}), do: {array_to_in(type), value}
|
||||||
|
|
||||||
|
defp array_to_in({:array, v}), do: {:in, array_to_in(v)}
|
||||||
|
|
||||||
|
defp array_to_in({:parameterized, type, constraints}),
|
||||||
|
do: {:parameterized, array_to_in(type), constraints}
|
||||||
|
|
||||||
|
defp array_to_in(v), do: v
|
||||||
|
|
||||||
|
defp vagueness({:in, type}), do: vagueness(type)
|
||||||
|
defp vagueness(:same), do: 2
|
||||||
|
defp vagueness(:any), do: 1
|
||||||
|
defp vagueness(_), do: 0
|
||||||
|
|
||||||
|
defp do_get_path(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.GetPath{arguments: [left, right], embedded?: pred_embedded?},
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
acc,
|
||||||
|
type \\ nil
|
||||||
|
) do
|
||||||
|
path = "$." <> Enum.join(right, ".")
|
||||||
|
|
||||||
|
{expr, acc} =
|
||||||
|
AshSql.Expr.dynamic_expr(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.Fragment{
|
||||||
|
embedded?: pred_embedded?,
|
||||||
|
arguments: [
|
||||||
|
raw: "json_extract(",
|
||||||
|
expr: left,
|
||||||
|
raw: ", ",
|
||||||
|
expr: path,
|
||||||
|
raw: ")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
type,
|
||||||
|
acc
|
||||||
|
)
|
||||||
|
|
||||||
|
if type do
|
||||||
|
{expr, acc} =
|
||||||
|
AshSql.Expr.dynamic_expr(
|
||||||
|
query,
|
||||||
|
%Ash.Query.Function.Type{arguments: [expr, type, []]},
|
||||||
|
bindings,
|
||||||
|
embedded?,
|
||||||
|
type,
|
||||||
|
acc
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, expr, acc}
|
||||||
|
else
|
||||||
|
{:ok, expr, acc}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp determine_type_at_path(type, path) do
|
||||||
|
path
|
||||||
|
|> Enum.reject(&is_integer/1)
|
||||||
|
|> do_determine_type_at_path(type)
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
{type, constraints} ->
|
||||||
|
AshSqlite.Types.parameterized_type(type, constraints)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_determine_type_at_path([], _), do: nil
|
||||||
|
|
||||||
|
defp do_determine_type_at_path([item], type) do
|
||||||
|
case Ash.Resource.Info.attribute(type, item) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
%{type: {:array, type}, constraints: constraints} ->
|
||||||
|
constraints = constraints[:items] || []
|
||||||
|
|
||||||
|
{type, constraints}
|
||||||
|
|
||||||
|
%{type: type, constraints: constraints} ->
|
||||||
|
{type, constraints}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_determine_type_at_path([item | rest], type) do
|
||||||
|
case Ash.Resource.Info.attribute(type, item) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
%{type: {:array, type}} ->
|
||||||
|
if Ash.Type.embedded_type?(type) do
|
||||||
|
type
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
%{type: type} ->
|
||||||
|
if Ash.Type.embedded_type?(type) do
|
||||||
|
type
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
type ->
|
||||||
|
do_determine_type_at_path(rest, type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,14 +0,0 @@
|
||||||
defmodule AshSqlite.Type.CiStringWrapper do
|
|
||||||
@moduledoc false
|
|
||||||
use Ash.Type
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def storage_type(_), do: :ci_string
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
defdelegate cast_input(value, constraints), to: Ash.Type.CiString
|
|
||||||
@impl true
|
|
||||||
defdelegate cast_stored(value, constraints), to: Ash.Type.CiString
|
|
||||||
@impl true
|
|
||||||
defdelegate dump_to_native(value, constraints), to: Ash.Type.CiString
|
|
||||||
end
|
|
|
@ -1,14 +0,0 @@
|
||||||
defmodule AshSqlite.Type.StringWrapper do
|
|
||||||
@moduledoc false
|
|
||||||
use Ash.Type
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def storage_type(_), do: :string
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
defdelegate cast_input(value, constraints), to: Ash.Type.String
|
|
||||||
@impl true
|
|
||||||
defdelegate cast_stored(value, constraints), to: Ash.Type.String
|
|
||||||
@impl true
|
|
||||||
defdelegate dump_to_native(value, constraints), to: Ash.Type.String
|
|
||||||
end
|
|
5
mix.exs
5
mix.exs
|
@ -169,10 +169,11 @@ defmodule AshSqlite.MixProject do
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:ecto_sql, "~> 3.9"},
|
{:ecto_sql, "~> 3.9"},
|
||||||
{:ecto_sqlite3, "~> 0.12.0"},
|
{:ecto_sqlite3, "~> 0.12"},
|
||||||
|
{:ash_sql, "~> 0.1.0-rc.2"},
|
||||||
{:ecto, "~> 3.9"},
|
{:ecto, "~> 3.9"},
|
||||||
{:jason, "~> 1.0"},
|
{:jason, "~> 1.0"},
|
||||||
{:ash, ash_version("~> 2.15 and >= 2.15.12")},
|
{:ash, ash_version("~> 3.0.0-rc.0")},
|
||||||
{:git_ops, "~> 2.5", only: [:dev, :test]},
|
{:git_ops, "~> 2.5", only: [:dev, :test]},
|
||||||
{:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false},
|
{:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false},
|
||||||
{:ex_check, "~> 0.14", only: [:dev, :test]},
|
{:ex_check, "~> 0.14", only: [:dev, :test]},
|
||||||
|
|
63
mix.lock
63
mix.lock
|
@ -1,46 +1,39 @@
|
||||||
%{
|
%{
|
||||||
"ash": {:hex, :ash, "2.15.15", "8649aad00ba93a6e8792889f27f36954376745dde600c739bc180054d6a76469", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4da1105adcc4991841889a238ea7475f74923c36d465f886db862333cb54ecb0"},
|
"ash": {:hex, :ash, "3.0.0-rc.6", "78d9bc068a0c632e4fe2db8a8802f772c65329c8bc15877ceb6eb2ac83e1fa8b", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.8", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.7 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3e0ccc857572d10972868886aff46f9b1d11c90f8b357f85f2887e71f702e916"},
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
"ash_sql": {:hex, :ash_sql, "0.1.1-rc.2", "281e036180ea069c24239ea051fd6551708c21a0690b099acb326d3d7005302e", [:mix], [{:ash, "~> 3.0.0-rc.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "76a21857b8d823ee47732c20746830732be9a005c72b11db6bd8e203e459a11c"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||||
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
||||||
"credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"},
|
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
|
||||||
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
|
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
|
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||||
"earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"},
|
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
||||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
|
||||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.12.0", "9ee845ac45a76e3c5c0fe65898f3538f5b0969912a95f0beef3d4ae8e63f6a06", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "4eaf8550df1fd0043bcf039a5dce407fd8afc30a115ced173fe6b9815eeedb55"},
|
"elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
|
||||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||||
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
||||||
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
|
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
|
||||||
"excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
|
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
|
||||||
"exqlite": {:hex, :exqlite, "0.14.0", "f275c6fe1ce35d383b4ed52461ca98c02354eeb2c651c13f5b4badcfd39b743f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "e335eca54749d04dcdedcbc87be85e2176030aab3d7b74b6323fda7e3552ee4c"},
|
"exqlite": {:hex, :exqlite, "0.20.0", "99b711eb1a3309b380ff54901d3d7db8e7afaf4b68a34398a69e1fa1b9b2054e", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "385ed37b8317101b7f9b58333910798ebe395e77ee6ca261be74a1a06b3d61f6"},
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||||
"git_ops": {:hex, :git_ops, "2.5.5", "4f8369f3c9347e06a7f289de98fadfc95194149156335c5292479a53eddbccd2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "3b1e3b12968f9da6f79b5e2b2274477206949376e3579d05a5f3d439eda0b746"},
|
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"},
|
||||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
|
||||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
|
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
|
||||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
"reactor": {:hex, :reactor, "0.8.1", "1aec71d16083901277727c8162f6dd0f07e80f5ca98911b6ef4f2c95e6e62758", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae3936d97a3e4a316744f70c77b85345b08b70da334024c26e6b5eb8ede1246b"},
|
||||||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
|
||||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
"spark": {:hex, :spark, "2.1.11", "8093149dfd583b5ce2c06e1fea1faaf4125b50e4703138b2cbefb78c8f4aa07f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1877d92ab993b860e9d828bfd72d50367c0d3a53dd84f4de5d221baf66ae8723"},
|
||||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
"splode": {:hex, :splode, "0.2.1", "020079ec06c9e00f8b6586852e781b5e07aee6ba588f3f45dd993831c87b0511", [:mix], [], "hexpm", "d232a933666061fe1f659d9906042fa94b9b393bb1129a4fde6fa680033b2611"},
|
||||||
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
|
|
||||||
"spark": {:hex, :spark, "1.1.44", "be9f2669b03ae43447bda77045598a4500988538a7d0ba576b8e306332822147", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e49bf5ca770cb0bb9cac7ed8da5eb7871156b3236c8c535f3f4caa93377059a3"},
|
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
|
||||||
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.AtomicsTest do
|
defmodule AshSqlite.AtomicsTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
import Ash.Expr
|
import Ash.Expr
|
||||||
|
|
||||||
|
@ -10,40 +10,40 @@ defmodule AshSqlite.AtomicsTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
||||||
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
||||||
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{price: 2}] = Post |> Api.read!()
|
assert [%{price: 2}] = Post |> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a basic atomic works" do
|
test "a basic atomic works" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{price: 2} =
|
assert %{price: 2} =
|
||||||
post
|
post
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "an atomic that violates a constraint will return the proper error" do
|
test "an atomic that violates a constraint will return the proper error" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert_raise Ash.Error.Invalid, ~r/does not exist/, fn ->
|
assert_raise Ash.Error.Invalid, ~r/does not exist/, fn ->
|
||||||
post
|
post
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.atomic_update(:organization_id, Ash.UUID.generate())
|
|> Ash.Changeset.atomic_update(:organization_id, Ash.UUID.generate())
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,13 +51,13 @@ defmodule AshSqlite.AtomicsTest do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
post =
|
post =
|
||||||
post
|
post
|
||||||
|> Ash.Changeset.for_update(:update, %{})
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|> Ash.Changeset.atomic_update(:score, expr(score_after_winning))
|
|> Ash.Changeset.atomic_update(:score, expr(score_after_winning))
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
assert post.score == 1
|
assert post.score == 1
|
||||||
end
|
end
|
||||||
|
@ -66,7 +66,7 @@ defmodule AshSqlite.AtomicsTest do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert Post.increment_score!(post, 2).score == 2
|
assert Post.increment_score!(post, 2).score == 2
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
defmodule AshSqlite.BulkCreateTest do
|
defmodule AshSqlite.BulkCreateTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
describe "bulk creates" do
|
describe "bulk creates" do
|
||||||
test "bulk creates insert each input" do
|
test "bulk creates insert each input" do
|
||||||
Api.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create)
|
Ash.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create)
|
||||||
|
|
||||||
assert [%{title: "fred"}, %{title: "george"}] =
|
assert [%{title: "fred"}, %{title: "george"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(:title)
|
|> Ash.Query.sort(:title)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "bulk creates can be streamed" do
|
test "bulk creates can be streamed" do
|
||||||
assert [{:ok, %{title: "fred"}}, {:ok, %{title: "george"}}] =
|
assert [{:ok, %{title: "fred"}}, {:ok, %{title: "george"}}] =
|
||||||
Api.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create,
|
Ash.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create,
|
||||||
return_stream?: true,
|
return_stream?: true,
|
||||||
return_records?: true
|
return_records?: true
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}},
|
{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}},
|
||||||
{:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20}}
|
{:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20}}
|
||||||
] =
|
] =
|
||||||
Api.bulk_create!(
|
Ash.bulk_create!(
|
||||||
[
|
[
|
||||||
%{title: "fred", uniq_one: "one", uniq_two: "two", price: 10},
|
%{title: "fred", uniq_one: "one", uniq_two: "two", price: 10},
|
||||||
%{title: "george", uniq_one: "three", uniq_two: "four", price: 20}
|
%{title: "george", uniq_one: "three", uniq_two: "four", price: 20}
|
||||||
|
@ -42,7 +42,7 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 1000}},
|
{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 1000}},
|
||||||
{:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20_000}}
|
{:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20_000}}
|
||||||
] =
|
] =
|
||||||
Api.bulk_create!(
|
Ash.bulk_create!(
|
||||||
[
|
[
|
||||||
%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000},
|
%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000},
|
||||||
%{title: "else", uniq_one: "three", uniq_two: "four", price: 20_000}
|
%{title: "else", uniq_one: "three", uniq_two: "four", price: 20_000}
|
||||||
|
@ -65,7 +65,7 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "bulk creates can create relationships" do
|
test "bulk creates can create relationships" do
|
||||||
Api.bulk_create!(
|
Ash.bulk_create!(
|
||||||
[%{title: "fred", rating: %{score: 5}}, %{title: "george", rating: %{score: 0}}],
|
[%{title: "fred", rating: %{score: 5}}, %{title: "george", rating: %{score: 0}}],
|
||||||
Post,
|
Post,
|
||||||
:create
|
:create
|
||||||
|
@ -78,14 +78,14 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(:title)
|
|> Ash.Query.sort(:title)
|
||||||
|> Ash.Query.load(:ratings)
|
|> Ash.Query.load(:ratings)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validation errors" do
|
describe "validation errors" do
|
||||||
test "skips invalid by default" do
|
test "skips invalid by default" do
|
||||||
assert %{records: [_], errors: [_]} =
|
assert %{records: [_], errors: [_]} =
|
||||||
Api.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create,
|
Ash.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create,
|
||||||
return_records?: true,
|
return_records?: true,
|
||||||
return_errors?: true
|
return_errors?: true
|
||||||
)
|
)
|
||||||
|
@ -93,7 +93,7 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
|
|
||||||
test "returns errors in the stream" do
|
test "returns errors in the stream" do
|
||||||
assert [{:ok, _}, {:error, _}] =
|
assert [{:ok, _}, {:error, _}] =
|
||||||
Api.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create,
|
Ash.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create,
|
||||||
return_records?: true,
|
return_records?: true,
|
||||||
return_stream?: true,
|
return_stream?: true,
|
||||||
return_errors?: true
|
return_errors?: true
|
||||||
|
@ -107,9 +107,9 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
org =
|
org =
|
||||||
AshSqlite.Test.Organization
|
AshSqlite.Test.Organization
|
||||||
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Api.bulk_create(
|
Ash.bulk_create(
|
||||||
[
|
[
|
||||||
%{title: "fred", organization_id: org.id},
|
%{title: "fred", organization_id: org.id},
|
||||||
%{title: "george", organization_id: Ash.UUID.generate()}
|
%{title: "george", organization_id: Ash.UUID.generate()}
|
||||||
|
@ -122,11 +122,11 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
assert [] =
|
assert [] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(:title)
|
|> Ash.Query.sort(:title)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "database errors don't affect other batches" do
|
test "database errors don't affect other batches" do
|
||||||
Api.bulk_create(
|
Ash.bulk_create(
|
||||||
[%{title: "george", organization_id: Ash.UUID.generate()}, %{title: "fred"}],
|
[%{title: "george", organization_id: Ash.UUID.generate()}, %{title: "fred"}],
|
||||||
Post,
|
Post,
|
||||||
:create,
|
:create,
|
||||||
|
@ -137,7 +137,7 @@ defmodule AshSqlite.BulkCreateTest do
|
||||||
assert [%{title: "fred"}] =
|
assert [%{title: "fred"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(:title)
|
|> Ash.Query.sort(:title)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.CalculationTest do
|
defmodule AshSqlite.CalculationTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Account, Api, Author, Comment, Post, User}
|
alias AshSqlite.Test.{Account, Author, Comment, Post, User}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
@ -8,26 +8,26 @@ defmodule AshSqlite.CalculationTest do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}})
|
|> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{title: "Mr."} =
|
assert %{title: "Mr."} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(id == ^author.id)
|
|> Ash.Query.filter(id == ^author.id)
|
||||||
|> Ash.Query.load(:title)
|
|> Ash.Query.load(:title)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculations can use the || operator" do
|
test "calculations can use the || operator" do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}})
|
|> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{first_name_or_bob: "bob"} =
|
assert %{first_name_or_bob: "bob"} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(id == ^author.id)
|
|> Ash.Query.filter(id == ^author.id)
|
||||||
|> Ash.Query.load(:first_name_or_bob)
|
|> Ash.Query.load(:first_name_or_bob)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculations can use the && operator" do
|
test "calculations can use the && operator" do
|
||||||
|
@ -37,24 +37,24 @@ defmodule AshSqlite.CalculationTest do
|
||||||
first_name: "fred",
|
first_name: "fred",
|
||||||
bio: %{title: "Mr.", bio: "Bones"}
|
bio: %{title: "Mr.", bio: "Bones"}
|
||||||
})
|
})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{first_name_and_bob: "bob"} =
|
assert %{first_name_and_bob: "bob"} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(id == ^author.id)
|
|> Ash.Query.filter(id == ^author.id)
|
||||||
|> Ash.Query.load(:first_name_and_bob)
|
|> Ash.Query.load(:first_name_and_bob)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "concat calculation can be filtered on" do
|
test "concat calculation can be filtered on" do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "is", last_name: "match"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "is", last_name: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "not", last_name: "match"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "not", last_name: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
author_id = author.id
|
author_id = author.id
|
||||||
|
|
||||||
|
@ -62,18 +62,18 @@ defmodule AshSqlite.CalculationTest do
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.load(:full_name)
|
|> Ash.Query.load(:full_name)
|
||||||
|> Ash.Query.filter(full_name == "is match")
|
|> Ash.Query.filter(full_name == "is match")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "conditional calculations can be filtered on" do
|
test "conditional calculations can be filtered on" do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
author_id = author.id
|
author_id = author.id
|
||||||
|
|
||||||
|
@ -81,45 +81,45 @@ defmodule AshSqlite.CalculationTest do
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.load([:conditional_full_name, :full_name])
|
|> Ash.Query.load([:conditional_full_name, :full_name])
|
||||||
|> Ash.Query.filter(conditional_full_name == "(none)")
|
|> Ash.Query.filter(conditional_full_name == "(none)")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parameterized calculations can be filtered on" do
|
test "parameterized calculations can be filtered on" do
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{param_full_name: "tom holland"} =
|
assert %{param_full_name: "tom holland"} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.load(:param_full_name)
|
|> Ash.Query.load(:param_full_name)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
|
|
||||||
assert %{param_full_name: "tom~holland"} =
|
assert %{param_full_name: "tom~holland"} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.load(param_full_name: [separator: "~"])
|
|> Ash.Query.load(param_full_name: [separator: "~"])
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
|
|
||||||
assert %{} =
|
assert %{} =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(param_full_name(separator: "~") == "tom~holland")
|
|> Ash.Query.filter(param_full_name(separator: "~") == "tom~holland")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parameterized related calculations can be filtered on" do
|
test "parameterized related calculations can be filtered on" do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{title: "match"} =
|
assert %{title: "match"} =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(author.param_full_name(separator: "~") == "tom~holland")
|
|> Ash.Query.filter(author.param_full_name(separator: "~") == "tom~holland")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
|
|
||||||
assert %{title: "match"} =
|
assert %{title: "match"} =
|
||||||
Comment
|
Comment
|
||||||
|
@ -127,137 +127,94 @@ defmodule AshSqlite.CalculationTest do
|
||||||
author.param_full_name(separator: "~") == "tom~holland" and
|
author.param_full_name(separator: "~") == "tom~holland" and
|
||||||
author.param_full_name(separator: " ") == "tom holland"
|
author.param_full_name(separator: " ") == "tom holland"
|
||||||
)
|
)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "parameterized calculations can be sorted on" do
|
test "parameterized calculations can be sorted on" do
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom", last_name: "holland"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "abc", last_name: "def"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "abc", last_name: "def"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{first_name: "abc"}, %{first_name: "tom"}] =
|
assert [%{first_name: "abc"}, %{first_name: "tom"}] =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.sort(param_full_name: [separator: "~"])
|
|> Ash.Query.sort(param_full_name: [separator: "~"])
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculations using if and literal boolean results can run" do
|
test "calculations using if and literal boolean results can run" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.load(:was_created_in_the_last_month)
|
|> Ash.Query.load(:was_created_in_the_last_month)
|
||||||
|> Ash.Query.filter(was_created_in_the_last_month == true)
|
|> Ash.Query.filter(was_created_in_the_last_month == true)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "nested conditional calculations can be loaded" do
|
test "nested conditional calculations can be loaded" do
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{last_name: "holland"})
|
|> Ash.Changeset.for_create(:create, %{last_name: "holland"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "tom"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "tom"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{nested_conditional: "No First Name"}, %{nested_conditional: "No Last Name"}] =
|
assert [%{nested_conditional: "No First Name"}, %{nested_conditional: "No Last Name"}] =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.load(:nested_conditional)
|
|> Ash.Query.load(:nested_conditional)
|
||||||
|> Ash.Query.sort(:nested_conditional)
|
|> Ash.Query.sort(:nested_conditional)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loading a calculation loads its dependent loads" do
|
test "loading a calculation loads its dependent loads" do
|
||||||
user =
|
user =
|
||||||
User
|
User
|
||||||
|> Ash.Changeset.for_create(:create, %{is_active: true})
|
|> Ash.Changeset.for_create(:create, %{is_active: true})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
account =
|
account =
|
||||||
Account
|
Account
|
||||||
|> Ash.Changeset.for_create(:create, %{is_active: true})
|
|> Ash.Changeset.for_create(:create, %{is_active: true})
|
||||||
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|> Api.load!([:active])
|
|> Ash.load!([:active])
|
||||||
|
|
||||||
assert account.active
|
assert account.active
|
||||||
end
|
end
|
||||||
|
|
||||||
# describe "string join expression" do
|
|
||||||
# test "no nil values" do
|
|
||||||
# author =
|
|
||||||
# Author
|
|
||||||
# |> Ash.Changeset.for_create(:create, %{
|
|
||||||
# first_name: "Bill",
|
|
||||||
# last_name: "Jones",
|
|
||||||
# bio: %{title: "Mr.", bio: "Bones"}
|
|
||||||
# })
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert %{
|
|
||||||
# full_name_with_nils: "Bill Jones",
|
|
||||||
# full_name_with_nils_no_joiner: "BillJones"
|
|
||||||
# } =
|
|
||||||
# Author
|
|
||||||
# |> Ash.Query.filter(id == ^author.id)
|
|
||||||
# |> Ash.Query.load(:full_name_with_nils)
|
|
||||||
# |> Ash.Query.load(:full_name_with_nils_no_joiner)
|
|
||||||
# |> Api.read_one!()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# test "with nil value" do
|
|
||||||
# author =
|
|
||||||
# Author
|
|
||||||
# |> Ash.Changeset.for_create(:create, %{
|
|
||||||
# first_name: "Bill",
|
|
||||||
# bio: %{title: "Mr.", bio: "Bones"}
|
|
||||||
# })
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert %{
|
|
||||||
# full_name_with_nils: "Bill",
|
|
||||||
# full_name_with_nils_no_joiner: "Bill"
|
|
||||||
# } =
|
|
||||||
# Author
|
|
||||||
# |> Ash.Query.filter(id == ^author.id)
|
|
||||||
# |> Ash.Query.load(:full_name_with_nils)
|
|
||||||
# |> Ash.Query.load(:full_name_with_nils_no_joiner)
|
|
||||||
# |> Api.read_one!()
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
describe "-/1" do
|
describe "-/1" do
|
||||||
test "makes numbers negative" do
|
test "makes numbers negative" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", score: 42})
|
|> Ash.Changeset.for_create(:create, %{title: "match", score: 42})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{negative_score: -42}] =
|
assert [%{negative_score: -42}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.load(:negative_score)
|
|> Ash.Query.load(:negative_score)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "maps" do
|
describe "maps" do
|
||||||
test "maps can be constructed" do
|
test "maps can be constructed" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", score: 42})
|
|> Ash.Changeset.for_create(:create, %{title: "match", score: 42})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{score_map: %{negative_score: %{foo: -42}}}] =
|
assert [%{score_map: %{negative_score: %{foo: -42}}}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.load(:score_map)
|
|> Ash.Query.load(:score_map)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dependent calc" do
|
test "dependent calc" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", price: 10_024})
|
|> Ash.Changeset.for_create(:create, %{title: "match", price: 10_024})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post.get_by_id(post.id,
|
Post.get_by_id(post.id,
|
||||||
query: Post |> Ash.Query.select([:id]) |> Ash.Query.load([:price_string_with_currency_sign])
|
query: Post |> Ash.Query.select([:id]) |> Ash.Query.load([:price_string_with_currency_sign])
|
||||||
|
@ -267,10 +224,14 @@ defmodule AshSqlite.CalculationTest do
|
||||||
test "nested get_path works" do
|
test "nested get_path works" do
|
||||||
assert "thing" =
|
assert "thing" =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", price: 10_024, stuff: %{foo: %{bar: "thing"}}})
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
title: "match",
|
||||||
|
price: 10_024,
|
||||||
|
stuff: %{foo: %{bar: "thing"}}
|
||||||
|
})
|
||||||
|> Ash.Changeset.deselect(:stuff)
|
|> Ash.Changeset.deselect(:stuff)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|> Api.load!(:foo_bar_from_stuff)
|
|> Ash.load!(:foo_bar_from_stuff)
|
||||||
|> Map.get(:foo_bar_from_stuff)
|
|> Map.get(:foo_bar_from_stuff)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -282,19 +243,19 @@ defmodule AshSqlite.CalculationTest do
|
||||||
last_name: "Jones",
|
last_name: "Jones",
|
||||||
bio: %{title: "Mr.", bio: "Bones"}
|
bio: %{title: "Mr.", bio: "Bones"}
|
||||||
})
|
})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %AshSqlite.Test.Money{} =
|
assert %AshSqlite.Test.Money{} =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", price: 10_024})
|
|> Ash.Changeset.for_create(:create, %{title: "match", price: 10_024})
|
||||||
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|> Api.load!(:calc_returning_json)
|
|> Ash.load!(:calc_returning_json)
|
||||||
|> Map.get(:calc_returning_json)
|
|> Map.get(:calc_returning_json)
|
||||||
|
|
||||||
assert [%AshSqlite.Test.Money{}] =
|
assert [%AshSqlite.Test.Money{}] =
|
||||||
author
|
author
|
||||||
|> Api.load!(posts: :calc_returning_json)
|
|> Ash.load!(posts: :calc_returning_json)
|
||||||
|> Map.get(:posts)
|
|> Map.get(:posts)
|
||||||
|> Enum.map(&Map.get(&1, :calc_returning_json))
|
|> Enum.map(&Map.get(&1, :calc_returning_json))
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
defmodule AshSqlite.Test.CustomIndexTest do
|
defmodule AshSqlite.Test.CustomIndexTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "unique constraint errors are properly caught" do
|
test "unique constraint errors are properly caught" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "first", uniq_custom_one: "what", uniq_custom_two: "what2"})
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|> Api.create!()
|
title: "first",
|
||||||
|
uniq_custom_one: "what",
|
||||||
|
uniq_custom_two: "what2"
|
||||||
|
})
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
assert_raise Ash.Error.Invalid,
|
assert_raise Ash.Error.Invalid,
|
||||||
~r/Invalid value provided for uniq_custom_one: dude what the heck/,
|
~r/Invalid value provided for uniq_custom_one: dude what the heck/,
|
||||||
fn ->
|
fn ->
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
title: "first",
|
title: "first",
|
||||||
uniq_custom_one: "what",
|
uniq_custom_one: "what",
|
||||||
uniq_custom_two: "what2"
|
uniq_custom_two: "what2"
|
||||||
})
|
})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
defmodule AshSqlite.EmbeddableResourceTest do
|
defmodule AshSqlite.EmbeddableResourceTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Author, Bio, Post}
|
alias AshSqlite.Test.{Author, Bio, Post}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
%{post: post}
|
%{post: post}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculations can load json", %{post: post} do
|
test "calculations can load json", %{post: post} do
|
||||||
assert %{calc_returning_json: %AshSqlite.Test.Money{amount: 100, currency: :usd}} =
|
assert %{calc_returning_json: %AshSqlite.Test.Money{amount: 100, currency: :usd}} =
|
||||||
Api.load!(post, :calc_returning_json)
|
Ash.load!(post, :calc_returning_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "embeds with list attributes set to nil are loaded as nil" do
|
test "embeds with list attributes set to nil are loaded as nil" do
|
||||||
post =
|
post =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{bio: %Bio{list_of_strings: nil}})
|
|> Ash.Changeset.for_create(:create, %{bio: %Bio{list_of_strings: nil}})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert is_nil(post.bio.list_of_strings)
|
assert is_nil(post.bio.list_of_strings)
|
||||||
|
|
||||||
post = Api.reload!(post)
|
post = Ash.reload!(post)
|
||||||
|
|
||||||
assert is_nil(post.bio.list_of_strings)
|
assert is_nil(post.bio.list_of_strings)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
defmodule AshSqlite.EnumTest do
|
defmodule AshSqlite.EnumTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "valid values are properly inserted" do
|
test "valid values are properly inserted" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title", status: :open})
|
|> Ash.Changeset.for_create(:create, %{title: "title", status: :open})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
defmodule AshSqlite.FilterTest do
|
defmodule AshSqlite.FilterTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Author, Comment, Post}
|
alias AshSqlite.Test.{Author, Comment, Post}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
describe "with no filter applied" do
|
describe "with no filter applied" do
|
||||||
test "with no data" do
|
test "with no data" do
|
||||||
assert [] = Api.read!(Post)
|
assert [] = Ash.read!(Post)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with data" do
|
test "with data" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%Post{title: "title"}] = Api.read!(Post)
|
assert [%Post{title: "title"}] = Ash.read!(Post)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ defmodule AshSqlite.FilterTest do
|
||||||
assert_raise Ash.Error.Invalid, fn ->
|
assert_raise Ash.Error.Invalid, fn ->
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(id == "foo")
|
|> Ash.Query.filter(id == "foo")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -33,33 +33,33 @@ defmodule AshSqlite.FilterTest do
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title")
|
|> Ash.Query.filter(title == "title")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with data that matches" do
|
test "with data that matches" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title")
|
|> Ash.Query.filter(title == "title")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "title"}] = results
|
assert [%Post{title: "title"}] = results
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with some data that matches and some data that doesnt" do
|
test "with some data that matches and some data that doesnt" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "no_title")
|
|> Ash.Query.filter(title == "no_title")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
@ -67,18 +67,18 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data that doesn't match" do
|
test "with related data that doesn't match" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "not match"})
|
|> Ash.Changeset.for_create(:create, %{title: "not match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(comments.title == "match")
|
|> Ash.Query.filter(comments.title == "match")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
@ -86,31 +86,31 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data two steps away that matches" do
|
test "with related data two steps away that matches" do
|
||||||
author =
|
author =
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.new(%{first_name: "match"})
|
|> Ash.Changeset.for_create(:create, %{first_name: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "not match"})
|
|> Ash.Changeset.for_create(:create, %{title: "not match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(author.posts.linked_posts.title == "title")
|
|> Ash.Query.filter(author.posts.linked_posts.title == "title")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [_] = results
|
assert [_] = results
|
||||||
end
|
end
|
||||||
|
@ -118,18 +118,18 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data that does match" do
|
test "with related data that does match" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(comments.title == "match")
|
|> Ash.Query.filter(comments.title == "match")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "title"}] = results
|
assert [%Post{title: "title"}] = results
|
||||||
end
|
end
|
||||||
|
@ -137,23 +137,23 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data that does and doesn't match" do
|
test "with related data that does and doesn't match" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "not match"})
|
|> Ash.Changeset.for_create(:create, %{title: "not match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(comments.title == "match")
|
|> Ash.Query.filter(comments.title == "match")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "title"}] = results
|
assert [%Post{title: "title"}] = results
|
||||||
end
|
end
|
||||||
|
@ -162,22 +162,22 @@ defmodule AshSqlite.FilterTest do
|
||||||
describe "in" do
|
describe "in" do
|
||||||
test "it properly filters" do
|
test "it properly filters" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title1"})
|
|> Ash.Changeset.for_create(:create, %{title: "title1"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%Post{title: "title1"}, %Post{title: "title2"}] =
|
assert [%Post{title: "title1"}, %Post{title: "title2"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title in ["title1", "title2"])
|
|> Ash.Query.filter(title in ["title1", "title2"])
|
||||||
|> Ash.Query.sort(title: :asc)
|
|> Ash.Query.sort(title: :asc)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -186,37 +186,37 @@ defmodule AshSqlite.FilterTest do
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title" or score == 1)
|
|> Ash.Query.filter(title == "title" or score == 1)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with data that doesn't match" do
|
test "with data that doesn't match" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "no title", score: 2})
|
|> Ash.Changeset.for_create(:create, %{title: "no title", score: 2})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title" or score == 1)
|
|> Ash.Query.filter(title == "title" or score == 1)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with data that matches both conditions" do
|
test "with data that matches both conditions" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "title", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{score: 1, title: "nothing"})
|
|> Ash.Changeset.for_create(:create, %{score: 1, title: "nothing"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title" or score == 1)
|
|> Ash.Query.filter(title == "title" or score == 1)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|> Enum.sort_by(& &1.score)
|
|> Enum.sort_by(& &1.score)
|
||||||
|
|
||||||
assert [%Post{title: "title", score: 0}, %Post{title: "nothing", score: 1}] = results
|
assert [%Post{title: "title", score: 0}, %Post{title: "nothing", score: 1}] = results
|
||||||
|
@ -224,17 +224,17 @@ defmodule AshSqlite.FilterTest do
|
||||||
|
|
||||||
test "with data that matches one condition and data that matches nothing" do
|
test "with data that matches one condition and data that matches nothing" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "title", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{score: 2, title: "nothing"})
|
|> Ash.Changeset.for_create(:create, %{score: 2, title: "nothing"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "title" or score == 1)
|
|> Ash.Query.filter(title == "title" or score == 1)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|> Enum.sort_by(& &1.score)
|
|> Enum.sort_by(& &1.score)
|
||||||
|
|
||||||
assert [%Post{title: "title", score: 0}] = results
|
assert [%Post{title: "title", score: 0}] = results
|
||||||
|
@ -243,18 +243,18 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data in an or statement that matches, while basic filter doesn't match" do
|
test "with related data in an or statement that matches, while basic filter doesn't match" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "doesn't match"})
|
|> Ash.Changeset.for_create(:create, %{title: "doesn't match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "match" or comments.title == "match")
|
|> Ash.Query.filter(title == "match" or comments.title == "match")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "doesn't match"}] = results
|
assert [%Post{title: "doesn't match"}] = results
|
||||||
end
|
end
|
||||||
|
@ -262,18 +262,18 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data in an or statement that doesn't match, while basic filter does match" do
|
test "with related data in an or statement that doesn't match, while basic filter does match" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "doesn't match"})
|
|> Ash.Changeset.for_create(:create, %{title: "doesn't match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == "match" or comments.title == "match")
|
|> Ash.Query.filter(title == "match" or comments.title == "match")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "match"}] = results
|
assert [%Post{title: "match"}] = results
|
||||||
end
|
end
|
||||||
|
@ -281,25 +281,25 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "with related data and an inner join condition" do
|
test "with related data and an inner join condition" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title == comments.title)
|
|> Ash.Query.filter(title == comments.title)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "match"}] = results
|
assert [%Post{title: "match"}] = results
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(title != comments.title)
|
|> Ash.Query.filter(title != comments.title)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
@ -311,13 +311,13 @@ defmodule AshSqlite.FilterTest do
|
||||||
|> Ash.Changeset.for_create(:create,
|
|> Ash.Changeset.for_create(:create,
|
||||||
bio: %{title: "Dr.", bio: "Strange", years_of_experience: 10}
|
bio: %{title: "Dr.", bio: "Strange", years_of_experience: 10}
|
||||||
)
|
)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Author
|
Author
|
||||||
|> Ash.Changeset.for_create(:create,
|
|> Ash.Changeset.for_create(:create,
|
||||||
bio: %{title: "Highlander", bio: "There can be only one."}
|
bio: %{title: "Highlander", bio: "There can be only one."}
|
||||||
)
|
)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
@ -326,261 +326,172 @@ defmodule AshSqlite.FilterTest do
|
||||||
assert [%{bio: %{title: "Dr."}}] =
|
assert [%{bio: %{title: "Dr."}}] =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(bio[:title] == "Dr.")
|
|> Ash.Query.filter(bio[:title] == "Dr.")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "works using simple equality for integers" do
|
test "works using simple equality for integers" do
|
||||||
assert [%{bio: %{title: "Dr."}}] =
|
assert [%{bio: %{title: "Dr."}}] =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(bio[:years_of_experience] == 10)
|
|> Ash.Query.filter(bio[:years_of_experience] == 10)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculations that use embeds can be filtered on" do
|
test "calculations that use embeds can be filtered on" do
|
||||||
assert [%{bio: %{title: "Dr."}}] =
|
assert [%{bio: %{title: "Dr."}}] =
|
||||||
Author
|
Author
|
||||||
|> Ash.Query.filter(title == "Dr.")
|
|> Ash.Query.filter(title == "Dr.")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "basic expressions" do
|
describe "basic expressions" do
|
||||||
test "basic expressions work" do
|
test "basic expressions work" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", score: 4})
|
|> Ash.Changeset.for_create(:create, %{title: "match", score: 4})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "non_match", score: 2})
|
|> Ash.Changeset.for_create(:create, %{title: "non_match", score: 2})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "match"}] =
|
assert [%{title: "match"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(score + 1 == 5)
|
|> Ash.Query.filter(score + 1 == 5)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "case insensitive fields" do
|
describe "case insensitive fields" do
|
||||||
test "it matches case insensitively" do
|
test "it matches case insensitively" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", category: "FoObAr"})
|
|> Ash.Changeset.for_create(:create, %{title: "match", category: "FoObAr"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{category: "bazbuz"})
|
|> Ash.Changeset.for_create(:create, %{category: "bazbuz"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "match"}] =
|
assert [%{title: "match"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(category == "fOoBaR")
|
|> Ash.Query.filter(category == "fOoBaR")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# describe "contains/2" do
|
|
||||||
# test "it works when it matches" do
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "bazbuz"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert [%{title: "match"}] =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Query.filter(contains(title, "atc"))
|
|
||||||
# |> Api.read!()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# test "it works when a case insensitive string is provided as a value" do
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "bazbuz"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert [%{title: "match"}] =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Query.filter(contains(title, ^%Ash.CiString{string: "ATC"}))
|
|
||||||
# |> Api.read!()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# test "it works on a case insensitive column" do
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{category: "match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{category: "bazbuz"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert [%{category: %Ash.CiString{string: "match"}}] =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Query.filter(contains(category, ^"ATC"))
|
|
||||||
# |> Api.read!()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# test "it works on a case insensitive calculation" do
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{category: "match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{category: "bazbuz"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert [%{category: %Ash.CiString{string: "match"}}] =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Query.filter(contains(category_label, ^"ATC"))
|
|
||||||
# |> Api.read!()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# test "it works on related values" do
|
|
||||||
# post =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Comment
|
|
||||||
# |> Ash.Changeset.new(%{title: "abba"})
|
|
||||||
# |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# post2 =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Changeset.new(%{title: "no_match"})
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# Comment
|
|
||||||
# |> Ash.Changeset.new(%{title: "acca"})
|
|
||||||
# |> Ash.Changeset.manage_relationship(:post, post2, type: :append_and_remove)
|
|
||||||
# |> Api.create!()
|
|
||||||
|
|
||||||
# assert [%{title: "match"}] =
|
|
||||||
# Post
|
|
||||||
# |> Ash.Query.filter(contains(comments.title, ^"bb"))
|
|
||||||
# |> Api.read!()
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
describe "exists/2" do
|
describe "exists/2" do
|
||||||
test "it works with single relationships" do
|
test "it works with single relationships" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "abba"})
|
|> Ash.Changeset.for_create(:create, %{title: "abba"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
post2 =
|
post2 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "no_match"})
|
|> Ash.Changeset.for_create(:create, %{title: "no_match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "acca"})
|
|> Ash.Changeset.for_create(:create, %{title: "acca"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post2, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post2, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "match"}] =
|
assert [%{title: "match"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(exists(comments, title == ^"abba"))
|
|> Ash.Query.filter(exists(comments, title == ^"abba"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works with many to many relationships" do
|
test "it works with many to many relationships" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "a"})
|
|> Ash.Changeset.for_create(:create, %{title: "a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(exists(linked_posts, title == ^"a"))
|
|> Ash.Query.filter(exists(linked_posts, title == ^"a"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works with join association relationships" do
|
test "it works with join association relationships" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "a"})
|
|> Ash.Changeset.for_create(:create, %{title: "a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(exists(linked_posts, title == ^"a"))
|
|> Ash.Query.filter(exists(linked_posts, title == ^"a"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works with nested relationships as the path" do
|
test "it works with nested relationships as the path" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "a"})
|
|> Ash.Changeset.for_create(:create, %{title: "a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "comment"})
|
|> Ash.Changeset.for_create(:create, %{title: "comment"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(exists(linked_posts.comments, title == ^"comment"))
|
|> Ash.Query.filter(exists(linked_posts.comments, title == ^"comment"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works with an `at_path`" do
|
test "it works with an `at_path`" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "a"})
|
|> Ash.Changeset.for_create(:create, %{title: "a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
other_post =
|
other_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "other_a"})
|
|> Ash.Changeset.for_create(:create, %{title: "other_a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "comment"})
|
|> Ash.Changeset.for_create(:create, %{title: "comment"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "comment"})
|
|> Ash.Changeset.for_create(:create, %{title: "comment"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, other_post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, other_post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [other_post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [other_post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|
@ -588,7 +499,7 @@ defmodule AshSqlite.FilterTest do
|
||||||
linked_posts.title == "a" and
|
linked_posts.title == "a" and
|
||||||
linked_posts.exists(comments, title == ^"comment")
|
linked_posts.exists(comments, title == ^"comment")
|
||||||
)
|
)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|
@ -596,66 +507,66 @@ defmodule AshSqlite.FilterTest do
|
||||||
linked_posts.title == "a" and
|
linked_posts.title == "a" and
|
||||||
linked_posts.exists(comments, title == ^"comment")
|
linked_posts.exists(comments, title == ^"comment")
|
||||||
)
|
)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works with nested relationships inside of exists" do
|
test "it works with nested relationships inside of exists" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "a"})
|
|> Ash.Changeset.for_create(:create, %{title: "a"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "comment"})
|
|> Ash.Changeset.for_create(:create, %{title: "comment"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "b"})
|
|> Ash.Changeset.for_create(:create, %{title: "b"})
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "b"}] =
|
assert [%{title: "b"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(exists(linked_posts, comments.title == ^"comment"))
|
|> Ash.Query.filter(exists(linked_posts, comments.title == ^"comment"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "filtering on enum types" do
|
describe "filtering on enum types" do
|
||||||
test "it allows simple filtering" do
|
test "it allows simple filtering" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(status_enum: "open")
|
|> Ash.Changeset.for_create(:create, status_enum: "open")
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{status_enum: :open} =
|
assert %{status_enum: :open} =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(status_enum == ^"open")
|
|> Ash.Query.filter(status_enum == ^"open")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it allows simple filtering without casting" do
|
test "it allows simple filtering without casting" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(status_enum_no_cast: "open")
|
|> Ash.Changeset.for_create(:create, status_enum_no_cast: "open")
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert %{status_enum_no_cast: :open} =
|
assert %{status_enum_no_cast: :open} =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(status_enum_no_cast == ^"open")
|
|> Ash.Query.filter(status_enum_no_cast == ^"open")
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "atom filters" do
|
describe "atom filters" do
|
||||||
test "it works on matches" do
|
test "it works on matches" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
result =
|
result =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(type == :sponsored)
|
|> Ash.Query.filter(type == :sponsored)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "match"}] = result
|
assert [%Post{title: "match"}] = result
|
||||||
end
|
end
|
||||||
|
@ -664,20 +575,20 @@ defmodule AshSqlite.FilterTest do
|
||||||
describe "like" do
|
describe "like" do
|
||||||
test "like builds and matches" do
|
test "like builds and matches" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "MaTcH"})
|
|> Ash.Changeset.for_create(:create, %{title: "MaTcH"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(like(title, "%aTc%"))
|
|> Ash.Query.filter(like(title, "%aTc%"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "MaTcH"}] = results
|
assert [%Post{title: "MaTcH"}] = results
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(like(title, "%atc%"))
|
|> Ash.Query.filter(like(title, "%atc%"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] = results
|
assert [] = results
|
||||||
end
|
end
|
||||||
|
@ -686,20 +597,20 @@ defmodule AshSqlite.FilterTest do
|
||||||
describe "ilike" do
|
describe "ilike" do
|
||||||
test "ilike builds and matches" do
|
test "ilike builds and matches" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "MaTcH"})
|
|> Ash.Changeset.for_create(:create, %{title: "MaTcH"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(ilike(title, "%aTc%"))
|
|> Ash.Query.filter(ilike(title, "%aTc%"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "MaTcH"}] = results
|
assert [%Post{title: "MaTcH"}] = results
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(ilike(title, "%atc%"))
|
|> Ash.Query.filter(ilike(title, "%atc%"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{title: "MaTcH"}] = results
|
assert [%Post{title: "MaTcH"}] = results
|
||||||
end
|
end
|
||||||
|
@ -709,22 +620,22 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "double replacement works" do
|
test "double replacement works" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match", score: 4})
|
|> Ash.Changeset.for_create(:create, %{title: "match", score: 4})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "non_match", score: 2})
|
|> Ash.Changeset.for_create(:create, %{title: "non_match", score: 2})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: "match"}] =
|
assert [%{title: "match"}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(fragment("? = ?", title, ^post.title))
|
|> Ash.Query.filter(fragment("? = ?", title, ^post.title))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [] =
|
assert [] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(fragment("? = ?", title, "nope"))
|
|> Ash.Query.filter(fragment("? = ?", title, "nope"))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -732,13 +643,13 @@ defmodule AshSqlite.FilterTest do
|
||||||
test "it doesn't raise an error" do
|
test "it doesn't raise an error" do
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(not is_nil(popular_ratings.id))
|
|> Ash.Query.filter(not is_nil(popular_ratings.id))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it doesn't raise an error when nested" do
|
test "it doesn't raise an error when nested" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(not is_nil(comments.popular_ratings.id))
|
|> Ash.Query.filter(not is_nil(comments.popular_ratings.id))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.Test.LoadTest do
|
defmodule AshSqlite.Test.LoadTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Comment, Post}
|
alias AshSqlite.Test.{Comment, Post}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
@ -8,18 +8,18 @@ defmodule AshSqlite.Test.LoadTest do
|
||||||
assert %Post{comments: %Ash.NotLoaded{type: :relationship}} =
|
assert %Post{comments: %Ash.NotLoaded{type: :relationship}} =
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.load(:comments)
|
|> Ash.Query.load(:comments)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Post{comments: [%{title: "match"}]}] = results
|
assert [%Post{comments: [%{title: "match"}]}] = results
|
||||||
end
|
end
|
||||||
|
@ -28,18 +28,18 @@ defmodule AshSqlite.Test.LoadTest do
|
||||||
assert %Comment{post: %Ash.NotLoaded{type: :relationship}} =
|
assert %Comment{post: %Ash.NotLoaded{type: :relationship}} =
|
||||||
comment =
|
comment =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{})
|
|> Ash.Changeset.for_create(:create, %{})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "match"})
|
|> Ash.Changeset.for_create(:create, %{title: "match"})
|
||||||
|> Ash.Changeset.manage_relationship(:comments, [comment], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:comments, [comment], type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.load(:post)
|
|> Ash.Query.load(:post)
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [%Comment{post: %{title: "match"}}] = results
|
assert [%Comment{post: %{title: "match"}}] = results
|
||||||
end
|
end
|
||||||
|
@ -47,29 +47,29 @@ defmodule AshSqlite.Test.LoadTest do
|
||||||
test "many_to_many loads work" do
|
test "many_to_many loads work" do
|
||||||
source_post =
|
source_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "source"})
|
|> Ash.Changeset.for_create(:create, %{title: "source"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
destination_post =
|
destination_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "destination"})
|
|> Ash.Changeset.for_create(:create, %{title: "destination"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
destination_post2 =
|
destination_post2 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "destination"})
|
|> Ash.Changeset.for_create(:create, %{title: "destination"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
source_post
|
source_post
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2],
|
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2],
|
||||||
type: :append_and_remove
|
type: :append_and_remove
|
||||||
)
|
)
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
source_post
|
source_post
|
||||||
|> Api.load!(:linked_posts)
|
|> Ash.load!(:linked_posts)
|
||||||
|
|
||||||
assert %{linked_posts: [%{title: "destination"}, %{title: "destination"}]} = results
|
assert %{linked_posts: [%{title: "destination"}, %{title: "destination"}]} = results
|
||||||
end
|
end
|
||||||
|
@ -77,29 +77,29 @@ defmodule AshSqlite.Test.LoadTest do
|
||||||
test "many_to_many loads work when nested" do
|
test "many_to_many loads work when nested" do
|
||||||
source_post =
|
source_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "source"})
|
|> Ash.Changeset.for_create(:create, %{title: "source"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
destination_post =
|
destination_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "destination"})
|
|> Ash.Changeset.for_create(:create, %{title: "destination"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
source_post
|
source_post
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post],
|
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post],
|
||||||
type: :append_and_remove
|
type: :append_and_remove
|
||||||
)
|
)
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
destination_post
|
destination_post
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [source_post], type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:linked_posts, [source_post], type: :append_and_remove)
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
results =
|
results =
|
||||||
source_post
|
source_post
|
||||||
|> Api.load!(linked_posts: :linked_posts)
|
|> Ash.load!(linked_posts: :linked_posts)
|
||||||
|
|
||||||
assert %{linked_posts: [%{title: "destination", linked_posts: [%{title: "source"}]}]} =
|
assert %{linked_posts: [%{title: "destination", linked_posts: [%{title: "source"}]}]} =
|
||||||
results
|
results
|
||||||
|
@ -221,25 +221,25 @@ defmodule AshSqlite.Test.LoadTest do
|
||||||
test "loading many to many relationships on records works without loading its join relationship when using code interface" do
|
test "loading many to many relationships on records works without loading its join relationship when using code interface" do
|
||||||
source_post =
|
source_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "source"})
|
|> Ash.Changeset.for_create(:create, %{title: "source"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
destination_post =
|
destination_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "abc"})
|
|> Ash.Changeset.for_create(:create, %{title: "abc"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
destination_post2 =
|
destination_post2 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "def"})
|
|> Ash.Changeset.for_create(:create, %{title: "def"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
source_post
|
source_post
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2],
|
|> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2],
|
||||||
type: :append_and_remove
|
type: :append_and_remove
|
||||||
)
|
)
|
||||||
|> Api.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
assert %{linked_posts: [_, _]} = Post.get_by_id!(source_post.id, load: [:linked_posts])
|
assert %{linked_posts: [_, _]} = Post.get_by_id!(source_post.id, load: [:linked_posts])
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
defmodule AshSqlite.Test.ManualRelationshipsTest do
|
defmodule AshSqlite.Test.ManualRelationshipsTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Comment, Post}
|
alias AshSqlite.Test.{Comment, Post}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
describe "manual first" do
|
describe "manual first" do
|
||||||
test "relationships can be filtered on with no data" do
|
test "relationships can be filtered on with no data" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [] =
|
assert [] =
|
||||||
Post |> Ash.Query.filter(comments_containing_title.title == "title") |> Api.read!()
|
Post |> Ash.Query.filter(comments_containing_title.title == "title") |> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "relationships can be filtered on with data" do
|
test "relationships can be filtered on with data" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "no match"})
|
|> Ash.Changeset.for_create(:create, %{title: "no match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [_] =
|
assert [_] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(comments_containing_title.title == "title2")
|
|> Ash.Query.filter(comments_containing_title.title == "title2")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,44 +45,44 @@ defmodule AshSqlite.Test.ManualRelationshipsTest do
|
||||||
test "relationships can be filtered on with no data" do
|
test "relationships can be filtered on with no data" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "no match"})
|
|> Ash.Changeset.for_create(:create, %{title: "no match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [] =
|
assert [] =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(post.comments_containing_title.title == "title2")
|
|> Ash.Query.filter(post.comments_containing_title.title == "title2")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "relationships can be filtered on with data" do
|
test "relationships can be filtered on with data" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "no match"})
|
|> Ash.Changeset.for_create(:create, %{title: "no match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [_, _] =
|
assert [_, _] =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(post.comments_containing_title.title == "title2")
|
|> Ash.Query.filter(post.comments_containing_title.title == "title2")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,27 +90,27 @@ defmodule AshSqlite.Test.ManualRelationshipsTest do
|
||||||
test "relationships can be filtered on with data" do
|
test "relationships can be filtered on with data" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "title2"})
|
|> Ash.Changeset.for_create(:create, %{title: "title2"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "no match"})
|
|> Ash.Changeset.for_create(:create, %{title: "no match"})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [_, _] =
|
assert [_, _] =
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Query.filter(post.comments_containing_title.post.title == "title")
|
|> Ash.Query.filter(post.comments_containing_title.post.title == "title")
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
|
|
||||||
defmodule unquote(mod) do
|
defmodule unquote(mod) do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: nil,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -34,25 +35,17 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmacrop defapi(resources) do
|
defmacrop defdomain(resources) do
|
||||||
quote do
|
quote do
|
||||||
Code.compiler_options(ignore_module_conflict: true)
|
Code.compiler_options(ignore_module_conflict: true)
|
||||||
|
|
||||||
defmodule Registry do
|
defmodule Domain do
|
||||||
use Ash.Registry
|
use Ash.Domain
|
||||||
|
|
||||||
entries do
|
|
||||||
for resource <- unquote(resources) do
|
|
||||||
entry(resource)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule Api do
|
|
||||||
use Ash.Api
|
|
||||||
|
|
||||||
resources do
|
resources do
|
||||||
registry(Registry)
|
for resource <- unquote(resources) do
|
||||||
|
resource(resource)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -89,11 +82,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
Mix.shell(Mix.Shell.Process)
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -169,11 +162,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
Mix.shell(Mix.Shell.Process)
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -199,9 +192,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -228,9 +221,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -252,11 +245,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
send(self(), {:mix_shell_input, :yes?, true})
|
send(self(), {:mix_shell_input, :yes?, true})
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -277,11 +270,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
send(self(), {:mix_shell_input, :yes?, false})
|
send(self(), {:mix_shell_input, :yes?, false})
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -304,12 +297,12 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
send(self(), {:mix_shell_input, :yes?, true})
|
send(self(), {:mix_shell_input, :yes?, true})
|
||||||
send(self(), {:mix_shell_input, :prompt, "subject"})
|
send(self(), {:mix_shell_input, :prompt, "subject"})
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -335,11 +328,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
send(self(), {:mix_shell_input, :yes?, false})
|
send(self(), {:mix_shell_input, :yes?, false})
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -368,9 +361,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Post2])
|
defdomain([Post, Post2])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -401,11 +394,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
Mix.shell(Mix.Shell.Process)
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -435,14 +428,14 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
[api: Api]
|
[domain: Domain]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns code(1) if snapshots and resources don't fit", %{api: api} do
|
test "returns code(1) if snapshots and resources don't fit", %{domain: domain} do
|
||||||
assert catch_exit(
|
assert catch_exit(
|
||||||
AshSqlite.MigrationGenerator.generate(api,
|
AshSqlite.MigrationGenerator.generate(domain,
|
||||||
snapshot_path: "test_snapshot_path",
|
snapshot_path: "test_snapshot_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
check: true
|
check: true
|
||||||
|
@ -482,9 +475,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Post2])
|
defdomain([Post, Post2])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -517,9 +510,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Post2])
|
defdomain([Post, Post2])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -552,9 +545,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Post2])
|
defdomain([Post, Post2])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -578,7 +571,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -615,6 +608,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
|
|
||||||
defmodule Comment do
|
defmodule Comment do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: nil,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -634,6 +628,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
|
|
||||||
defmodule Post do
|
defmodule Post do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: nil,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -662,16 +657,16 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Comment])
|
defdomain([Post, Comment])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
format: false
|
format: false
|
||||||
)
|
)
|
||||||
|
|
||||||
[api: Api]
|
[domain: Domain]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it uses the relationship's table context if it is set" do
|
test "it uses the relationship's table context if it is set" do
|
||||||
|
@ -698,11 +693,10 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post])
|
defdomain([Post])
|
||||||
|
|
||||||
log =
|
|
||||||
capture_log(fn ->
|
capture_log(fn ->
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -735,6 +729,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
|
|
||||||
defmodule Comment do
|
defmodule Comment do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: nil,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -751,11 +746,11 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Comment])
|
defdomain([Post, Comment])
|
||||||
|
|
||||||
Mix.shell(Mix.Shell.Process)
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
@ -776,6 +771,7 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
|
|
||||||
defmodule Comment do
|
defmodule Comment do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: nil,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -792,9 +788,9 @@ defmodule AshSqlite.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defapi([Post, Comment])
|
defdomain([Post, Comment])
|
||||||
|
|
||||||
AshSqlite.MigrationGenerator.generate(Api,
|
AshSqlite.MigrationGenerator.generate(Domain,
|
||||||
snapshot_path: "test_snapshots_path",
|
snapshot_path: "test_snapshots_path",
|
||||||
migration_path: "test_migration_path",
|
migration_path: "test_migration_path",
|
||||||
quiet: true,
|
quiet: true,
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
defmodule AshSqlite.PolymorphismTest do
|
defmodule AshSqlite.PolymorphismTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post, Rating}
|
alias AshSqlite.Test.{Post, Rating}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "you can create related data" do
|
test "you can create related data" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, rating: %{score: 10})
|
|> Ash.Changeset.for_create(:create, rating: %{score: 10})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{score: 10}] =
|
assert [%{score: 10}] =
|
||||||
Rating
|
Rating
|
||||||
|> Ash.Query.set_context(%{data_layer: %{table: "post_ratings"}})
|
|> Ash.Query.set_context(%{data_layer: %{table: "post_ratings"}})
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "you can read related data" do
|
test "you can read related data" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_create(:create, rating: %{score: 10})
|
|> Ash.Changeset.for_create(:create, rating: %{score: 10})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{score: 10}] =
|
assert [%{score: 10}] =
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.load(:ratings)
|
|> Ash.Query.load(:ratings)
|
||||||
|> Api.read_one!()
|
|> Ash.read_one!()
|
||||||
|> Map.get(:ratings)
|
|> Map.get(:ratings)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
defmodule AshSqlite.Test.PrimaryKeyTest do
|
defmodule AshSqlite.Test.PrimaryKeyTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, IntegerPost, Post, PostView}
|
alias AshSqlite.Test.{IntegerPost, Post, PostView}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "creates record with integer primary key" do
|
test "creates record with integer primary key" do
|
||||||
assert %IntegerPost{} = IntegerPost |> Ash.Changeset.new(%{title: "title"}) |> Api.create!()
|
assert %IntegerPost{} =
|
||||||
|
IntegerPost |> Ash.Changeset.for_create(:create, %{title: "title"}) |> Ash.create!()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates record with uuid primary key" do
|
test "creates record with uuid primary key" do
|
||||||
assert %Post{} = Post |> Ash.Changeset.new(%{title: "title"}) |> Api.create!()
|
assert %Post{} = Post |> Ash.Changeset.for_create(:create, %{title: "title"}) |> Ash.create!()
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "resources without a primary key" do
|
describe "resources without a primary key" do
|
||||||
|
@ -18,12 +19,12 @@ defmodule AshSqlite.Test.PrimaryKeyTest do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_action(:create, %{title: "not very interesting"})
|
|> Ash.Changeset.for_action(:create, %{title: "not very interesting"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert {:ok, view} =
|
assert {:ok, view} =
|
||||||
PostView
|
PostView
|
||||||
|> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id})
|
|> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id})
|
||||||
|> Api.create()
|
|> Ash.create()
|
||||||
|
|
||||||
assert view.browser == :firefox
|
assert view.browser == :firefox
|
||||||
assert view.post_id == post.id
|
assert view.post_id == post.id
|
||||||
|
@ -34,14 +35,14 @@ defmodule AshSqlite.Test.PrimaryKeyTest do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.for_action(:create, %{title: "not very interesting"})
|
|> Ash.Changeset.for_action(:create, %{title: "not very interesting"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
expected =
|
expected =
|
||||||
PostView
|
PostView
|
||||||
|> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id})
|
|> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert {:ok, [actual]} = Api.read(PostView)
|
assert {:ok, [actual]} = Ash.read(PostView)
|
||||||
|
|
||||||
assert actual.time == expected.time
|
assert actual.time == expected.time
|
||||||
assert actual.browser == expected.browser
|
assert actual.browser == expected.browser
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
defmodule AshSqlite.SelectTest do
|
defmodule AshSqlite.SelectTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "values not selected in the query are not present in the response" do
|
test "values not selected in the query are not present in the response" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{title: nil}] = Api.read!(Ash.Query.select(Post, :id))
|
assert [%{title: %Ash.NotLoaded{}}] = Ash.read!(Ash.Query.select(Post, :id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
defmodule AshSqlite.SortTest do
|
defmodule AshSqlite.SortTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Comment, Post, PostLink}
|
alias AshSqlite.Test.{Comment, Post, PostLink}
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "multi-column sorts work" do
|
test "multi-column sorts work" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "bbb", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{title: "aaa", score: 0},
|
%{title: "aaa", score: 0},
|
||||||
%{title: "aaa", score: 1},
|
%{title: "aaa", score: 1},
|
||||||
%{title: "bbb"}
|
%{title: "bbb"}
|
||||||
] =
|
] =
|
||||||
Api.read!(
|
Ash.read!(
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :asc, score: :asc)
|
|> Ash.Query.sort(title: :asc, score: :asc)
|
||||||
)
|
)
|
||||||
|
@ -32,31 +32,31 @@ defmodule AshSqlite.SortTest do
|
||||||
test "multi-column sorts work on inclusion" do
|
test "multi-column sorts work on inclusion" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "bbb", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "aaa", likes: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", likes: 1})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "bbb", likes: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "bbb", likes: 1})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Comment
|
Comment
|
||||||
|> Ash.Changeset.new(%{title: "aaa", likes: 2})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", likes: 2})
|
||||||
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
posts =
|
posts =
|
||||||
Post
|
Post
|
||||||
|
@ -68,7 +68,7 @@ defmodule AshSqlite.SortTest do
|
||||||
|> Ash.Query.limit(1)
|
|> Ash.Query.limit(1)
|
||||||
)
|
)
|
||||||
|> Ash.Query.sort([:title, :score])
|
|> Ash.Query.sort([:title, :score])
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{title: "aaa", comments: [%{title: "aaa"}]},
|
%{title: "aaa", comments: [%{title: "aaa"}]},
|
||||||
|
@ -79,23 +79,23 @@ defmodule AshSqlite.SortTest do
|
||||||
|
|
||||||
test "multicolumn sort works with a select statement" do
|
test "multicolumn sort works with a select statement" do
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "bbb", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{title: "aaa", score: 0},
|
%{title: "aaa", score: 0},
|
||||||
%{title: "aaa", score: 1},
|
%{title: "aaa", score: 1},
|
||||||
%{title: "bbb"}
|
%{title: "bbb"}
|
||||||
] =
|
] =
|
||||||
Api.read!(
|
Ash.read!(
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :asc, score: :asc)
|
|> Ash.Query.sort(title: :asc, score: :asc)
|
||||||
|> Ash.Query.select([:title, :score])
|
|> Ash.Query.select([:title, :score])
|
||||||
|
@ -105,43 +105,43 @@ defmodule AshSqlite.SortTest do
|
||||||
test "sorting when joining to a many to many relationship sorts properly" do
|
test "sorting when joining to a many to many relationship sorts properly" do
|
||||||
post1 =
|
post1 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "aaa", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
post2 =
|
post2 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "bbb", score: 1})
|
|> Ash.Changeset.for_create(:create, %{title: "bbb", score: 1})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
post3 =
|
post3 =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "ccc", score: 0})
|
|> Ash.Changeset.for_create(:create, %{title: "ccc", score: 0})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
PostLink
|
PostLink
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:source_post, post1, type: :append)
|
|> Ash.Changeset.manage_relationship(:source_post, post1, type: :append)
|
||||||
|> Ash.Changeset.manage_relationship(:destination_post, post3, type: :append)
|
|> Ash.Changeset.manage_relationship(:destination_post, post3, type: :append)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
PostLink
|
PostLink
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:source_post, post2, type: :append)
|
|> Ash.Changeset.manage_relationship(:source_post, post2, type: :append)
|
||||||
|> Ash.Changeset.manage_relationship(:destination_post, post2, type: :append)
|
|> Ash.Changeset.manage_relationship(:destination_post, post2, type: :append)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
PostLink
|
PostLink
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.manage_relationship(:source_post, post3, type: :append)
|
|> Ash.Changeset.manage_relationship(:source_post, post3, type: :append)
|
||||||
|> Ash.Changeset.manage_relationship(:destination_post, post1, type: :append)
|
|> Ash.Changeset.manage_relationship(:destination_post, post1, type: :append)
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{title: "aaa"},
|
%{title: "aaa"},
|
||||||
%{title: "bbb"},
|
%{title: "bbb"},
|
||||||
%{title: "ccc"}
|
%{title: "ccc"}
|
||||||
] =
|
] =
|
||||||
Api.read!(
|
Ash.read!(
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :asc)
|
|> Ash.Query.sort(title: :asc)
|
||||||
|> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"])
|
|> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"])
|
||||||
|
@ -152,7 +152,7 @@ defmodule AshSqlite.SortTest do
|
||||||
%{title: "bbb"},
|
%{title: "bbb"},
|
||||||
%{title: "aaa"}
|
%{title: "aaa"}
|
||||||
] =
|
] =
|
||||||
Api.read!(
|
Ash.read!(
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :desc)
|
|> Ash.Query.sort(title: :desc)
|
||||||
|> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"] or title == "aaa")
|
|> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"] or title == "aaa")
|
||||||
|
@ -163,7 +163,7 @@ defmodule AshSqlite.SortTest do
|
||||||
%{title: "bbb"},
|
%{title: "bbb"},
|
||||||
%{title: "aaa"}
|
%{title: "aaa"}
|
||||||
] =
|
] =
|
||||||
Api.read!(
|
Ash.read!(
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.sort(title: :desc)
|
|> Ash.Query.sort(title: :desc)
|
||||||
|> Ash.Query.filter(
|
|> Ash.Query.filter(
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
defmodule AshSqlite.Test.Api do
|
|
||||||
@moduledoc false
|
|
||||||
use Ash.Api
|
|
||||||
|
|
||||||
resources do
|
|
||||||
registry(AshSqlite.Test.Registry)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.Test.Concat do
|
defmodule AshSqlite.Test.Concat do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Calculation
|
use Ash.Resource.Calculation
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
|
@ -11,16 +11,16 @@ defmodule AshSqlite.Test.Concat do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def expression(opts, %{separator: separator}) do
|
def expression(opts, %{arguments: %{separator: separator}}) do
|
||||||
Enum.reduce(opts[:keys], nil, fn key, expr ->
|
Enum.reduce(opts[:keys], nil, fn key, expr ->
|
||||||
if expr do
|
if expr do
|
||||||
if separator do
|
if separator do
|
||||||
Ash.Query.expr(^expr <> ^separator <> ref(^key))
|
expr(^expr <> ^separator <> ^ref(key))
|
||||||
else
|
else
|
||||||
Ash.Query.expr(^expr <> ref(^key))
|
expr(^expr <> ^ref(key))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Ash.Query.expr(ref(^key))
|
expr(^ref(key))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
23
test/support/domain.ex
Normal file
23
test/support/domain.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
defmodule AshSqlite.Test.Domain do
|
||||||
|
@moduledoc false
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource(AshSqlite.Test.Post)
|
||||||
|
resource(AshSqlite.Test.Comment)
|
||||||
|
resource(AshSqlite.Test.IntegerPost)
|
||||||
|
resource(AshSqlite.Test.Rating)
|
||||||
|
resource(AshSqlite.Test.PostLink)
|
||||||
|
resource(AshSqlite.Test.PostView)
|
||||||
|
resource(AshSqlite.Test.Author)
|
||||||
|
resource(AshSqlite.Test.Profile)
|
||||||
|
resource(AshSqlite.Test.User)
|
||||||
|
resource(AshSqlite.Test.Account)
|
||||||
|
resource(AshSqlite.Test.Organization)
|
||||||
|
resource(AshSqlite.Test.Manager)
|
||||||
|
end
|
||||||
|
|
||||||
|
authorization do
|
||||||
|
authorize(:when_requested)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +0,0 @@
|
||||||
defmodule AshSqlite.Test.Registry do
|
|
||||||
@moduledoc false
|
|
||||||
use Ash.Registry
|
|
||||||
|
|
||||||
entries do
|
|
||||||
entry(AshSqlite.Test.Post)
|
|
||||||
entry(AshSqlite.Test.Comment)
|
|
||||||
entry(AshSqlite.Test.IntegerPost)
|
|
||||||
entry(AshSqlite.Test.Rating)
|
|
||||||
entry(AshSqlite.Test.PostLink)
|
|
||||||
entry(AshSqlite.Test.PostView)
|
|
||||||
entry(AshSqlite.Test.Author)
|
|
||||||
entry(AshSqlite.Test.Profile)
|
|
||||||
entry(AshSqlite.Test.User)
|
|
||||||
entry(AshSqlite.Test.Account)
|
|
||||||
entry(AshSqlite.Test.Organization)
|
|
||||||
entry(AshSqlite.Test.Manager)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -13,7 +13,7 @@ defmodule AshSqlite.Test.Post.CommentsContainingTitle do
|
||||||
query
|
query
|
||||||
|> Ash.Query.filter(post_id in ^post_ids)
|
|> Ash.Query.filter(post_id in ^post_ids)
|
||||||
|> Ash.Query.filter(contains(title, post.title))
|
|> Ash.Query.filter(contains(title, post.title))
|
||||||
|> AshSqlite.Test.Api.read!(actor: actor, authorize?: authorize?)
|
|> Ash.read!(actor: actor, authorize?: authorize?)
|
||||||
|> Enum.group_by(& &1.post_id)}
|
|> Enum.group_by(& &1.post_id)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
defmodule AshSqlite.Test.Account do
|
defmodule AshSqlite.Test.Account do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: AshSqlite.DataLayer
|
use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id)
|
uuid_primary_key(:id)
|
||||||
attribute(:is_active, :boolean)
|
attribute(:is_active, :boolean, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
calculations do
|
||||||
calculate(
|
calculate(
|
||||||
:active,
|
:active,
|
||||||
:boolean,
|
:boolean,
|
||||||
expr(is_active)
|
expr(is_active),
|
||||||
|
public?: true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,6 +27,6 @@ defmodule AshSqlite.Test.Account do
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to(:user, AshSqlite.Test.User)
|
belongs_to(:user, AshSqlite.Test.User, public?: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Author do
|
defmodule AshSqlite.Test.Author do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -10,19 +11,20 @@ defmodule AshSqlite.Test.Author do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id, writable?: true)
|
uuid_primary_key(:id, writable?: true)
|
||||||
attribute(:first_name, :string)
|
attribute(:first_name, :string, public?: true)
|
||||||
attribute(:last_name, :string)
|
attribute(:last_name, :string, public?: true)
|
||||||
attribute(:bio, AshSqlite.Test.Bio)
|
attribute(:bio, AshSqlite.Test.Bio, public?: true)
|
||||||
attribute(:badges, {:array, :atom})
|
attribute(:badges, {:array, :atom}, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
has_one(:profile, AshSqlite.Test.Profile)
|
has_one(:profile, AshSqlite.Test.Profile, public?: true)
|
||||||
has_many(:posts, AshSqlite.Test.Post)
|
has_many(:posts, AshSqlite.Test.Post, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
calculations do
|
||||||
|
|
|
@ -3,15 +3,17 @@ defmodule AshSqlite.Test.Bio do
|
||||||
use Ash.Resource, data_layer: :embedded
|
use Ash.Resource, data_layer: :embedded
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
attribute(:title, :string)
|
attribute(:title, :string, public?: true)
|
||||||
attribute(:bio, :string)
|
attribute(:bio, :string, public?: true)
|
||||||
attribute(:years_of_experience, :integer)
|
attribute(:years_of_experience, :integer, public?: true)
|
||||||
|
|
||||||
attribute :list_of_strings, {:array, :string} do
|
attribute :list_of_strings, {:array, :string} do
|
||||||
|
public?(true)
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
default(nil)
|
default(nil)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Comment do
|
defmodule AshSqlite.Test.Comment do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer,
|
data_layer: AshSqlite.DataLayer,
|
||||||
authorizers: [
|
authorizers: [
|
||||||
Ash.Policy.Authorizer
|
Ash.Policy.Authorizer
|
||||||
|
@ -23,6 +24,7 @@ defmodule AshSqlite.Test.Comment do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:read, :update, :destroy])
|
defaults([:read, :update, :destroy])
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
|
@ -35,22 +37,24 @@ defmodule AshSqlite.Test.Comment do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id)
|
uuid_primary_key(:id)
|
||||||
attribute(:title, :string)
|
attribute(:title, :string, public?: true)
|
||||||
attribute(:likes, :integer)
|
attribute(:likes, :integer, public?: true)
|
||||||
attribute(:arbitrary_timestamp, :utc_datetime_usec)
|
attribute(:arbitrary_timestamp, :utc_datetime_usec, public?: true)
|
||||||
create_timestamp(:created_at, writable?: true)
|
create_timestamp(:created_at, writable?: true, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to(:post, AshSqlite.Test.Post)
|
belongs_to(:post, AshSqlite.Test.Post, public?: true)
|
||||||
belongs_to(:author, AshSqlite.Test.Author)
|
belongs_to(:author, AshSqlite.Test.Author, public?: true)
|
||||||
|
|
||||||
has_many(:ratings, AshSqlite.Test.Rating,
|
has_many(:ratings, AshSqlite.Test.Rating,
|
||||||
|
public?: true,
|
||||||
destination_attribute: :resource_id,
|
destination_attribute: :resource_id,
|
||||||
relationship_context: %{data_layer: %{table: "comment_ratings"}}
|
relationship_context: %{data_layer: %{table: "comment_ratings"}}
|
||||||
)
|
)
|
||||||
|
|
||||||
has_many(:popular_ratings, AshSqlite.Test.Rating,
|
has_many(:popular_ratings, AshSqlite.Test.Rating,
|
||||||
|
public?: true,
|
||||||
destination_attribute: :resource_id,
|
destination_attribute: :resource_id,
|
||||||
relationship_context: %{data_layer: %{table: "comment_ratings"}},
|
relationship_context: %{data_layer: %{table: "comment_ratings"}},
|
||||||
filter: expr(score > 5)
|
filter: expr(score > 5)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.IntegerPost do
|
defmodule AshSqlite.Test.IntegerPost do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -9,11 +10,12 @@ defmodule AshSqlite.Test.IntegerPost do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
integer_primary_key(:id)
|
integer_primary_key(:id)
|
||||||
attribute(:title, :string)
|
attribute(:title, :string, public?: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Manager do
|
defmodule AshSqlite.Test.Manager do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -9,6 +10,7 @@ defmodule AshSqlite.Test.Manager do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:read, :update, :destroy])
|
defaults([:read, :update, :destroy])
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
|
@ -25,14 +27,15 @@ defmodule AshSqlite.Test.Manager do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id)
|
uuid_primary_key(:id)
|
||||||
attribute(:name, :string)
|
attribute(:name, :string, public?: true)
|
||||||
attribute(:code, :string, allow_nil?: false)
|
attribute(:code, :string, allow_nil?: false, public?: true)
|
||||||
attribute(:must_be_present, :string, allow_nil?: false)
|
attribute(:must_be_present, :string, allow_nil?: false, public?: true)
|
||||||
attribute(:role, :string)
|
attribute(:role, :string, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to :organization, AshSqlite.Test.Organization do
|
belongs_to :organization, AshSqlite.Test.Organization do
|
||||||
|
public?(true)
|
||||||
attribute_writable?(true)
|
attribute_writable?(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Organization do
|
defmodule AshSqlite.Test.Organization do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -9,17 +10,12 @@ defmodule AshSqlite.Test.Organization do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id, writable?: true)
|
uuid_primary_key(:id, writable?: true)
|
||||||
attribute(:name, :string)
|
attribute(:name, :string, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
# relationships do
|
|
||||||
# has_many(:users, AshSqlite.Test.User)
|
|
||||||
# has_many(:posts, AshSqlite.Test.Post)
|
|
||||||
# has_many(:managers, AshSqlite.Test.Manager)
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Post do
|
defmodule AshSqlite.Test.Post do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer,
|
data_layer: AshSqlite.DataLayer,
|
||||||
authorizers: [
|
authorizers: [
|
||||||
Ash.Policy.Authorizer
|
Ash.Policy.Authorizer
|
||||||
|
@ -31,6 +32,7 @@ defmodule AshSqlite.Test.Post do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:update, :destroy])
|
defaults([:update, :destroy])
|
||||||
|
|
||||||
read :read do
|
read :read do
|
||||||
|
@ -66,71 +68,82 @@ defmodule AshSqlite.Test.Post do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id, writable?: true)
|
uuid_primary_key(:id, writable?: true)
|
||||||
attribute(:title, :string)
|
attribute(:title, :string, public?: true)
|
||||||
attribute(:score, :integer)
|
attribute(:score, :integer, public?: true)
|
||||||
attribute(:public, :boolean)
|
attribute(:public, :boolean, public?: true)
|
||||||
attribute(:category, :ci_string)
|
attribute(:category, :ci_string, public?: true)
|
||||||
attribute(:type, :atom, default: :sponsored, private?: true, writable?: false)
|
attribute(:type, :atom, default: :sponsored, writable?: false)
|
||||||
attribute(:price, :integer)
|
attribute(:price, :integer, public?: true)
|
||||||
attribute(:decimal, :decimal, default: Decimal.new(0))
|
attribute(:decimal, :decimal, default: Decimal.new(0), public?: true)
|
||||||
attribute(:status, AshSqlite.Test.Types.Status)
|
attribute(:status, AshSqlite.Test.Types.Status, public?: true)
|
||||||
attribute(:status_enum, AshSqlite.Test.Types.StatusEnum)
|
attribute(:status_enum, AshSqlite.Test.Types.StatusEnum, public?: true)
|
||||||
attribute(:status_enum_no_cast, AshSqlite.Test.Types.StatusEnumNoCast, source: :status_enum)
|
|
||||||
attribute(:stuff, :map)
|
attribute(:status_enum_no_cast, AshSqlite.Test.Types.StatusEnumNoCast,
|
||||||
attribute(:uniq_one, :string)
|
source: :status_enum,
|
||||||
attribute(:uniq_two, :string)
|
public?: true
|
||||||
attribute(:uniq_custom_one, :string)
|
)
|
||||||
attribute(:uniq_custom_two, :string)
|
|
||||||
|
attribute(:stuff, :map, public?: true)
|
||||||
|
attribute(:uniq_one, :string, public?: true)
|
||||||
|
attribute(:uniq_two, :string, public?: true)
|
||||||
|
attribute(:uniq_custom_one, :string, public?: true)
|
||||||
|
attribute(:uniq_custom_two, :string, public?: true)
|
||||||
create_timestamp(:created_at)
|
create_timestamp(:created_at)
|
||||||
update_timestamp(:updated_at)
|
update_timestamp(:updated_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
code_interface do
|
code_interface do
|
||||||
define_for(AshSqlite.Test.Api)
|
|
||||||
define(:get_by_id, action: :read, get_by: [:id])
|
define(:get_by_id, action: :read, get_by: [:id])
|
||||||
define(:increment_score, args: [{:optional, :amount}])
|
define(:increment_score, args: [{:optional, :amount}])
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to :organization, AshSqlite.Test.Organization do
|
belongs_to :organization, AshSqlite.Test.Organization do
|
||||||
|
public?(true)
|
||||||
attribute_writable?(true)
|
attribute_writable?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
belongs_to(:author, AshSqlite.Test.Author)
|
belongs_to(:author, AshSqlite.Test.Author, public?: true)
|
||||||
|
|
||||||
has_many(:comments, AshSqlite.Test.Comment, destination_attribute: :post_id)
|
has_many(:comments, AshSqlite.Test.Comment, destination_attribute: :post_id, public?: true)
|
||||||
|
|
||||||
has_many :comments_matching_post_title, AshSqlite.Test.Comment do
|
has_many :comments_matching_post_title, AshSqlite.Test.Comment do
|
||||||
|
public?(true)
|
||||||
filter(expr(title == parent_expr(title)))
|
filter(expr(title == parent_expr(title)))
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many :popular_comments, AshSqlite.Test.Comment do
|
has_many :popular_comments, AshSqlite.Test.Comment do
|
||||||
|
public?(true)
|
||||||
destination_attribute(:post_id)
|
destination_attribute(:post_id)
|
||||||
filter(expr(likes > 10))
|
filter(expr(likes > 10))
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many :comments_containing_title, AshSqlite.Test.Comment do
|
has_many :comments_containing_title, AshSqlite.Test.Comment do
|
||||||
|
public?(true)
|
||||||
manual(AshSqlite.Test.Post.CommentsContainingTitle)
|
manual(AshSqlite.Test.Post.CommentsContainingTitle)
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many(:ratings, AshSqlite.Test.Rating,
|
has_many(:ratings, AshSqlite.Test.Rating,
|
||||||
|
public?: true,
|
||||||
destination_attribute: :resource_id,
|
destination_attribute: :resource_id,
|
||||||
relationship_context: %{data_layer: %{table: "post_ratings"}}
|
relationship_context: %{data_layer: %{table: "post_ratings"}}
|
||||||
)
|
)
|
||||||
|
|
||||||
has_many(:post_links, AshSqlite.Test.PostLink,
|
has_many(:post_links, AshSqlite.Test.PostLink,
|
||||||
|
public?: true,
|
||||||
destination_attribute: :source_post_id,
|
destination_attribute: :source_post_id,
|
||||||
filter: [state: :active]
|
filter: [state: :active]
|
||||||
)
|
)
|
||||||
|
|
||||||
many_to_many(:linked_posts, __MODULE__,
|
many_to_many(:linked_posts, __MODULE__,
|
||||||
|
public?: true,
|
||||||
through: AshSqlite.Test.PostLink,
|
through: AshSqlite.Test.PostLink,
|
||||||
join_relationship: :post_links,
|
join_relationship: :post_links,
|
||||||
source_attribute_on_join_resource: :source_post_id,
|
source_attribute_on_join_resource: :source_post_id,
|
||||||
destination_attribute_on_join_resource: :destination_post_id
|
destination_attribute_on_join_resource: :destination_post_id
|
||||||
)
|
)
|
||||||
|
|
||||||
has_many(:views, AshSqlite.Test.PostView)
|
has_many(:views, AshSqlite.Test.PostView, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
validations do
|
validations do
|
||||||
|
@ -191,10 +204,10 @@ end
|
||||||
|
|
||||||
defmodule CalculatePostPriceString do
|
defmodule CalculatePostPriceString do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Calculation
|
use Ash.Resource.Calculation
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def select(_, _, _), do: [:price]
|
def load(_, _, _), do: [:price]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def calculate(records, _, _) do
|
def calculate(records, _, _) do
|
||||||
|
@ -208,7 +221,7 @@ end
|
||||||
|
|
||||||
defmodule CalculatePostPriceStringWithSymbol do
|
defmodule CalculatePostPriceStringWithSymbol do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Calculation
|
use Ash.Resource.Calculation
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def load(_, _, _), do: [:price_string]
|
def load(_, _, _), do: [:price_string]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.PostLink do
|
defmodule AshSqlite.Test.PostLink do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -9,6 +10,7 @@ defmodule AshSqlite.Test.PostLink do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ defmodule AshSqlite.Test.PostLink do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
attribute :state, :atom do
|
attribute :state, :atom do
|
||||||
|
public?(true)
|
||||||
constraints(one_of: [:active, :archived])
|
constraints(one_of: [:active, :archived])
|
||||||
default(:active)
|
default(:active)
|
||||||
end
|
end
|
||||||
|
@ -25,11 +28,13 @@ defmodule AshSqlite.Test.PostLink do
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to :source_post, AshSqlite.Test.Post do
|
belongs_to :source_post, AshSqlite.Test.Post do
|
||||||
|
public?(true)
|
||||||
allow_nil?(false)
|
allow_nil?(false)
|
||||||
primary_key?(true)
|
primary_key?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
belongs_to :destination_post, AshSqlite.Test.Post do
|
belongs_to :destination_post, AshSqlite.Test.Post do
|
||||||
|
public?(true)
|
||||||
allow_nil?(false)
|
allow_nil?(false)
|
||||||
primary_key?(true)
|
primary_key?(true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
defmodule AshSqlite.Test.PostView do
|
defmodule AshSqlite.Test.PostView do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: AshSqlite.DataLayer
|
use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read])
|
defaults([:create, :read])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
create_timestamp(:time)
|
create_timestamp(:time)
|
||||||
attribute(:browser, :atom, constraints: [one_of: [:firefox, :chrome, :edge]])
|
attribute(:browser, :atom, constraints: [one_of: [:firefox, :chrome, :edge]], public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to :post, AshSqlite.Test.Post do
|
belongs_to :post, AshSqlite.Test.Post do
|
||||||
|
public?(true)
|
||||||
allow_nil?(false)
|
allow_nil?(false)
|
||||||
attribute_writable?(true)
|
attribute_writable?(true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Profile do
|
defmodule AshSqlite.Test.Profile do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -10,14 +11,15 @@ defmodule AshSqlite.Test.Profile do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id, writable?: true)
|
uuid_primary_key(:id, writable?: true)
|
||||||
attribute(:description, :string)
|
attribute(:description, :string, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to(:author, AshSqlite.Test.Author)
|
belongs_to(:author, AshSqlite.Test.Author, public?: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule AshSqlite.Test.Rating do
|
defmodule AshSqlite.Test.Rating do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
|
domain: AshSqlite.Test.Domain,
|
||||||
data_layer: AshSqlite.DataLayer
|
data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -9,12 +10,13 @@ defmodule AshSqlite.Test.Rating do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id)
|
uuid_primary_key(:id)
|
||||||
attribute(:score, :integer)
|
attribute(:score, :integer, public?: true)
|
||||||
attribute(:resource_id, :uuid)
|
attribute(:resource_id, :uuid, public?: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
defmodule AshSqlite.Test.User do
|
defmodule AshSqlite.Test.User do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: AshSqlite.DataLayer
|
use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
|
default_accept(:*)
|
||||||
defaults([:create, :read, :update, :destroy])
|
defaults([:create, :read, :update, :destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key(:id)
|
uuid_primary_key(:id)
|
||||||
attribute(:is_active, :boolean)
|
attribute(:is_active, :boolean, public?: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
sqlite do
|
sqlite do
|
||||||
|
@ -17,7 +18,7 @@ defmodule AshSqlite.Test.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to(:organization, AshSqlite.Test.Organization)
|
belongs_to(:organization, AshSqlite.Test.Organization, public?: true)
|
||||||
has_many(:accounts, AshSqlite.Test.Account)
|
has_many(:accounts, AshSqlite.Test.Account, public?: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,11 +5,13 @@ defmodule AshSqlite.Test.Money do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
attribute :amount, :integer do
|
attribute :amount, :integer do
|
||||||
|
public?(true)
|
||||||
allow_nil?(false)
|
allow_nil?(false)
|
||||||
constraints(min: 0)
|
constraints(min: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :currency, :atom do
|
attribute :currency, :atom do
|
||||||
|
public?(true)
|
||||||
constraints(one_of: [:eur, :usd])
|
constraints(one_of: [:eur, :usd])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.Test.TypeTest do
|
defmodule AshSqlite.Test.TypeTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
@ -9,6 +9,6 @@ defmodule AshSqlite.Test.TypeTest do
|
||||||
|
|
||||||
Post
|
Post
|
||||||
|> Ash.Query.filter(fragment("? = ?", id, type(^uuid, :uuid)))
|
|> Ash.Query.filter(fragment("? = ?", id, type(^uuid, :uuid)))
|
||||||
|> Api.read!()
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,34 +1,43 @@
|
||||||
defmodule AshSqlite.Test.UniqueIdentityTest do
|
defmodule AshSqlite.Test.UniqueIdentityTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
test "unique constraint errors are properly caught" do
|
test "unique constraint errors are properly caught" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title"})
|
|> Ash.Changeset.for_create(:create, %{title: "title"})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
assert_raise Ash.Error.Invalid,
|
assert_raise Ash.Error.Invalid,
|
||||||
~r/Invalid value provided for id: has already been taken/,
|
~r/Invalid value provided for id: has already been taken/,
|
||||||
fn ->
|
fn ->
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{id: post.id})
|
|> Ash.Changeset.for_create(:create, %{id: post.id})
|
||||||
|> Api.create!()
|
|> Ash.create!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a unique constraint can be used to upsert when the resource has a base filter" do
|
test "a unique constraint can be used to upsert when the resource has a base filter" do
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title", uniq_one: "fred", uniq_two: "astair", price: 10})
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|> Api.create!()
|
title: "title",
|
||||||
|
uniq_one: "fred",
|
||||||
|
uniq_two: "astair",
|
||||||
|
price: 10
|
||||||
|
})
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
new_post =
|
new_post =
|
||||||
Post
|
Post
|
||||||
|> Ash.Changeset.new(%{title: "title2", uniq_one: "fred", uniq_two: "astair"})
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|> Api.create!(upsert?: true, upsert_identity: :uniq_one_and_two)
|
title: "title2",
|
||||||
|
uniq_one: "fred",
|
||||||
|
uniq_two: "astair"
|
||||||
|
})
|
||||||
|
|> Ash.create!(upsert?: true, upsert_identity: :uniq_one_and_two)
|
||||||
|
|
||||||
assert new_post.id == post.id
|
assert new_post.id == post.id
|
||||||
assert new_post.price == 10
|
assert new_post.price == 10
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshSqlite.Test.UpsertTest do
|
defmodule AshSqlite.Test.UpsertTest do
|
||||||
use AshSqlite.RepoCase, async: false
|
use AshSqlite.RepoCase, async: false
|
||||||
alias AshSqlite.Test.{Api, Post}
|
alias AshSqlite.Test.Post
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ defmodule AshSqlite.Test.UpsertTest do
|
||||||
id: id,
|
id: id,
|
||||||
title: "title2"
|
title: "title2"
|
||||||
})
|
})
|
||||||
|> Api.create!(upsert?: true)
|
|> Ash.create!(upsert?: true)
|
||||||
|
|
||||||
assert new_post.id == id
|
assert new_post.id == id
|
||||||
assert new_post.created_at == new_post.updated_at
|
assert new_post.created_at == new_post.updated_at
|
||||||
|
@ -24,7 +24,7 @@ defmodule AshSqlite.Test.UpsertTest do
|
||||||
id: id,
|
id: id,
|
||||||
title: "title2"
|
title: "title2"
|
||||||
})
|
})
|
||||||
|> Api.create!(upsert?: true)
|
|> Ash.create!(upsert?: true)
|
||||||
|
|
||||||
assert updated_post.id == id
|
assert updated_post.id == id
|
||||||
assert updated_post.created_at == new_post.created_at
|
assert updated_post.created_at == new_post.created_at
|
||||||
|
@ -40,7 +40,7 @@ defmodule AshSqlite.Test.UpsertTest do
|
||||||
id: id,
|
id: id,
|
||||||
title: "title2"
|
title: "title2"
|
||||||
})
|
})
|
||||||
|> Api.create!(upsert?: true)
|
|> Ash.create!(upsert?: true)
|
||||||
|
|
||||||
assert new_post.id == id
|
assert new_post.id == id
|
||||||
assert new_post.created_at == new_post.updated_at
|
assert new_post.created_at == new_post.updated_at
|
||||||
|
@ -52,7 +52,7 @@ defmodule AshSqlite.Test.UpsertTest do
|
||||||
title: "title2",
|
title: "title2",
|
||||||
decimal: Decimal.new(5)
|
decimal: Decimal.new(5)
|
||||||
})
|
})
|
||||||
|> Api.create!(upsert?: true)
|
|> Ash.create!(upsert?: true)
|
||||||
|
|
||||||
assert updated_post.id == id
|
assert updated_post.id == id
|
||||||
assert Decimal.equal?(updated_post.decimal, Decimal.new(5))
|
assert Decimal.equal?(updated_post.decimal, Decimal.new(5))
|
||||||
|
|
Loading…
Reference in a new issue