ash icon indicating copy to clipboard operation
ash copied to clipboard

Support destroy and update actions on resources that have no primary key

Open timadevelop opened this issue 1 year ago • 2 comments

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

  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:

  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

timadevelop avatar Dec 13 '24 16:12 timadevelop

Manual actions are now skipped by that check as of 867a84bad7f81765326cc7b8d5ea4754c3014141

zachdaniel avatar Dec 13 '24 16:12 zachdaniel

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

zachdaniel avatar Dec 13 '24 16:12 zachdaniel