Support destroy and update actions on resources that have no primary key
- Dominant language
- Elixir
- Stars
- 2.5k
- Forks
- 426
- Avg merge
- 23h 26m
- Merged PRs (30d)
- 46
Description
Right now, ash does not allow defining `update` and `destroy` actions on resources without a PK:
```elixir
# require_primary_key? false and update :some_update in resource causes the following compile time error
actions -> update -> some_update:
AshPostgres.DataLayer does not support update actions on this resource.
This is either because of a limitation in the data layer, or specific configuration of the resource.
```
Some databases, e.g. timescaledb extension, do not allow having a PK on a table. Instead of PK, some timescaledb users utilize a unique index based on `time` + other columns (e.g. `id, device_id, time`).
While it's possible to define a generic action to workaround this issue, it would cause us to loose action type semantics and other instruments ash provides for `update`/`destroy` actions.
As @zachdaniel noted before https://discord.com/channels/711271361523351632/711271361523351636/1317093457982001225, perhaps it makes sense to
> 1. Allow manual destroy and update actions to bypass that check
> 2. Use the first identity to destroy/update if present, and change that check
> 3. Ensure the first identity is always selected if no primary key exists.
@zachdaniel afaiu the first point implies that ash will allow defining **manual** `update` & `destroy` actions *even if there is no identity/unique index*. I believe it makes sense to bypass the check **and** not require defining any `identities` because
a) both destroy and update actions might not be limited to a single row and
b) some resources might have their own way for identifying a specific entry they need to update/destroy that is not known for data layer or ash, but rather enforced on a higher level (app, device, transport protocol, etc.). I can imagine a scenario where someone would like to have no unique indices to improve data ingestion performance on some table. They might or might not enforce uniqueness in business logic instead, but *regardless of their decision*, it makes sense to allow defining manual `destroy` / `update` actions in Ash.Resources without any identity.
Short example with comments that might be useful:
```elixir
attributes do
# not a PK
attribute :id, :integer do
generated? false
primary_key? false
public? true
end
# in timescale, tables have to have some timstamp for chunking/partitioning
attribute :time, AshPostgres.TimestamptzUsec do
allow_nil? false
public? true
constraints precision: :microsecond, timezone: :utc
end
end
relationships do
belongs_to :device, Device do
allow_nil? false
public? true
end
end
resource do
require_primary_key? false
end
identities do
# in our use case we tolerate having this unique index, but at some point for specific hypertables,
# we might consider dropping this index whatsoever for ingestion performance
identity :unique_id_device_id_time, [:id, :device_id, :time]
end
actions do
defaults [:read]
destroy :expunge_old do
# ... destroy multiple entries
end
destroy :delete_specific do
# ... destroy specific resource, based on
# identity or with manual actions based on our own logic unknown to ash & data layer
end
update :reset_time do
# ... same but for update
end
create :create do
# create works fine
accept :*
# allow upserts
upsert? true
# use ON CONFLICT (:id, :device_id, time)
upsert_identity :unique_id_device_id_time
# force DO NOTHING
upsert_fields []
end
end
```
Contributor guide
Research direction
Start by tracing the AshPostgres.DataLayer check that rejects update and destroy actions when a resource has no primary key, then review the resource action and identity behavior described in the issue. Done means manual actions can support the stated no-primary-key cases while preserving the relevant update and destroy semantics, with the chosen identity behavior made explicit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- elixir, postgres
- Domain
- backend-api-design, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100