Skip to content

Commit 3385bd2

Browse files
improvement: add storage type option (#342)
--------- Co-authored-by: Zach Daniel <[email protected]>
1 parent f0d07a3 commit 3385bd2

File tree

12 files changed

+195
-5
lines changed

12 files changed

+195
-5
lines changed

.formatter.exs

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ spark_locals_without_parens = [
4343
skip_unique_indexes: 1,
4444
statement: 1,
4545
statement: 2,
46+
storage_types: 1,
4647
table: 1,
4748
template: 1,
4849
unique: 1,

documentation/dsls/DSL:-AshPostgres.DataLayer.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ end
4040
|------|------|---------|------|
4141
| [`repo`](#postgres-repo){: #postgres-repo .spark-required} | `module \| (any, any -> any)` | | The repo that will be used to fetch your data. See the `AshPostgres.Repo` documentation for more. Can also be a function that takes a resource and a type `:read \| :mutate` and returns the repo |
4242
| [`migrate?`](#postgres-migrate?){: #postgres-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` |
43+
| [`storage_types`](#postgres-storage_types){: #postgres-storage_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto type that should be used for that attribute. Only necessary if you need to override the defaults. |
4344
| [`migration_types`](#postgres-migration_types){: #postgres-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. |
4445
| [`migration_defaults`](#postgres-migration_defaults){: #postgres-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\\"`. |
4546
| [`calculations_to_sql`](#postgres-calculations_to_sql){: #postgres-calculations_to_sql } | `keyword` | | A keyword list of calculations and their SQL representation. Used when creating unique indexes for identities over calculations |
@@ -114,7 +115,7 @@ index ["column", "column2"], unique: true, where: "thing = TRUE"
114115
| [`prefix`](#postgres-custom_indexes-index-prefix){: #postgres-custom_indexes-index-prefix } | `String.t` | | specify an optional prefix for the index. |
115116
| [`where`](#postgres-custom_indexes-index-where){: #postgres-custom_indexes-index-where } | `String.t` | | specify conditions for a partial index. |
116117
| [`include`](#postgres-custom_indexes-index-include){: #postgres-custom_indexes-index-include } | `list(String.t)` | | specify fields for a covering index. This is not supported by all databases. For more information on PostgreSQL support, please read the official docs. |
117-
| [`nulls_distinct`](#postgres-custom_indexes-index-nulls_distinct){: #postgres-custom_indexes-index-nulls_distinct } | `boolean` | `true` | specify whether null values should be considered distinct for a unique index. |
118+
| [`nulls_distinct`](#postgres-custom_indexes-index-nulls_distinct){: #postgres-custom_indexes-index-nulls_distinct } | `boolean` | `true` | specify whether null values should be considered distinct for a unique index. Requires PostgreSQL 15 or later |
118119
| [`message`](#postgres-custom_indexes-index-message){: #postgres-custom_indexes-index-message } | `String.t` | | A custom message to use for unique indexes that have been violated |
119120
| [`all_tenants?`](#postgres-custom_indexes-index-all_tenants?){: #postgres-custom_indexes-index-all_tenants? } | `boolean` | `false` | Whether or not the index should factor in the multitenancy attribute or not. |
120121

lib/custom_index.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ defmodule AshPostgres.CustomIndex do
6262
],
6363
nulls_distinct: [
6464
type: :boolean,
65-
doc: "specify whether null values should be considered distinct for a unique index. Requires PostgreSQL 15 or later",
65+
doc:
66+
"specify whether null values should be considered distinct for a unique index. Requires PostgreSQL 15 or later",
6667
default: true
6768
],
6869
message: [

lib/data_layer.ex

+6
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ defmodule AshPostgres.DataLayer do
287287
doc:
288288
"Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations`"
289289
],
290+
storage_types: [
291+
type: :keyword_list,
292+
default: [],
293+
doc:
294+
"A keyword list of attribute names to the ecto type that should be used for that attribute. Only necessary if you need to override the defaults."
295+
],
290296
migration_types: [
291297
type: :keyword_list,
292298
default: [],

lib/data_layer/info.ex

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ defmodule AshPostgres.DataLayer.Info do
7878
Extension.get_opt(resource, [:postgres], :migration_types, [])
7979
end
8080

81+
@doc "A keyword list of customized storage types"
82+
def storage_types(resource) do
83+
Extension.get_opt(resource, [:postgres], :storage_types, [])
84+
end
85+
8186
@doc "A keyword list of customized migration defaults"
8287
def migration_defaults(resource) do
8388
Extension.get_opt(resource, [:postgres], :migration_defaults, [])

lib/sql_implementation.ex

+24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ defmodule AshPostgres.SqlImplementation do
1717
def require_extension_for_citext, do: {true, "citext"}
1818

1919
@impl true
20+
def storage_type(resource, field) do
21+
case AshPostgres.DataLayer.Info.storage_types(resource)[field] do
22+
nil ->
23+
nil
24+
25+
{:array, type} ->
26+
parameterized_type({:array, Ash.Type.get_type(type)}, [], false)
27+
28+
{:array, type, constraints} ->
29+
parameterized_type({:array, Ash.Type.get_type(type)}, constraints, false)
30+
31+
{type, constraints} ->
32+
parameterized_type(type, constraints, false)
33+
34+
type ->
35+
parameterized_type(type, [], false)
36+
end
37+
end
38+
39+
@impl true
40+
def expr(_query, [], _bindings, _embedded?, acc, type) when type in [:map, :jsonb] do
41+
{:ok, Ecto.Query.dynamic(fragment("'[]'::jsonb")), acc}
42+
end
43+
2044
def expr(
2145
query,
2246
%like{arguments: [arg1, arg2], embedded?: pred_embedded?},

mix.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ defmodule AshPostgres.MixProject do
163163
defp deps do
164164
[
165165
{:ash, ash_version("~> 3.0 and >= 3.0.15")},
166-
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.6")},
166+
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.8")},
167167
{:igniter, "~> 0.2.6"},
168168
{:ecto_sql, "~> 3.9"},
169169
{:ecto, "~> 3.9"},

mix.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
%{
22
"ash": {:hex, :ash, "3.0.16", "8eaebd5a9f3ee404937ac811a240799613b0619026e097436132d60eaf18ed16", [: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.1 and < 1.0.0-0", [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.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36c0d7653f7fb1d13cc03e1cc7ea7f6b9aadd278b9c9375ff5f0636ed0d7a785"},
3-
"ash_sql": {:hex, :ash_sql, "0.2.7", "56bfddcb4cf3edbbf702e2b665497309e43672fbf449ef049f4805211b9cd1b7", [:mix], [{:ash, "~> 3.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", "14622713cc08ede8fd0d2618b1718d759a6ee28839b8f738e6ee084703bd9437"},
3+
"ash_sql": {:hex, :ash_sql, "0.2.8", "ad20dc5487b68a095a12f84918d8ffab4bef12de59b7c5cf962a7c1dc03f5d81", [:mix], [{:ash, "~> 3.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", "d120568a19f3d72a634c3bfa9ce5fa0fae1d3c44ee4cdb8c78f151e490bf849b"},
44
"benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
66
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
77
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
88
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [: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", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
9-
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
9+
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
1010
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
1111
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
1212
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"attributes": [
3+
{
4+
"allow_nil?": false,
5+
"default": "fragment(\"gen_random_uuid()\")",
6+
"generated?": false,
7+
"primary_key?": true,
8+
"references": null,
9+
"size": null,
10+
"source": "id",
11+
"type": "uuid"
12+
},
13+
{
14+
"allow_nil?": true,
15+
"default": "nil",
16+
"generated?": false,
17+
"primary_key?": false,
18+
"references": null,
19+
"size": null,
20+
"source": "first_name",
21+
"type": "text"
22+
},
23+
{
24+
"allow_nil?": true,
25+
"default": "nil",
26+
"generated?": false,
27+
"primary_key?": false,
28+
"references": null,
29+
"size": null,
30+
"source": "last_name",
31+
"type": "text"
32+
},
33+
{
34+
"allow_nil?": true,
35+
"default": "nil",
36+
"generated?": false,
37+
"primary_key?": false,
38+
"references": null,
39+
"size": null,
40+
"source": "bio",
41+
"type": "map"
42+
},
43+
{
44+
"allow_nil?": true,
45+
"default": "nil",
46+
"generated?": false,
47+
"primary_key?": false,
48+
"references": null,
49+
"size": null,
50+
"source": "bios",
51+
"type": "jsonb"
52+
},
53+
{
54+
"allow_nil?": true,
55+
"default": "nil",
56+
"generated?": false,
57+
"primary_key?": false,
58+
"references": null,
59+
"size": null,
60+
"source": "badges",
61+
"type": [
62+
"array",
63+
"text"
64+
]
65+
}
66+
],
67+
"base_filter": null,
68+
"check_constraints": [],
69+
"custom_indexes": [],
70+
"custom_statements": [],
71+
"has_create_action": true,
72+
"hash": "D0080403BD79419DCA499838D2BE1F8AE2744F28A2ADC775B74D8EDA0EC11DB1",
73+
"identities": [],
74+
"multitenancy": {
75+
"attribute": null,
76+
"global": null,
77+
"strategy": null
78+
},
79+
"repo": "Elixir.AshPostgres.TestRepo",
80+
"schema": null,
81+
"table": "authors"
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule AshPostgres.TestRepo.Migrations.MigrateResources33 do
2+
@moduledoc """
3+
Updates resources based on their most recent snapshots.
4+
5+
This file was autogenerated with `mix ash_postgres.generate_migrations`
6+
"""
7+
8+
use Ecto.Migration
9+
10+
def up do
11+
alter table(:authors) do
12+
add(:bios, :jsonb)
13+
end
14+
end
15+
16+
def down do
17+
alter table(:authors) do
18+
remove(:bios)
19+
end
20+
end
21+
end

test/storage_types_test.exs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule AshPostgres.StorageTypesTest do
2+
use AshPostgres.RepoCase, async: false
3+
4+
alias Ash.BulkResult
5+
alias AshPostgres.Test.Author
6+
7+
require Ash.Query
8+
9+
test "can save {:array, :map} as jsonb" do
10+
%{id: id} =
11+
Author
12+
|> Ash.Changeset.for_create(
13+
:create,
14+
%{bios: [%{title: "bio1"}, %{title: "bio2"}]}
15+
)
16+
|> Ash.create!()
17+
18+
# testing empty list edge case
19+
%BulkResult{records: [author]} =
20+
Author
21+
|> Ash.Query.filter(id == ^id)
22+
|> Ash.bulk_update(:update, %{bios: []},
23+
return_errors?: true,
24+
notify?: true,
25+
strategy: [:atomic, :stream, :atomic_batches],
26+
allow_stream_with: :full_read,
27+
return_records?: true
28+
)
29+
30+
assert author.bios == []
31+
32+
%BulkResult{records: [author]} =
33+
Author
34+
|> Ash.Query.filter(id == ^id)
35+
|> Ash.bulk_update(:update, %{bios: [%{a: 1}]},
36+
return_errors?: true,
37+
notify?: true,
38+
strategy: [:atomic, :stream, :atomic_batches],
39+
allow_stream_with: :full_read,
40+
return_records?: true
41+
)
42+
43+
assert author.bios == [%{"a" => 1}]
44+
end
45+
end

test/support/resources/author.ex

+4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ defmodule AshPostgres.Test.Author do
1818
postgres do
1919
table("authors")
2020
repo(AshPostgres.TestRepo)
21+
22+
migration_types bios: :jsonb
23+
storage_types(bios: :jsonb)
2124
end
2225

2326
attributes do
2427
uuid_primary_key(:id, writable?: true)
2528
attribute(:first_name, :string, public?: true)
2629
attribute(:last_name, :string, public?: true)
2730
attribute(:bio, AshPostgres.Test.Bio, public?: true)
31+
attribute(:bios, {:array, :map}, public?: true)
2832
attribute(:badges, {:array, :atom}, public?: true)
2933
end
3034

0 commit comments

Comments
 (0)