From 8a4e6e5c79bcf3e1fb50d13364e11b71a5a8c955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:28:50 -0400 Subject: [PATCH 01/25] chore(deps-dev): bump the dev-dependencies group with 3 updates (#235) Bumps the dev-dependencies group with 3 updates: [benchee](https://github.com/bencheeorg/benchee), [credo](https://github.com/rrrene/credo) and [igniter](https://github.com/ash-project/igniter). Updates `benchee` from 1.5.0 to 1.5.1 - [Release notes](https://github.com/bencheeorg/benchee/releases) - [Changelog](https://github.com/bencheeorg/benchee/blob/main/CHANGELOG.md) - [Commits](https://github.com/bencheeorg/benchee/compare/1.5.0...1.5.1) Updates `credo` from 1.7.18 to 1.7.19 - [Release notes](https://github.com/rrrene/credo/releases) - [Changelog](https://github.com/rrrene/credo/blob/master/CHANGELOG.md) - [Commits](https://github.com/rrrene/credo/compare/v1.7.18...v1.7.19) Updates `igniter` from 0.8.1 to 0.8.2 - [Release notes](https://github.com/ash-project/igniter/releases) - [Changelog](https://github.com/ash-project/igniter/blob/main/CHANGELOG.md) - [Commits](https://github.com/ash-project/igniter/compare/v0.8.1...v0.8.2) --- updated-dependencies: - dependency-name: benchee dependency-version: 1.5.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: credo dependency-version: 1.7.19 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: igniter dependency-version: 0.8.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.lock b/mix.lock index f259ec6..0f0e900 100644 --- a/mix.lock +++ b/mix.lock @@ -1,51 +1,51 @@ %{ "ash": {:hex, :ash, "3.27.7", "349e47b9fc293c8de56866f900f6e1a3a5deea1e110d205749f94a9833431811", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.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]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "eb8a1a74090d7f1753a63fe422cd493b7f50736e2d95d280ccfb508956dccc1d"}, - "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, + "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [: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", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"}, + "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [: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", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "crux": {:hex, :crux, "0.1.3", "c698dee09d811678dcddad11a02a832c6bff100f1a7aee49ac44c87485bdbac8", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "04188ea9c1cee13e3ef132417200765857402dcc581f45a8a7862eec3b0530ff"}, "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, - "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, + "deep_merge": {:hex, :deep_merge, "1.0.2", "476aa7ea61c54de96220051b998d893869069094da65b96101aebf79416f8a1e", [:mix], [], "hexpm", "737a53cdc9758fedbb608bdc213969e65729466c4ef3cd8e8726d0335dff116c"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.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", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"}, "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, - "ex_ast": {:hex, :ex_ast, "0.12.0", "052ad63711da41b7efbfb3490dbf3d757bb67caec17d02f6deb0db4a0363e5f6", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "66b4797f157d32f0a63c6da227515f78816c0ac8f621f6d7a2b22108e7b4dd85"}, + "ex_ast": {:hex, :ex_ast, "0.12.5", "b9f0e2e1d0cce79e860a5f6fd8ae82fd8ac1f7e7182d0b45a0985cdd2df1cd20", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "c7ef42abba201cfe0bb4377eeb63911c581e51552e925a4bd52c26c786a20f98"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.10.0", "225780d8dcf9ef3393d26fa8d41d6a454a71149393c040e4b708c7c0b9c2b0f1", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "acd4a542eb425a58ce54505de69be704af3d0ef23c9b8cf9a3299d88e5d098ae"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "igniter": {:hex, :igniter, "0.8.1", "3c6ea47f3a6031015e29da8b4ba5c685f0a2e409facf63041fd83e982ca3aa89", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "d99472e6daf3bfc3675d699c6c7ace9196f377207aab83e09d7b95e9d90e8ae8"}, + "igniter": {:hex, :igniter, "0.8.2", "28621d9d2559c86a0249a195d9a7c55e8967b781850feff0bce6c4013b75f1c3", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "89146ad3fba21f3ea10873ae509fc760618623d4578e4723929a9dd1532aa30f"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "multigraph": {:hex, :multigraph, "0.16.1-mg.4", "2bbe149f5411b0e3bf0624c7bf2e3da2738efeac2f9a67bbbcb807ab171f0a76", [:mix], [], "hexpm", "b9f3e2577cef4658eeedf97c76d22a86d33a7aab702a93c1da9c122e849e9037"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, + "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, "reactor": {:hex, :reactor, "1.0.2", "79e4e81d016ab0016afd10bb4c18cb3a574f08f10f8e53be5f08ce27f8eed541", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "19fd55aaaadaae28f55133351051c25d4ac217f99e3e5a67940cc4a321e3948e"}, - "req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"}, + "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, - "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, + "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, "spark": {:hex, :spark, "2.7.0", "e685b33c038f12851993880bb7e3b326117612eb746fe15828678c152f8321c6", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "e2f675fbda32375b01d9ee7c652671531027fd043bf4a91bafdb2ab716aa1122"}, - "spitfire": {:hex, :spitfire, "0.3.12", "0f7780e4c6ea3753b65ea0c4924f3dfd5c21a51aaa734ffb9dd0b68d2544f27e", [:mix], [], "hexpm", "a389931287b85330c0e954ab06447e198516ab368a232a0200ed77ca13ca9acf"}, + "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, - "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, + "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, From 97a7b28cd6c8d31ebc5ed03996ff5c71b18faa8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:33:08 -0400 Subject: [PATCH 02/25] chore(deps): bump ash (#236) Bumps the production-dependencies group with 1 update in the / directory: [ash](https://github.com/ash-project/ash). Updates `ash` from 3.27.7 to 3.29.3 - [Release notes](https://github.com/ash-project/ash/releases) - [Changelog](https://github.com/ash-project/ash/blob/main/CHANGELOG.md) - [Commits](https://github.com/ash-project/ash/compare/v3.27.7...v3.29.3) --- updated-dependencies: - dependency-name: ash dependency-version: 3.29.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 0f0e900..21117c2 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.27.7", "349e47b9fc293c8de56866f900f6e1a3a5deea1e110d205749f94a9833431811", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.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]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "eb8a1a74090d7f1753a63fe422cd493b7f50736e2d95d280ccfb508956dccc1d"}, + "ash": {:hex, :ash, "3.29.3", "ba3c286c10fef489e01e1896bb6e80dbcac357c55b6d375da9e9eb7b49e88234", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "2a7a45d3a3b980457b5cc7e339a84a35c232a7ab99047bbf7ce55e7dc36df357"}, "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [: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", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, @@ -42,7 +42,7 @@ "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, - "spark": {:hex, :spark, "2.7.0", "e685b33c038f12851993880bb7e3b326117612eb746fe15828678c152f8321c6", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "e2f675fbda32375b01d9ee7c652671531027fd043bf4a91bafdb2ab716aa1122"}, + "spark": {:hex, :spark, "2.7.2", "36becc6ff03b40908cc821d403d7f06d893498e293d2f718afc6ca097fcb9d93", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "adb323ddbf9dbbe326f9e5def54ac96c47911e852b2c270bb19a5147c56f1b45"}, "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, From 467de7c644495156dcf64279b5f87da8edbed874 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Tue, 14 Jul 2026 04:26:59 +0900 Subject: [PATCH 03/25] fix: preserve shared context in lateral join source queries (#238) --- lib/query.ex | 24 +++++---- test/query_test.exs | 126 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 test/query_test.exs diff --git a/lib/query.ex b/lib/query.ex index fe7832b..d7a0c3f 100644 --- a/lib/query.ex +++ b/lib/query.ex @@ -99,16 +99,8 @@ defmodule AshSql.Query do {data, path} -> lateral_join_source_query = path |> List.first() |> elem(0) - lateral_join_source_query.resource - |> Ash.Query.set_context(%{ - :data_layer => - Map.put( - lateral_join_source_query.context[:data_layer] || %{}, - :no_inner_join?, - true - ) - |> Map.delete(:lateral_join_source) - }) + lateral_join_source_query + |> rebuild_lateral_join_source_query() |> Ash.Query.set_tenant(lateral_join_source_query.tenant) |> set_lateral_join_prefix(data_layer_query) |> filter_for_records(data) @@ -225,6 +217,18 @@ defmodule AshSql.Query do end end + @doc false + def rebuild_lateral_join_source_query(lateral_join_source_query) do + lateral_join_source_query.resource + |> Ash.Query.set_context(Map.take(lateral_join_source_query.context, [:shared])) + |> Ash.Query.set_context(%{ + data_layer: + (lateral_join_source_query.context[:data_layer] || %{}) + |> Map.put(:no_inner_join?, true) + |> Map.delete(:lateral_join_source) + }) + end + defp filter_for_records(query, records) do keys = case Ash.Resource.Info.primary_key(query.resource) do diff --git a/test/query_test.exs b/test/query_test.exs new file mode 100644 index 0000000..848d62a --- /dev/null +++ b/test/query_test.exs @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: 2024 ash_sql contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshSql.QueryTest do + use ExUnit.Case, async: false + + defmodule RecordSharedContext do + use Ash.Resource.Preparation + + @impl true + def prepare(query, _opts, context) do + send(self(), {:shared_context, context.source_context[:shared]}) + query + end + end + + defmodule AuthorPreferences do + use Ash.Resource, domain: AshSql.QueryTest.Domain + + attributes do + uuid_primary_key(:id) + attribute(:favorite_topic_id, :uuid) + end + + preparations do + prepare(AshSql.QueryTest.RecordSharedContext) + end + + actions do + defaults([:read]) + end + end + + defmodule Author do + use Ash.Resource, domain: AshSql.QueryTest.Domain + + attributes do + uuid_primary_key(:id) + end + + relationships do + has_one(:preferences, AshSql.QueryTest.AuthorPreferences, + destination_attribute: :id, + source_attribute: :id + ) + end + + actions do + defaults([:read]) + end + end + + defmodule Comment do + use Ash.Resource, domain: AshSql.QueryTest.Domain + + attributes do + uuid_primary_key(:id) + attribute(:post_id, :uuid) + attribute(:topic_id, :uuid) + end + + actions do + defaults([:read]) + end + end + + defmodule Post do + use Ash.Resource, domain: AshSql.QueryTest.Domain + + attributes do + uuid_primary_key(:id) + attribute(:author_id, :uuid) + end + + relationships do + belongs_to(:author, AshSql.QueryTest.Author) + + has_many :comments, AshSql.QueryTest.Comment do + destination_attribute(:post_id) + filter(expr(parent(author.preferences.favorite_topic_id) == topic_id)) + end + end + + actions do + defaults([:read]) + end + end + + defmodule Domain do + use Ash.Domain, validate_config_inclusion?: false + + resources do + resource(AshSql.QueryTest.Post) + resource(AshSql.QueryTest.Comment) + resource(AshSql.QueryTest.Author) + resource(AshSql.QueryTest.AuthorPreferences) + end + end + + test "lateral join source keeps shared context for parent-path preparations" do + shared = %{current_user_id: Ash.UUID.generate()} + + source_query = + Post + |> Ash.Query.new() + |> Ash.Query.set_context(%{shared: shared, data_layer: %{lateral_join_source: :discarded}}) + + rebuilt_query = AshSql.Query.rebuild_lateral_join_source_query(source_query) + + assert rebuilt_query.context[:shared] == shared + assert rebuilt_query.context[:data_layer][:no_inner_join?] + refute Map.has_key?(rebuilt_query.context[:data_layer], :lateral_join_source) + + relationship = Ash.Resource.Info.relationship(Post, :comments) + + assert Ash.Filter.find(relationship.filter, fn + %Ash.Query.Parent{} -> true + _ -> false + end) + + Ash.Query.for_read(AuthorPreferences, :read, %{}, context: rebuilt_query.context) + + assert_receive {:shared_context, ^shared} + end +end From 35c712f4baecee3bced5c64c9228c18af78ba849 Mon Sep 17 00:00:00 2001 From: Jechol Lee Date: Tue, 14 Jul 2026 04:29:46 +0900 Subject: [PATCH 04/25] improvement: Use relationship read action arguments in joins (#237) --- lib/join.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/join.ex b/lib/join.ex index f3645f5..eb867aa 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -465,7 +465,10 @@ defmodule AshSql.Join do if query.__validated_for_action__ == read_action.name do query else - Ash.Query.for_read(query, read_action.name, %{}, + Ash.Query.for_read( + query, + read_action.name, + Map.get(relationship, :read_action_arguments, %{}), actor: context[:private][:actor], tenant: context[:private][:tenant] ) From 4437706b8cacf31f7bc7d6e04f4ed4ebd50b1b8c Mon Sep 17 00:00:00 2001 From: Avery Lopez <95703511+averypeck@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:27:58 -0500 Subject: [PATCH 05/25] fix: use aggregate read_action instead of using default relationship read (#239) --- lib/aggregate.ex | 62 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/lib/aggregate.ex b/lib/aggregate.ex index 50243ee..9622033 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -188,7 +188,11 @@ defmodule AshSql.Aggregate do raise "No such relationship #{inspect(resource)}.#{first_relationship}. aggregates: #{inspect(aggregates)}" first_relationship -> - {first_relationship, rest} + if rest == [] do + {override_read_action(first_relationship, read_action.name), rest} + else + {first_relationship, rest} + end end end @@ -1561,7 +1565,7 @@ defmodule AshSql.Aggregate do defp join_all_relationships( agg_root_query, - _aggregates, + aggregates, relationship_path, first_relationship, _is_single?, @@ -1579,16 +1583,17 @@ defmodule AshSql.Aggregate do end end) + relationships = + first_relationship.destination + |> AshSql.Join.relationship_path_to_relationships(relationship_path) + |> List.update_at(-1, &override_read_action(&1, hd(aggregates).query.action.name)) + AshSql.Join.join_all_relationships( agg_root_query, Map.values(join_filters), [], [ - {:inner, - AshSql.Join.relationship_path_to_relationships( - first_relationship.destination, - relationship_path - )} + {:inner, relationships} ], [], nil, @@ -1707,7 +1712,7 @@ defmodule AshSql.Aggregate do relationship_path: relationship_path, join_filters: join_filters, field: %Ash.Query.Calculation{} = field - }, + } = aggregate, _ ) do ref = @@ -1719,7 +1724,8 @@ defmodule AshSql.Aggregate do with true <- join_filters == %{}, [] <- Ash.Filter.used_aggregates(ref, :all), - [] <- Ash.Filter.relationship_paths(ref) do + [] <- Ash.Filter.relationship_paths(ref), + true <- read_action_matches_relationship_default?(resource, aggregate) do true else _ -> @@ -1767,7 +1773,8 @@ defmodule AshSql.Aggregate do } with [] <- Ash.Filter.used_aggregates(ref, :all), - [] <- Ash.Filter.relationship_paths(ref) do + [] <- Ash.Filter.relationship_paths(ref), + true <- read_action_matches_relationship_default?(resource, aggregate) do true else _ -> @@ -1778,9 +1785,10 @@ defmodule AshSql.Aggregate do false _ -> - name in query.__ash_bindings__.sql_behaviour.simple_join_first_aggregates(resource) || - (join_filters in [nil, %{}, []] && - single_path?(resource, relationship_path)) + (name in query.__ash_bindings__.sql_behaviour.simple_join_first_aggregates(resource) || + (join_filters in [nil, %{}, []] && + single_path?(resource, relationship_path))) && + read_action_matches_relationship_default?(resource, aggregate) end end @@ -2489,6 +2497,34 @@ defmodule AshSql.Aggregate do end end + defp read_action_matches_relationship_default?(_resource, %{relationship_path: []}), do: true + + defp read_action_matches_relationship_default?(resource, aggregate) do + last_relationship = + resource + |> AshSql.Join.relationship_path_to_relationships(aggregate.relationship_path) + |> List.last() + + default_action_name = + last_relationship.read_action || + Ash.Resource.Info.primary_action!(last_relationship.destination, :read).name + + case aggregate.query && aggregate.query.action do + nil -> true + action -> action.name == default_action_name + end + end + + defp override_read_action(relationship, action_name) do + if relationship.read_action == action_name do + relationship + else + relationship + |> Map.put(:read_action, action_name) + |> Map.replace(:read_action_arguments, %{}) + end + end + defp single_path?(_, []), do: true defp single_path?(resource, [relationship | rest]) do From ee54634abfe679e4ccf4f719623563d728902432 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 21 Jul 2026 20:41:15 -0400 Subject: [PATCH 06/25] fix: ensure limits/offsets are applied in all aggregates --- lib/aggregate.ex | 94 ++++++++++++++++++++++++++++++++++++++++-------- lib/join.ex | 3 +- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/lib/aggregate.ex b/lib/aggregate.ex index 9622033..e0d9fb6 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -517,31 +517,40 @@ defmodule AshSql.Aggregate do tenant, join_filters ) do + limited? = limited_relationship?(first_relationship) + AshSql.Join.related_subquery( first_relationship, tmp_query, start_bindings_at: start_bindings_at, refs_at_path: root_data_path, skip_distinct_for_first_rel?: true, + sort?: limited?, on_subquery: fn subquery -> base_binding = subquery.__ash_bindings__.root_binding current_binding = subquery.__ash_bindings__.current subquery = - subquery - |> Ecto.Query.exclude(:select) - |> Ecto.Query.select(%{}) - - subquery = - apply_relationship_subquery( - subquery, - first_relationship, - query, - tenant, - source_binding, - current_binding, - base_binding - ) + if limited? do + apply_limited_relationship_subquery( + subquery, + first_relationship, + source_binding, + base_binding + ) + else + subquery + |> Ecto.Query.exclude(:select) + |> Ecto.Query.select(%{}) + |> apply_relationship_subquery( + first_relationship, + query, + tenant, + source_binding, + current_binding, + base_binding + ) + end subquery = AshSql.Join.set_join_prefix( @@ -602,6 +611,63 @@ defmodule AshSql.Aggregate do ) end + # Relationships that declare a `limit` (or `offset`) need the limit applied + # to the correlated rows *before* the aggregation's `GROUP BY`, otherwise the + # limit caps the number of groups (always 1 in a lateral join) instead of the + # number of rows per group. + defp limited_relationship?(relationship) do + (is_integer(Map.get(relationship, :limit)) or + (Map.get(relationship, :offset) || 0) > 0) and + is_nil(Map.get(relationship, :manual)) and + !Map.get(relationship, :no_attributes?) and + relationship.type != :many_to_many + end + + # Builds: + # + # SELECT ... FROM ( + # SELECT * FROM destination + # WHERE destination.destination_attribute = parent.source_attribute + # ORDER BY LIMIT + # ) AS + # GROUP BY destination_attribute + # + # The correlation, sort and limit all live in the inner subquery so that the + # limit bounds the rows per parent, and the aggregate functions fold the + # already-limited rows. + defp apply_limited_relationship_subquery(subquery, rel, source_binding, base_binding) do + field = rel.destination_attribute + + inner = + from(row in subquery, + where: + field( + parent_as(^source_binding), + ^rel.source_attribute + ) == + field( + as(^base_binding), + ^rel.destination_attribute + ) + ) + + inner = + case Map.get(rel, :limit) do + limit when is_integer(limit) -> Ecto.Query.limit(inner, ^limit) + _ -> inner + end + + from(row in subquery(inner), as: ^base_binding) + |> Map.put(:__ash_bindings__, subquery.__ash_bindings__) + |> Ecto.Query.select(%{}) + |> then(fn wrapped -> + from(row in wrapped, + group_by: field(row, ^field), + select_merge: %{^field => field(row, ^field)} + ) + end) + end + defp apply_relationship_subquery( subquery, %{manual: {module, opts}} = rel, diff --git a/lib/join.ex b/lib/join.ex index eb867aa..4da2176 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -400,7 +400,8 @@ defmodule AshSql.Join do if opts[:return_subquery?] do subquery(query) else - if Enum.empty?(query.joins) && Enum.empty?(query.order_bys) && Enum.empty?(query.wheres) do + if Enum.empty?(query.joins) && Enum.empty?(query.order_bys) && Enum.empty?(query.wheres) && + Enum.empty?(query.group_bys) && is_nil(query.limit) do query else from(row in subquery(query), as: ^(opts[:start_bindings_at] || 0)) From de442529c841f36debc855312f69a7b769f2e472 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 21 Jul 2026 20:45:05 -0400 Subject: [PATCH 07/25] chore: release version v0.6.6 --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c06f47..1f700d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,23 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.6.6](https://github.com/ash-project/ash_sql/compare/v0.6.5...v0.6.6) (2026-07-22) + + + + +### Bug Fixes: + +* ensure limits/offsets are applied in all aggregates by [@zachdaniel](https://github.com/zachdaniel) + +* use aggregate read_action instead of using default relationship read (#239) by averypeck [(#239)](https://github.com/ash-project/ash_sql/pull/239) + +* preserve shared context in lateral join source queries (#238) by [@nallwhy](https://github.com/nallwhy) [(#238)](https://github.com/ash-project/ash_sql/pull/238) + +### Improvements: + +* Use relationship read action arguments in joins (#237) by [@jechol](https://github.com/jechol) [(#237)](https://github.com/ash-project/ash_sql/pull/237) + ## [v0.6.5](https://github.com/ash-project/ash_sql/compare/v0.6.4...v0.6.5) (2026-06-20) diff --git a/README.md b/README.md index f1e1ac0..36d7a18 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Shared functionality for ecto-based sql data layers. ```elixir def deps do [ - {:ash_sql, "~> 0.6.5"} + {:ash_sql, "~> 0.6.6"} ] end ``` diff --git a/mix.exs b/mix.exs index 3bcbee6..c81e053 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule AshSql.MixProject do Shared utilities for ecto-based sql data layers. """ - @version "0.6.5" + @version "0.6.6" def project do [ From 75f5f9b8c32f9ca002024ac38fc9b8739443c9f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:13:43 +0000 Subject: [PATCH 08/25] chore(deps): bump the production-dependencies group with 2 updates Bumps the production-dependencies group with 2 updates: [ash](https://github.com/ash-project/ash) and [ecto](https://github.com/elixir-ecto/ecto). Updates `ash` from 3.29.3 to 3.31.0 - [Release notes](https://github.com/ash-project/ash/releases) - [Changelog](https://github.com/ash-project/ash/blob/main/CHANGELOG.md) - [Commits](https://github.com/ash-project/ash/compare/v3.29.3...v3.31.0) Updates `ecto` from 3.14.0 to 3.14.1 - [Release notes](https://github.com/elixir-ecto/ecto/releases) - [Changelog](https://github.com/elixir-ecto/ecto/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-ecto/ecto/compare/v3.14.0...v3.14.1) --- updated-dependencies: - dependency-name: ash dependency-version: 3.31.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-dependencies - dependency-name: ecto dependency-version: 3.14.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] --- mix.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.lock b/mix.lock index 21117c2..a18e82c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,43 +1,43 @@ %{ - "ash": {:hex, :ash, "3.29.3", "ba3c286c10fef489e01e1896bb6e80dbcac357c55b6d375da9e9eb7b49e88234", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "2a7a45d3a3b980457b5cc7e339a84a35c232a7ab99047bbf7ce55e7dc36df357"}, + "ash": {:hex, :ash, "3.31.0", "9ccd7d5ff00a5329a33786063e4a3040681b61d736730cd7aa3f24e5843c1d4f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "7518ecf694e1b423478ddaa5baf713e9d38448df6873f3be6316f758bc6ab68d"}, "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [: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", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, - "crux": {:hex, :crux, "0.1.3", "c698dee09d811678dcddad11a02a832c6bff100f1a7aee49ac44c87485bdbac8", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "04188ea9c1cee13e3ef132417200765857402dcc581f45a8a7862eec3b0530ff"}, + "crux": {:hex, :crux, "0.1.4", "1fa21f5ca886d3498f83871a3ef19379b80abf8cfcf2ed32007e80279e714f1e", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "ff7d880cf732d82360aa81e439b8d4831cd30768061c9233f90c97e10162b9c0"}, "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "deep_merge": {:hex, :deep_merge, "1.0.2", "476aa7ea61c54de96220051b998d893869069094da65b96101aebf79416f8a1e", [:mix], [], "hexpm", "737a53cdc9758fedbb608bdc213969e65729466c4ef3cd8e8726d0335dff116c"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.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", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"}, + "ecto": {:hex, :ecto, "3.14.1", "7b740d87bdf45996aa0c2c2e081640906f10caa7ce5ba328fd294c7d49d0cc6f", [:mix], [{:decimal, "~> 3.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", "24b991956796700f467d0a3ef3d303138a3ef9ddddf8b98f43758ee067b20a30"}, "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, - "ex_ast": {:hex, :ex_ast, "0.12.5", "b9f0e2e1d0cce79e860a5f6fd8ae82fd8ac1f7e7182d0b45a0985cdd2df1cd20", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "c7ef42abba201cfe0bb4377eeb63911c581e51552e925a4bd52c26c786a20f98"}, + "ex_ast": {:hex, :ex_ast, "0.13.1", "b3d80ec163733176f63662ac44d2511445c224f6b5e4e3ce01f5eff83c4a5993", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "bd15f68cde5ec945b859bd67416f26cf5499f1aef9b067eb2163ed244c7e703a"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.10.0", "225780d8dcf9ef3393d26fa8d41d6a454a71149393c040e4b708c7c0b9c2b0f1", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "acd4a542eb425a58ce54505de69be704af3d0ef23c9b8cf9a3299d88e5d098ae"}, - "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "igniter": {:hex, :igniter, "0.8.2", "28621d9d2559c86a0249a195d9a7c55e8967b781850feff0bce6c4013b75f1c3", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "89146ad3fba21f3ea10873ae509fc760618623d4578e4723929a9dd1532aa30f"}, + "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, + "igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"}, + "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "multigraph": {:hex, :multigraph, "0.16.1-mg.4", "2bbe149f5411b0e3bf0624c7bf2e3da2738efeac2f9a67bbbcb807ab171f0a76", [:mix], [], "hexpm", "b9f3e2577cef4658eeedf97c76d22a86d33a7aab702a93c1da9c122e849e9037"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, - "reactor": {:hex, :reactor, "1.0.2", "79e4e81d016ab0016afd10bb4c18cb3a574f08f10f8e53be5f08ce27f8eed541", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "19fd55aaaadaae28f55133351051c25d4ac217f99e3e5a67940cc4a321e3948e"}, - "req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"}, + "reactor": {:hex, :reactor, "1.0.3", "740dc5d22686c2bac78aa6ca31b4760ff16c939b3220776dd671157c306e6198", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "951d8c2187e2db0c75c2e5c15d7f77d4379d00e6fc8d115bd6931a4d74e3b496"}, + "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, @@ -46,10 +46,10 @@ "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, - "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, + "stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, - "ymlr": {:hex, :ymlr, "5.1.5", "0b9207c7940be3f2bc29b77cd55109d5aa2f4dcde6575942017335769e6f5628", [:mix], [], "hexpm", "7030cb240c46850caeb3b01be745307632be319b15f03083136f6251f49b516d"}, + "ymlr": {:hex, :ymlr, "5.1.6", "247479328fd5ea1e222ee59ecd5571ab2885cf1a6329aea458d4944cd035cfc1", [:mix], [], "hexpm", "b36d7c72384d2cb2094d2a3c8b0c3b12303e0ccd1b7b12333b81d75b1fe0b53a"}, } From 5ae3a4d87b67ab7d4e79a0e74dc0ab02be632a9e Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Tue, 4 Aug 2026 00:53:30 +0930 Subject: [PATCH 09/25] fix: apply each combination part to the whole of what precedes it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Ash.Query.combination_of/2` takes an ordered list of parts, and each part applies to the result of everything before it. There is no precedence between the combination types — the order in the list is the order of application. `combination_of/4` appended each operation onto the accumulated query, which renders as one flat chain of set operations. SQL then applies its own precedence to that chain, and `INTERSECT` binds tighter than `UNION` and `EXCEPT`, so a combination spanning more than one precedence level was regrouped: given `base`, `union`, `intersect`, the intersect was applied to the union's right operand rather than to the running result. The effect was a query whose meaning depended on its data layer. `Ash.DataLayer.Ets` applies the parts in the order given, so the two answered differently — for one three-part combination, `["alpha", "beta", "gamma"]` here against `["alpha", "gamma"]` there. Nest what has accumulated before applying the next part, so each operation takes everything preceding it as its left operand. A part already wrapped as a subquery is left alone, so this adds one level of nesting per part rather than one per part plus a trailing one. Verified against ash_postgres built with `ASH_SQL_VERSION=local`: its combination suite (18 tests) and full suite (828) stay green, and a three-part case returns the order given only with this change. --- lib/query.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/query.ex b/lib/query.ex index d7a0c3f..ed58ddf 100644 --- a/lib/query.ex +++ b/lib/query.ex @@ -22,6 +22,18 @@ defmodule AshSql.Query do _domain \\ nil ) do Enum.reduce(combination_of, subquery(first), fn {type, combination_of}, query -> + # Each part applies to the result of everything before it. Appending + # straight onto the accumulated query would build one flat chain of set + # operations, and SQL's own precedence would then decide the grouping — + # `INTERSECT` binds tighter than `UNION`/`EXCEPT`, so a part could end up + # applied to its predecessor rather than to the whole. Nesting what has + # accumulated keeps the order the parts were given in. + query = + case query do + %Ecto.SubQuery{} -> query + query -> subquery(query) + end + case type do :union -> Ecto.Query.union(query, ^combination_of) From b9e10dba16b2d2a59061f9e2d0d280ec54193706 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 11 Aug 2026 14:10:03 -0400 Subject: [PATCH 10/25] fix: fix aggregate references inside of unions --- lib/aggregate.ex | 55 ++++++++++++++++++++++++++++++++++++------------ lib/query.ex | 2 +- mix.lock | 8 +++---- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/aggregate.ex b/lib/aggregate.ex index e0d9fb6..b53ee76 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -2679,8 +2679,20 @@ defmodule AshSql.Aggregate do |> Ash.Resource.Info.attribute_names() |> MapSet.to_list() + # An upgraded combination query's source only exposes the default + # attributes plus the combination fieldset; others can't be re-selected. + available_attr_names = + if query.__ash_bindings__[:subquery_upgrade?] do + MapSet.union( + selected_by_default, + MapSet.new(query.__ash_bindings__[:already_selected] || []) + ) + else + MapSet.new(all_attr_names) + end + to_select = - Enum.reject(all_attr_names, &(&1 in selected_fields)) + Enum.reject(all_attr_names, &(&1 in selected_fields or &1 not in available_attr_names)) query_with_all_attrs = case query.select do @@ -2724,21 +2736,36 @@ defmodule AshSql.Aggregate do select_aggregates = (query_with_all_attrs.__ash_bindings__[:select_aggregates] || []) -- [:aggregates] + reselected_fields = + Enum.concat([ + selected_fields, + select_calculations, + select_aggregates, + flattened_calc_fields, + flattened_agg_fields + ]) + + # Upgraded combination queries return maps, which `struct/2` rejects, and + # `map/2` would drop field type info — merge each field explicitly instead. subquery_query = - from(row in subquery(query_with_all_attrs), - as: ^query.__ash_bindings__.root_binding, - select: - struct( - row, - ^Enum.concat([ - selected_fields, - select_calculations, - select_aggregates, - flattened_calc_fields, - flattened_agg_fields - ]) + if query.__ash_bindings__[:subquery_upgrade?] do + root_binding = query.__ash_bindings__.root_binding + + base = + from(row in subquery(query_with_all_attrs), + as: ^root_binding, + select: %{} ) - ) + + Enum.reduce(reselected_fields, base, fn field, q -> + from(row in q, select_merge: %{^field => field(as(^root_binding), ^field)}) + end) + else + from(row in subquery(query_with_all_attrs), + as: ^query.__ash_bindings__.root_binding, + select: struct(row, ^reselected_fields) + ) + end root_binding = query.__ash_bindings__.root_binding diff --git a/lib/query.ex b/lib/query.ex index ed58ddf..a0f5f1c 100644 --- a/lib/query.ex +++ b/lib/query.ex @@ -565,7 +565,7 @@ defmodule AshSql.Query do | select: %{select | expr: {:merge, [], [merge_base, {:%{}, [], merging}]}} } |> Map.update!(:__ash_bindings__, fn bindings -> - Map.update(bindings, :select_calculations, [], &(&1 -- [:calculations])) + Map.update(bindings, :select_calculations, [], &((&1 || []) -- [:calculations])) end) {calculation_merges, aggregate_merges, new_query} diff --git a/mix.lock b/mix.lock index a18e82c..fbc69d1 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.31.0", "9ccd7d5ff00a5329a33786063e4a3040681b61d736730cd7aa3f24e5843c1d4f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "7518ecf694e1b423478ddaa5baf713e9d38448df6873f3be6316f758bc6ab68d"}, + "ash": {:hex, :ash, "3.31.2", "adc1298d04e79b14549efc06637d85a622b052d1f6fcc9fe3b884e084bde63ef", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "7126c1857f57cd73b2bf5c36dda3fa36a46b5353433207999c60727d59d3edac"}, "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [: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", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, @@ -36,15 +36,15 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, - "reactor": {:hex, :reactor, "1.0.3", "740dc5d22686c2bac78aa6ca31b4760ff16c939b3220776dd671157c306e6198", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "951d8c2187e2db0c75c2e5c15d7f77d4379d00e6fc8d115bd6931a4d74e3b496"}, + "reactor": {:hex, :reactor, "1.0.5", "43a3298fb5874248e72d61662704d5013194c427afcf740e14336f3379f53ed3", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "8867decb795d0b87a9dea5d146721af60312c0b45443d125d9be535946e8da19"}, "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, "spark": {:hex, :spark, "2.7.2", "36becc6ff03b40908cc821d403d7f06d893498e293d2f718afc6ca097fcb9d93", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "adb323ddbf9dbbe326f9e5def54ac96c47911e852b2c270bb19a5147c56f1b45"}, - "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, - "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, + "spitfire": {:hex, :spitfire, "0.4.0", "6d98c10cf585434b9439ba0c6dd3cc7aeff0e06ab73bfe5488f42e7c0f883d9b", [:mix], [], "hexpm", "7e5c6d1523c111b59f332f9dc49edc0377111d0c17167a29830f0e98233f5472"}, + "splode": {:hex, :splode, "0.3.2", "7716b6b2260a98a6f018c65cc0393da2cdf17314202eb351a0956e8762190dff", [:mix], [], "hexpm", "08fd658f80da7f1cd254b149164dcff8acd44b22f032001d5416b97223d32bc9"}, "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, "stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, From 4804739139fb307cbb547dab55d56d44107e478e Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 11 Aug 2026 14:12:23 -0400 Subject: [PATCH 11/25] chore: release version v0.6.7 --- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f700d8..99f712f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.6.7](https://github.com/ash-project/ash_sql/compare/v0.6.6...v0.6.7) (2026-08-11) + + + + +### Bug Fixes: + +* fix aggregate references inside of unions by [@zachdaniel](https://github.com/zachdaniel) + +* apply each combination part to the whole of what precedes it by [@matt-beanland](https://github.com/matt-beanland) [(#244)](https://github.com/ash-project/ash_sql/pull/244) + ## [v0.6.6](https://github.com/ash-project/ash_sql/compare/v0.6.5...v0.6.6) (2026-07-22) diff --git a/README.md b/README.md index 36d7a18..5cb63cf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Shared functionality for ecto-based sql data layers. ```elixir def deps do [ - {:ash_sql, "~> 0.6.6"} + {:ash_sql, "~> 0.6.7"} ] end ``` diff --git a/mix.exs b/mix.exs index c81e053..df256a2 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule AshSql.MixProject do Shared utilities for ecto-based sql data layers. """ - @version "0.6.6" + @version "0.6.7" def project do [ From ac2cd4ada85cf59cd2da8e41b401a3257c898708 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 14 Aug 2026 14:49:52 +0930 Subject: [PATCH 12/25] improvement: support the Duration form of ago, from_now, datetime_add and date_add `Ash.Query.Function.Ago`, `FromNow`, `DateTimeAdd` and `DateAdd` each accept a `Duration` as well as an integer/interval-name pair, and evaluate it in Elixir, but only the interval form was rendered here. The Duration form reached `default_dynamic_expr/6`, matched nothing, and raised `Unsupported expression`, so it worked on data layers that evaluate expressions at runtime and failed on every SQL-backed one. Adds a clause per function, rendering the duration as an interval parameter. Ecto has a native `:duration` type and Postgres accepts a `%Duration{}` directly, so no interval-name string building is needed, and multi-unit durations work. The datetime operand is cast, as Ecto's own `datetime_add/3` does with `type_unless_typed`; without it Postgres resolves `? - ?::interval` as interval arithmetic. `date_add` casts back to `::date` for the same reason Ecto's does. `from_now/1` is uncallable until the corresponding ash change releases, so its clause is untestable downstream until then. --- lib/expr.ex | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/lib/expr.ex b/lib/expr.ex index 1b9ad65..9c3203c 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -305,6 +305,27 @@ defmodule AshSql.Expr do ), acc} end + defp default_dynamic_expr( + query, + %Ago{arguments: [duration], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {duration, acc} = + do_dynamic_expr( + query, + duration, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + :duration + ) + + {Ecto.Query.dynamic(fragment("(?::timestamp - ?)", ^DateTime.utc_now(), ^duration)), acc} + end + defp default_dynamic_expr( query, %StartOfDay{arguments: [value], embedded?: pred_embedded?}, @@ -446,6 +467,27 @@ defmodule AshSql.Expr do ), acc} end + defp default_dynamic_expr( + query, + %FromNow{arguments: [duration], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {duration, acc} = + do_dynamic_expr( + query, + duration, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + :duration + ) + + {Ecto.Query.dynamic(fragment("(?::timestamp + ?)", ^DateTime.utc_now(), ^duration)), acc} + end + defp default_dynamic_expr( query, %DateTimeAdd{arguments: [datetime, amount, interval], embedded?: pred_embedded?}, @@ -478,6 +520,36 @@ defmodule AshSql.Expr do acc} end + defp default_dynamic_expr( + query, + %DateTimeAdd{arguments: [datetime, duration], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {datetime, acc} = + do_dynamic_expr( + query, + datetime, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc + ) + + {duration, acc} = + do_dynamic_expr( + query, + duration, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + :duration + ) + + {Ecto.Query.dynamic(fragment("(?::timestamp + ?)", ^datetime, ^duration)), acc} + end + defp default_dynamic_expr( query, %DateAdd{arguments: [date, amount, interval], embedded?: pred_embedded?}, @@ -509,6 +581,38 @@ defmodule AshSql.Expr do {Ecto.Query.dynamic(fragment("(?)", date_add(^date, ^amount, ^to_string(interval)))), acc} end + defp default_dynamic_expr( + query, + %DateAdd{arguments: [date, duration], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {date, acc} = + do_dynamic_expr( + query, + date, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc + ) + + {duration, acc} = + do_dynamic_expr( + query, + duration, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + :duration + ) + + # `date + interval` is a timestamp in Postgres, so cast back, as Ecto's own + # `date_add/3` does. + {Ecto.Query.dynamic(fragment("((?::date + ?)::date)", ^date, ^duration)), acc} + end + defp default_dynamic_expr( query, %GetPath{ From 60a3fe654dae5e2f6a5d71742136321280be40e1 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Aug 2026 14:51:10 -0400 Subject: [PATCH 13/25] improvement: support a cast mechanism when casting refs --- lib/expr.ex | 30 +++++++++++++++++++++++------- lib/implementation.ex | 9 +++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/expr.ex b/lib/expr.ex index 9c3203c..197fcd2 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -2424,16 +2424,29 @@ defmodule AshSql.Expr do ) if type do - bindings = + {bindings, cast_type} = case arg1 do - %Ash.Query.Ref{attribute: %Ash.Resource.Attribute{}} -> - Map.put(bindings, :no_cast?, true) + %Ash.Query.Ref{ + attribute: %Ash.Resource.Attribute{type: attr_type, constraints: attr_constraints} + } -> + # only remap the cast for refs being cast to their own type — + # cross-type casts (e.g. a usec column cast to second precision) + # must keep their requested target + cast_type = + if type == + parameterized_type(bindings.sql_behaviour, attr_type, attr_constraints, :expr) do + bindings.sql_behaviour.ref_cast_type(type) + else + type + end + + {Map.put(bindings, :no_cast?, true), cast_type} _ -> if Ash.Expr.expr?(arg1) do - bindings + {bindings, type} else - Map.put(bindings, :no_cast?, true) + {Map.put(bindings, :no_cast?, true), type} end end @@ -2464,7 +2477,7 @@ defmodule AshSql.Expr do {expr, acc} _ -> - {query.__ash_bindings__.sql_behaviour.type_expr(expr, type), acc} + {query.__ash_bindings__.sql_behaviour.type_expr(expr, cast_type), acc} end else do_dynamic_expr(query, arg1, bindings, embedded?, acc, arg2) @@ -2990,7 +3003,10 @@ defmodule AshSql.Expr do Ecto.Query.dynamic(field(as(^ref_binding), ^name)) end - query.__ash_bindings__.sql_behaviour.type_expr(ref_dynamic, type) + query.__ash_bindings__.sql_behaviour.type_expr( + ref_dynamic, + query.__ash_bindings__.sql_behaviour.ref_cast_type(type) + ) end {expr, acc} diff --git a/lib/implementation.ex b/lib/implementation.ex index 496065a..177c9c6 100644 --- a/lib/implementation.ex +++ b/lib/implementation.ex @@ -38,6 +38,7 @@ defmodule AshSql.Implementation do @callback require_extension_for_citext() :: {true, String.t()} | false @callback strpos_function() :: String.t() @callback type_expr(expr :: term, type :: term) :: term + @callback ref_cast_type(type :: term) :: term @optional_callbacks determine_types: 3 @@ -59,6 +60,13 @@ defmodule AshSql.Implementation do def equals_any?, do: true def storage_type(_, _), do: nil + # The cast type to use when casting a bare column reference, as opposed + # to a value or a computed expression. Implementations can use this to + # make ref casts match the column's actual DDL type (e.g. `timestamp(0)` + # instead of `timestamp` on postgres), so the parser can collapse them + # and partial/expression indexes on the bare column remain usable. + def ref_cast_type(type), do: type + def type_expr(expr, type) do type = if Ash.Type.ash_type?(type) do @@ -86,6 +94,7 @@ defmodule AshSql.Implementation do require_extension_for_citext: 0, simple_join_first_aggregates: 1, type_expr: 2, + ref_cast_type: 1, storage_type: 2, list_aggregate: 1, multicolumn_distinct?: 0 From bc86d42e82e07675cb6e7b9c0997d8b70041b3d2 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Aug 2026 14:52:12 -0400 Subject: [PATCH 14/25] chore: release version v0.6.8 --- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f712f..1a12710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,19 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.6.8](https://github.com/ash-project/ash_sql/compare/v0.6.7...v0.6.8) (2026-08-14) + + + + +### Improvements: + +* support a cast mechanism when casting refs by [@zachdaniel](https://github.com/zachdaniel) + +* support the Duration form of ago, from_now, datetime_add… by [@zachdaniel](https://github.com/zachdaniel) [(#245)](https://github.com/ash-project/ash_sql/pull/245) + +* support the Duration form of ago, from_now, datetime_add and date_add by [@matt-beanland](https://github.com/matt-beanland) [(#245)](https://github.com/ash-project/ash_sql/pull/245) + ## [v0.6.7](https://github.com/ash-project/ash_sql/compare/v0.6.6...v0.6.7) (2026-08-11) diff --git a/README.md b/README.md index 5cb63cf..4d1c3b5 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Shared functionality for ecto-based sql data layers. ```elixir def deps do [ - {:ash_sql, "~> 0.6.7"} + {:ash_sql, "~> 0.6.8"} ] end ``` diff --git a/mix.exs b/mix.exs index df256a2..8dc6e87 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule AshSql.MixProject do Shared utilities for ecto-based sql data layers. """ - @version "0.6.7" + @version "0.6.8" def project do [ From 4ba4c7ab65c79855c53b1d1879d683e9b91e23dd Mon Sep 17 00:00:00 2001 From: Alan McCann Date: Sun, 16 Aug 2026 12:36:59 -0400 Subject: [PATCH 15/25] Add Implementation.list_expr/6 so adapters can render list literals encode_list/6 renders every list literal as Postgres ARRAY[...] (or array_to_json(ARRAY[...])) unconditionally, and default_dynamic_expr/6 routes bare lists to it without consulting sql_behaviour, so a data layer whose database has no array constructor cannot intercept: on SQLite the statement fails to parse ((Exqlite.Error) near "[?]": syntax error). Add a list_expr/6 callback with the same shape and return contract as expr/6, defined as :error and overridable in __using__ exactly like expr/6. encode_list/6 consults it and falls back to the existing ARRAY[...] body, moved verbatim into a private default_encode_list/7, so an implementation that does not override it renders byte-identical SQL. The callback sits after the embedded-resource dump, at the exact point ARRAY[...] would otherwise be emitted, so implementations receive a plain list and nothing that renders correctly today is diverted. Closes #246. --- lib/expr.ex | 105 ++++++++++++++++++++++++------------------ lib/implementation.ex | 16 +++++++ 2 files changed, 76 insertions(+), 45 deletions(-) diff --git a/lib/expr.ex b/lib/expr.ex index 197fcd2..b99eb16 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -3510,57 +3510,72 @@ defmodule AshSql.Expr do value end - element_type = - case type do - {{:array, type}, _} -> type - {{:in, type}, _} -> type - _ -> nil - end + case bindings.sql_behaviour.list_expr(query, value, bindings, embedded?, acc, type) do + {:ok, expr, acc} -> + {expr, acc} - elements = - Enum.map(value, fn list_item -> - if type do - {:expr, %Ash.Query.Function.Type{arguments: [list_item, element_type, []]}} - else - {:expr, list_item} - end - end) - |> Enum.intersperse({:raw, ","}) + {:error, error} -> + raise "Error while building list expression: #{error}" - if is_map? do - do_dynamic_expr( - query, - %Fragment{ - embedded?: embedded?, - arguments: - [ - raw: "array_to_json(ARRAY[" - ] ++ elements ++ [raw: "])"] - }, - bindings, - embedded?, - acc, - type - ) - else - do_dynamic_expr( - query, - %Fragment{ - embedded?: embedded?, - arguments: - [ - raw: "ARRAY[" - ] ++ elements ++ [raw: "]"] - }, - bindings, - embedded?, - acc, - type - ) + :error -> + default_encode_list(query, value, bindings, embedded?, acc, type, is_map?) end end end + # `ARRAY[...]` is Postgres syntax. It stays the default so implementations that do not + # override `list_expr/6` render exactly as they did before. + defp default_encode_list(query, value, bindings, embedded?, acc, type, is_map?) do + element_type = + case type do + {{:array, type}, _} -> type + {{:in, type}, _} -> type + _ -> nil + end + + elements = + Enum.map(value, fn list_item -> + if type do + {:expr, %Ash.Query.Function.Type{arguments: [list_item, element_type, []]}} + else + {:expr, list_item} + end + end) + |> Enum.intersperse({:raw, ","}) + + if is_map? do + do_dynamic_expr( + query, + %Fragment{ + embedded?: embedded?, + arguments: + [ + raw: "array_to_json(ARRAY[" + ] ++ elements ++ [raw: "])"] + }, + bindings, + embedded?, + acc, + type + ) + else + do_dynamic_expr( + query, + %Fragment{ + embedded?: embedded?, + arguments: + [ + raw: "ARRAY[" + ] ++ elements ++ [raw: "]"] + }, + bindings, + embedded?, + acc, + type + ) + end + end + defp list_expr(query, value, bindings, embedded?, acc, type) do if list_requires_encoding?(value) do encode_list(query, value, bindings, embedded?, acc, type) diff --git a/lib/implementation.ex b/lib/implementation.ex index 177c9c6..090ca03 100644 --- a/lib/implementation.ex +++ b/lib/implementation.ex @@ -9,6 +9,20 @@ defmodule AshSql.Implementation do @callback repo(Ash.Resource.t(), :mutate | :read) :: module @callback expr(Ecto.Query.t(), Ash.Expr.t(), map, boolean, AshSql.Expr.ExprInfo.t(), term) :: {:ok, term, AshSql.Expr.ExprInfo.t()} | {:error, term} | :error + + @doc """ + Render a list literal that has to be encoded, i.e. one holding maps, lists or expressions. + + `AshSql.Expr` renders those as `ARRAY[...]` (or `array_to_json(ARRAY[...])`), which is + Postgres syntax. Implementations whose database has no array constructor override this to + render the list themselves. Returning `:error`, which is the default, keeps the `ARRAY[...]` + rendering. + + The value is passed after embedded resources have been dumped, so it is a plain list. + """ + @callback list_expr(Ecto.Query.t(), list(), map, boolean, AshSql.Expr.ExprInfo.t(), term) :: + {:ok, term, AshSql.Expr.ExprInfo.t()} | {:error, term} | :error + @callback simple_join_first_aggregates(Ash.Resource.t()) :: list(atom) @callback parameterized_type( @@ -50,6 +64,7 @@ defmodule AshSql.Implementation do def strpos_function, do: "strpos" def expr(_, _, _, _, _, _), do: :error + def list_expr(_, _, _, _, _, _), do: :error def simple_join_first_aggregates(_), do: [] def list_aggregate(_), do: nil def multicolumn_distinct?, do: true @@ -88,6 +103,7 @@ defmodule AshSql.Implementation do defoverridable array_overlap_operator?: 0, equals_any?: 0, expr: 6, + list_expr: 6, ilike?: 0, strpos_function: 0, require_ash_functions_for_or_and_and?: 0, From 64e976e19c30bb5356ccd76cebd373ee17938fe8 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sun, 16 Aug 2026 16:58:06 -0400 Subject: [PATCH 16/25] chore: release version v0.6.9 --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a12710..39401d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.6.9](https://github.com/ash-project/ash_sql/compare/v0.6.8...v0.6.9) (2026-08-16) + + + + +### Improvements: + +* add Implementation.list_expr/6 so adapters can render list literals by [@zachdaniel](https://github.com/zachdaniel) [(#247)](https://github.com/ash-project/ash_sql/pull/247) + ## [v0.6.8](https://github.com/ash-project/ash_sql/compare/v0.6.7...v0.6.8) (2026-08-14) diff --git a/README.md b/README.md index 4d1c3b5..3d55162 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Shared functionality for ecto-based sql data layers. ```elixir def deps do [ - {:ash_sql, "~> 0.6.8"} + {:ash_sql, "~> 0.6.9"} ] end ``` diff --git a/mix.exs b/mix.exs index 8dc6e87..4e10075 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule AshSql.MixProject do Shared utilities for ecto-based sql data layers. """ - @version "0.6.8" + @version "0.6.9" def project do [ From 2d45b52571116cbfbe9de473a45e22dd11598f4c Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 19 Aug 2026 11:19:22 +0930 Subject: [PATCH 17/25] feat: render the range expressions to their Postgres operators --- lib/expr.ex | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/lib/expr.ex b/lib/expr.ex index b99eb16..4f86c90 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -32,6 +32,11 @@ defmodule AshSql.Expr do Lazy, Length, Now, + RangeAdjacent, + RangeContains, + RangeLower, + RangeOverlaps, + RangeUpper, Rem, Round, StartOfDay, @@ -1167,6 +1172,114 @@ defmodule AshSql.Expr do end end + defp default_dynamic_expr( + query, + %RangeOverlaps{arguments: [left, right], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {[left_type, right_type], _type} = + determine_types(bindings.sql_behaviour, RangeOverlaps, [left, right], :boolean) + + {left_expr, acc} = + range_operand(query, left, left_type, bindings, pred_embedded? || embedded?, acc) + + {right_expr, acc} = + range_operand(query, right, right_type, bindings, pred_embedded? || embedded?, acc) + + {Ecto.Query.dynamic(fragment("(? && ?)", ^left_expr, ^right_expr)), acc} + end + + defp default_dynamic_expr( + query, + %RangeContains{arguments: [left, right], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {[left_type, right_type], _type} = + determine_types(bindings.sql_behaviour, RangeContains, [left, right], :boolean) + + {left_expr, acc} = + range_operand(query, left, left_type, bindings, pred_embedded? || embedded?, acc) + + {right_expr, acc} = + range_operand(query, right, right_type, bindings, pred_embedded? || embedded?, acc) + + {Ecto.Query.dynamic(fragment("(? @> ?)", ^left_expr, ^right_expr)), acc} + end + + defp default_dynamic_expr( + query, + %RangeAdjacent{arguments: [left, right], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + _type + ) do + {[left_type, right_type], _type} = + determine_types(bindings.sql_behaviour, RangeAdjacent, [left, right], :boolean) + + {left_expr, acc} = + range_operand(query, left, left_type, bindings, pred_embedded? || embedded?, acc) + + {right_expr, acc} = + range_operand(query, right, right_type, bindings, pred_embedded? || embedded?, acc) + + {Ecto.Query.dynamic(fragment("(? -|- ?)", ^left_expr, ^right_expr)), acc} + end + + defp default_dynamic_expr( + query, + %RangeLower{arguments: [range], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) do + {[determined_type], type} = + determine_types(bindings.sql_behaviour, RangeLower, [range], type) + + {range_expr, acc} = + do_dynamic_expr( + query, + range, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + determined_type || type + ) + + {Ecto.Query.dynamic(fragment("lower(?)", ^range_expr)), acc} + end + + defp default_dynamic_expr( + query, + %RangeUpper{arguments: [range], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) do + {[determined_type], type} = + determine_types(bindings.sql_behaviour, RangeUpper, [range], type) + + {range_expr, acc} = + do_dynamic_expr( + query, + range, + set_location(bindings, :sub_expr), + pred_embedded? || embedded?, + acc, + determined_type || type + ) + + {Ecto.Query.dynamic(fragment("upper(?)", ^range_expr)), acc} + end + defp default_dynamic_expr( query, %Length{arguments: [list], embedded?: pred_embedded?}, @@ -3205,6 +3318,14 @@ defmodule AshSql.Expr do end end + defp range_operand(query, operand, nil, bindings, embedded?, acc) do + do_dynamic_expr(query, operand, set_location(bindings, :sub_expr), embedded?, acc, nil) + end + + defp range_operand(query, operand, type, bindings, embedded?, acc) do + maybe_type_expr(query, operand, set_location(bindings, :sub_expr), embedded?, acc, type) + end + defp extract_list_value(value) when is_list(value), do: {:ok, value} defp extract_list_value(%MapSet{} = value), do: {:ok, value} From 91834ff46b26eccdab381386cd8e94bcbec6b349 Mon Sep 17 00:00:00 2001 From: Jonas Cosandey Date: Wed, 19 Aug 2026 11:30:52 +0200 Subject: [PATCH 18/25] fix: respect opts[:left_only?] argument passed to join_all_relationships --- lib/join.ex | 9 +++++++-- test/join_test.exs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 test/join_test.exs diff --git a/lib/join.ex b/lib/join.ex index 4da2176..509eb4c 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -82,8 +82,7 @@ defmodule AshSql.Join do parent_query, no_inner_join? ) do - no_inner_join? = - no_inner_join? || query.__ash_bindings__.context[:data_layer][:no_inner_join?] + no_inner_join? = left_join_only?(query, opts, no_inner_join?) case join_parent_paths(query, filter, relationship_paths) do {:ok, query} -> @@ -228,6 +227,12 @@ defmodule AshSql.Join do end end + @doc false + def left_join_only?(query, opts, explicit?) do + explicit? || query.__ash_bindings__.context[:data_layer][:no_inner_join?] || + opts[:left_only?] || false + end + @doc false def parent_expr(filter) do filter diff --git a/test/join_test.exs b/test/join_test.exs new file mode 100644 index 0000000..8e298f9 --- /dev/null +++ b/test/join_test.exs @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2024 ash_sql contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshSql.JoinTest do + use ExUnit.Case, async: true + + alias AshSql.Join + + defp query(data_layer_context \\ %{}) do + %{__ash_bindings__: %{context: %{data_layer: data_layer_context}}} + end + + describe "left_join_only?/3" do + test "a select-context join is never inner joined" do + assert Join.left_join_only?(query(), [left_only?: true], false) + end + + test "a filter-context join may still be inner joined" do + refute Join.left_join_only?(query(), [], false) + end + + test "the data layer context can force left joins" do + assert Join.left_join_only?(query(%{no_inner_join?: true}), [], false) + end + + test "an explicit no_inner_join? forces left joins" do + assert Join.left_join_only?(query(), [], true) + end + + test "returns a boolean rather than nil" do + assert Join.left_join_only?(query(), [], false) == false + end + end +end From 1c1eb852927e1ba8aa62d6f43a59e6e881d47cbf Mon Sep 17 00:00:00 2001 From: Jonas Cosandey Date: Wed, 19 Aug 2026 14:43:52 +0200 Subject: [PATCH 19/25] fix: pass actor to Ash.Query.for_read for aggregate and sort --- lib/aggregate.ex | 78 +++++++++++++++++++++------------------ lib/sort.ex | 55 +++++---------------------- test/aggregate_test.exs | 82 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 81 deletions(-) create mode 100644 test/aggregate_test.exs diff --git a/lib/aggregate.ex b/lib/aggregate.ex index b53ee76..9acaff5 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -969,41 +969,11 @@ defmodule AshSql.Aggregate do {:cont, {:ok, [aggregate | aggregates]}} aggregate, {:ok, aggregates} -> - related = Ash.Resource.Info.related(resource, aggregate.relationship_path) - - read_action = - aggregate.read_action || Ash.Resource.Info.primary_action!(related, :read).name - - with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action), - %{valid?: true} = aggregate_query <- - Ash.Query.build(aggregate_query, filter: aggregate.filter, sort: aggregate.sort) do - Ash.Query.Aggregate.new( - resource, - aggregate.name, - aggregate.kind, - path: aggregate.relationship_path, - query: aggregate_query, - field: aggregate.field, - default: aggregate.default, - filterable?: aggregate.filterable?, - type: aggregate.type, - sortable?: aggregate.filterable?, - include_nil?: aggregate.include_nil?, - constraints: aggregate.constraints, - implementation: aggregate.implementation, - uniq?: aggregate.uniq?, - read_action: - aggregate.read_action || - Ash.Resource.Info.primary_action!( - Ash.Resource.Info.related(resource, aggregate.relationship_path), - :read - ).name, - authorize?: aggregate.authorize? - ) - else - %{errors: errors} -> - {:error, errors} - end + resource + |> resource_aggregate_to_aggregate(aggregate, + actor: private_context[:actor], + tenant: private_context[:tenant] + ) |> case do {:ok, aggregate} -> aggregate = @@ -1027,6 +997,44 @@ defmodule AshSql.Aggregate do end) end + @doc false + def resource_aggregate_to_aggregate(resource, aggregate, opts \\ []) do + related = Ash.Resource.Info.related(resource, aggregate.relationship_path) + + read_action = + aggregate.read_action || Ash.Resource.Info.primary_action!(related, :read).name + + with %{valid?: true} = aggregate_query <- + Ash.Query.for_read(related, read_action, %{}, + actor: opts[:actor], + tenant: opts[:tenant] + ), + %{valid?: true} = aggregate_query <- + Ash.Query.build(aggregate_query, filter: aggregate.filter, sort: aggregate.sort) do + Ash.Query.Aggregate.new( + resource, + aggregate.name, + aggregate.kind, + path: aggregate.relationship_path, + query: aggregate_query, + field: aggregate.field, + default: aggregate.default, + filterable?: aggregate.filterable?, + type: aggregate.type, + sortable?: aggregate.filterable?, + include_nil?: aggregate.include_nil?, + constraints: aggregate.constraints, + implementation: aggregate.implementation, + uniq?: aggregate.uniq?, + read_action: read_action, + authorize?: aggregate.authorize? + ) + else + %{errors: errors} -> + {:error, errors} + end + end + defp add_first_join_aggregate( query, _resource, diff --git a/lib/sort.ex b/lib/sort.ex index 359246e..4776a3f 100644 --- a/lib/sort.ex +++ b/lib/sort.ex @@ -43,52 +43,15 @@ defmodule AshSql.Sort do {calculation, val} %Ash.Resource.Aggregate{} = aggregate -> - related = Ash.Resource.Info.related(resource, aggregate.relationship_path) - - read_action = - aggregate.read_action || - Ash.Resource.Info.primary_action!( - related, - :read - ).name - - with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action), - %{valid?: true} = aggregate_query <- - Ash.Query.build(aggregate_query, - filter: aggregate.filter, - sort: aggregate.sort - ) do - case Ash.Query.Aggregate.new( - resource, - aggregate.name, - aggregate.kind, - path: aggregate.relationship_path, - query: aggregate_query, - field: aggregate.field, - default: aggregate.default, - filterable?: aggregate.filterable?, - type: aggregate.type, - sortable?: aggregate.filterable?, - include_nil?: aggregate.include_nil?, - constraints: aggregate.constraints, - implementation: aggregate.implementation, - uniq?: aggregate.uniq?, - read_action: - aggregate.read_action || - Ash.Resource.Info.primary_action!( - Ash.Resource.Info.related(resource, aggregate.relationship_path), - :read - ).name, - authorize?: aggregate.authorize? - ) do - {:ok, agg} -> - {agg, val} - - {:error, error} -> - raise Ash.Error.to_ash_error(error) - end - else - %{errors: errors} -> raise Ash.Error.to_ash_error(errors) + case AshSql.Aggregate.resource_aggregate_to_aggregate(resource, aggregate, + actor: query.__ash_bindings__.context[:private][:actor], + tenant: query.__ash_bindings__.context[:private][:tenant] + ) do + {:ok, agg} -> + {agg, val} + + {:error, error} -> + raise Ash.Error.to_ash_error(error) end _ -> diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs new file mode 100644 index 0000000..691f6a9 --- /dev/null +++ b/test/aggregate_test.exs @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: 2024 ash_sql contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshSql.AggregateTest do + use ExUnit.Case, async: true + + defmodule Comment do + use Ash.Resource, domain: AshSql.AggregateTest.Domain, data_layer: Ash.DataLayer.Ets + + attributes do + uuid_primary_key(:id) + attribute(:post_id, :uuid) + attribute(:score, :integer) + end + + actions do + read :read_all do + primary?(true) + end + end + end + + defmodule Post do + use Ash.Resource, domain: AshSql.AggregateTest.Domain, data_layer: Ash.DataLayer.Ets + + attributes do + uuid_primary_key(:id) + end + + relationships do + has_many(:comments, Comment, destination_attribute: :post_id) + end + + aggregates do + max(:highest_score, :comments, :score) + end + + actions do + defaults([:read]) + end + end + + defmodule Domain do + use Ash.Domain, validate_config_inclusion?: false + + authorization do + require_actor?(true) + end + + resources do + resource(Post) + resource(Comment) + end + end + + defp build(opts) do + AshSql.Aggregate.resource_aggregate_to_aggregate( + Post, + Ash.Resource.Info.aggregate(Post, :highest_score), + opts + ) + end + + describe "resource_aggregate_to_aggregate/3" do + test "passes actor and tenant to the related read which may require an actor" do + actor = %{id: Ash.UUID.generate()} + + assert {:ok, aggregate} = build(actor: actor, tenant: "acme") + + assert aggregate.query.context.private.actor == actor + assert aggregate.query.tenant == "acme" + end + + test "reads the related resource through its primary read action" do + assert {:ok, aggregate} = build(actor: %{id: Ash.UUID.generate()}) + + assert aggregate.query.resource == Comment + assert aggregate.read_action == :read_all + end + end +end From 622b92731eb31b1b68a391ac07ec7bb9e8a58613 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Thu, 20 Aug 2026 07:04:12 +0930 Subject: [PATCH 20/25] chore: require ash 3.32 for the range expressions --- mix.exs | 2 +- mix.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index 4e10075..27dfecc 100644 --- a/mix.exs +++ b/mix.exs @@ -80,7 +80,7 @@ defmodule AshSql.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ash, ash_version("~> 3.24 and >= 3.24.5")}, + {:ash, ash_version("~> 3.32")}, {:ecto_sql, "~> 3.9"}, {:ecto, "~> 3.13 and >= 3.13.4"}, # dev/test dependencies diff --git a/mix.lock b/mix.lock index fbc69d1..a412880 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.31.2", "adc1298d04e79b14549efc06637d85a622b052d1f6fcc9fe3b884e084bde63ef", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "7126c1857f57cd73b2bf5c36dda3fa36a46b5353433207999c60727d59d3edac"}, + "ash": {:hex, :ash, "3.32.0", "046ec1b817b026aefb0a04013d340fec86ec4ceabb3f11517332edaaa7f89dca", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {: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, "~> 1.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.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [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", "ee80eeacaf5fa8995cc875741bc3f248ccd77a3e68a0927a086d9c7ed562e926"}, "benchee": {:hex, :benchee, "1.5.1", "b95cbc36c4b98969a5c592a246e171041eb683c56bad1cb4f49a3b081ba66087", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a539301f8dfd4efc5c5123bfb9d47ebde20092a863a5b5b16c2a60d2243dfce7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [: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", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, @@ -9,7 +9,7 @@ "deep_merge": {:hex, :deep_merge, "1.0.2", "476aa7ea61c54de96220051b998d893869069094da65b96101aebf79416f8a1e", [:mix], [], "hexpm", "737a53cdc9758fedbb608bdc213969e65729466c4ef3cd8e8726d0335dff116c"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.14.1", "7b740d87bdf45996aa0c2c2e081640906f10caa7ce5ba328fd294c7d49d0cc6f", [:mix], [{:decimal, "~> 3.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", "24b991956796700f467d0a3ef3d303138a3ef9ddddf8b98f43758ee067b20a30"}, + "ecto": {:hex, :ecto, "3.14.2", "99db28a864293a789c970651de711e3cae184291e0e7ea1166c54055ac41c1f3", [:mix], [{:decimal, "~> 3.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", "25d60b8c816a07d19d85b80bdf60978bd8b102209dda198d768cd7c6745339a6"}, "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, @@ -36,8 +36,8 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, - "reactor": {:hex, :reactor, "1.0.5", "43a3298fb5874248e72d61662704d5013194c427afcf740e14336f3379f53ed3", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "8867decb795d0b87a9dea5d146721af60312c0b45443d125d9be535946e8da19"}, - "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, + "reactor": {:hex, :reactor, "1.0.6", "546a87255693bcee99451d022cc86927161cdb527f5216d4dcac2b31e08eb122", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "e3f8fd8e870c2b011316ca2ac422bb4bc710cac9f02c578f8526f7a6348d932b"}, + "req": {:hex, :req, "0.7.3", "b141f1b465dabc5fb8ce67bd2f15a85fc80f6c75719910699560e32ce49f62ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "73b303030dccc2b6d023ee5ada380825ab3a7cd3863aead493db09ec420ffdf2"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, From 5b7df1800e334d51cfe18129cb147442cb0b1221 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 20 Aug 2026 17:20:29 -0400 Subject: [PATCH 21/25] chore: release version v0.7.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39401d8..c5b4c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,27 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.7.0](https://github.com/ash-project/ash_sql/compare/v0.6.9...v0.7.0) (2026-08-20) + + + + +### Features: + +* render the range expressions to their Postgres operators by [@zachdaniel](https://github.com/zachdaniel) [(#248)](https://github.com/ash-project/ash_sql/pull/248) + +* render the range expressions to their Postgres operators by [@matt-beanland](https://github.com/matt-beanland) [(#248)](https://github.com/ash-project/ash_sql/pull/248) + +### Bug Fixes: + +* pass actor to Ash.Query.for_read for aggregate and sort by [@zachdaniel](https://github.com/zachdaniel) [(#250)](https://github.com/ash-project/ash_sql/pull/250) + +* respect opts[:left_only?] argument passed to join_all_relationships by [@zachdaniel](https://github.com/zachdaniel) [(#249)](https://github.com/ash-project/ash_sql/pull/249) + +* pass actor to Ash.Query.for_read for aggregate and sort by Jonas Cosandey [(#250)](https://github.com/ash-project/ash_sql/pull/250) + +* respect opts[:left_only?] argument passed to join_all_relationships by Jonas Cosandey [(#249)](https://github.com/ash-project/ash_sql/pull/249) + ## [v0.6.9](https://github.com/ash-project/ash_sql/compare/v0.6.8...v0.6.9) (2026-08-16) diff --git a/README.md b/README.md index 3d55162..195167f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Shared functionality for ecto-based sql data layers. ```elixir def deps do [ - {:ash_sql, "~> 0.6.9"} + {:ash_sql, "~> 0.7.0"} ] end ``` diff --git a/mix.exs b/mix.exs index 27dfecc..c0ec3ce 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule AshSql.MixProject do Shared utilities for ecto-based sql data layers. """ - @version "0.6.9" + @version "0.7.0" def project do [ From 1652510febaada83a2c9af23428b05e17b47e2d1 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 21 Aug 2026 21:22:45 +0930 Subject: [PATCH 22/25] fix: round to no decimal places by default, over numeric so round/2 resolves --- lib/expr.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/expr.ex b/lib/expr.ex index 4f86c90..5676682 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -2453,15 +2453,15 @@ defmodule AshSql.Expr do acc, _type ) do - precision = Enum.at(rest, 0) || 1 + precision = Enum.at(rest, 0) || 0 frag = %Fragment{ embedded?: pred_embedded?, arguments: [ - raw: "ROUND(", + raw: "ROUND((", expr: num, - raw: ", ", + raw: ")::numeric, ", expr: precision, raw: ")" ] From 20bcdb320ab13d507eac6dff1f17f71123279d27 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 21 Aug 2026 22:17:09 +0930 Subject: [PATCH 23/25] fix: string_position is zero based, and nil when the substring is absent --- lib/expr.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/expr.ex b/lib/expr.ex index 4f86c90..d4ea67e 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -1462,11 +1462,11 @@ defmodule AshSql.Expr do %Fragment{ embedded?: pred_embedded?, arguments: [ - raw: "#{bindings.sql_behaviour.strpos_function()}((", + raw: "(NULLIF(#{bindings.sql_behaviour.strpos_function()}((", expr: left, raw: "), (", expr: right, - raw: "))" + raw: ")), 0) - 1)" ] }, bindings, From b50e4672ee96496740c4d1798602da7ac4240d3c Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 21 Aug 2026 23:34:42 +0930 Subject: [PATCH 24/25] fix: honour a fields constraint when extracting a map field --- lib/expr.ex | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/expr.ex b/lib/expr.ex index 4f86c90..f30ec3b 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -3523,6 +3523,8 @@ defmodule AshSql.Expr do :bracket end + declared_field = type && field_constraints(constraints, next) + {next, type, constraints} = cond do type && Ash.Type.embedded_type?(type) -> @@ -3559,6 +3561,10 @@ defmodule AshSql.Expr do {name, type, constraints} end + declared_field -> + {field_type, field_constraints} = declared_field + {next, field_type, field_constraints} + true -> {next, nil, nil} end @@ -3602,6 +3608,20 @@ defmodule AshSql.Expr do end end + # The type a `fields` constraint declares for `next`, if it declares one. + defp field_constraints(constraints, next) do + with true <- Keyword.keyword?(constraints || []), + fields when is_list(fields) <- constraints[:fields], + {_name, config} <- + Enum.find(fields, fn {name, _config} -> + if is_binary(next), do: to_string(name) == next, else: name == next + end) do + {config[:type], config[:constraints] || []} + else + _ -> nil + end + end + defp list_requires_encoding?(value) do !Enum.empty?(value) && Enum.any?(value, fn value -> From 8107857ee0aafc823df7cb5d00b2c87b7b2c3db6 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Sat, 22 Aug 2026 00:02:56 +0930 Subject: [PATCH 25/25] fix: the length of an empty array is zero --- lib/expr.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/expr.ex b/lib/expr.ex index 4f86c90..3f4bff9 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -1293,9 +1293,11 @@ defmodule AshSql.Expr do %Fragment{ embedded?: pred_embedded?, arguments: [ - raw: "array_length((", + raw: "coalesce(array_length((", expr: list, - raw: "), 1)" + raw: "), 1), cardinality((", + expr: list, + raw: ")))" ] }, bindings,