Support destroy and update actions on resources that have no primary key
Right now, ash does not allow defining update and destroy actions on resources without a PK:
# 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
- Allow manual destroy and update actions to bypass that check
- Use the first identity to destroy/update if present, and change that check
- 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:
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
Manual actions are now skipped by that check as of 867a84bad7f81765326cc7b8d5ea4754c3014141
The rest will be a bigger job, though, and will require more than a few changes so we'll have to leave it for now :)