improvement: support Ash 3.0, leverage ash_sql package

This commit is contained in:
Zach Daniel 2024-04-01 12:18:42 -04:00
parent 52a5051398
commit c12be48a5b
63 changed files with 1469 additions and 4778 deletions

View file

@ -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**

View file

@ -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

View file

@ -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

View 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>

View file

@ -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

View file

@ -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
... ...

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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]},

View file

@ -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"},
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -1,8 +0,0 @@
defmodule AshSqlite.Test.Api do
@moduledoc false
use Ash.Api
resources do
registry(AshSqlite.Test.Registry)
end
end

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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))