How to Read Views columns that are not matching Resource attributes?

How to read VIEW columns from a custom statement that don’t exist as attribute on the resource without throwing a Ash.Error.Query.NoSuchAttribute

defmodule MyApp.Members.Member do
  use Ash.Resource,
    domain: MyApp.Members,
    data_layer: AshPostgres.DataLayer

  postgres do
    table "members"
    repo SocialFund.Repo

    custom_statements do
      statement :view_member_contributions do
        # Migrations up
        up "CREATE VIEW view_member_contributions
              AS SELECT
               members.member_number,
               contributions.amount AS contributed_amount,
               contributions.month AS period
            FROM members LEFT JOIN  contributions
            ON members.id = contributions.member_id;"

        # Migration down
        down "DROP VIEW IF EXISTS view_member_contributions;"
      end
    end
  end

  actions do
    read :contribution_report do
      prepare build(select: [:member_number, :contributed_amount, :period])
      prepare set_context(%{data_layer: %{table: "view_member_contributions"}})
    end
  end

 # Other definitions
end

Hi kamaroly,
You can handle columns from a view that aren’t attributes on your resource by either defining virtual attributes marked as read_only: true or creating a custom read action that selects those extra columns. This way, Ash won’t throw a NoSuchAttribute error, but you can still access the data from your view.

1 Like

Thanks. Please share an example code.

1 Like

@kamaroly why would you not just make them attributes?

Attributes will create columns in the DB but views are only for reading. Is there a way to declare attributes without having them created as columns in the DB?

I am looking for a way to create an attribute that won’t generate migrations to create a new table in the DB and add that attribute as a column.

You can set migration_ignore_attributes in the Postgres block to tell it to ignore fields and not generate migrations for them.

Thanks @zachdaniel, It helped but another problem emerged.

I realised the query is always selecting ID but the action didn’t specify ID in its selection. This fails the query.

The action

    read :contribution_report do
      prepare build(select: [:contributed_amount, :period, :membership_no])
      prepare set_context(%{data_layer: %{table: "view_member_contribution_report"}})
    end

The error

iex(9)> Ash.read(MyApp.Members.Member, action: :contribution_report, tenant: "personal_team_1", authorize?: false)
[debug] QUERY ERROR source="view_member_contribution_report" db=0.0ms queue=0.9ms idle=1345.7ms
SELECT v0."id", v0."period", v0."contributed_amount", v0."membership_no" FROM "personal_team_1"."view_member_contribution_report" AS v0 []
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:835
{:error,
 %Ash.Error.Unknown{
   bread_crumbs: ["Error returned from: SocialFund.Members.Member.contribution_report"], 
   query: "#Query<>", 
   errors: [
     %Ash.Error.Unknown.UnknownError{
       error: "** (Postgrex.Error) ERROR 42703 (undefined_column) column v0.id does not exist\n\n    query: SELECT v0.\"id\", v0.\"period\", v0.\"contributed_amount\", v0.\"membership_no\" FROM \"personal_team_1\".\"view_member_contribution_report\" AS v0",
       field: nil,
       value: nil,
       splode: Ash.Error,
       bread_crumbs: ["Error returned from: SocialFund.Members.Member.contribution_report"],
       vars: [],
       path: [],
       stacktrace: #Splode.Stacktrace<>,
       class: :unknown
     }
   ]
 }}

We always select the primary key. Can that not be added to the view?

1 Like

It can be added.

I am seeing a limitation when there’s a need to offload aggregations to the DB due to complexity or speed optimisation. Something like below that won’t require id selection.

CREATE VIEW view_contributions_by_member_category AS
SELECT 
   members.category member_category, 
   sum(contributions.amount) AS contributed_amount
FROM members, contributions
WHERE members.id == contributions.member_id
GROUP BY  members.category member_category;

I hope we’ll be able to turn off id selection in the future.

Probably not, TBH. It’s used for a whole bunch of things in Ash proper. You can remove the id from your resource entirely if needed, but not de-select it for a resource that has a primary key.

1 Like