postgrest-client icon indicating copy to clipboard operation
postgrest-client copied to clipboard

Wrong select parameter generated by deleteByPrimaryKey with customEndpoint

Open tad-lispy opened this issue 4 years ago • 2 comments

Hi! Thanks a lot for this package. It's awesome. To the point:

I have an endpoint with the following definiton:

collections :  Postgrest.Endpoint Collection
collections =
    let
        selectFromDeck =
            [ "id"
            , "name"
            , "content->title"
            ]
                |> Postgrest.attributes
                |> Postgrest.resource "decks"
    in
    Postgrest.customEndpoint
        "collections"
        collectionDecoder
        { defaultSelect =
            Postgrest.allAttributes
                |> (::) selectFromDeck
                |> Just
        , defaultOrder = Nothing
        }

As you can see it gets collections and associated decks. It works correctly except for the deleteByPrimaryKey, like here:

deleteCollection : (Result Postgrest.Error CollectionId -> msg) -> Postgrest.JWT -> CollectionId -> Cmd msg
deleteCollection callback token id =
    id
        |> Postgrest.deleteByPrimaryKey collections collectionKey
        |> Postgrest.toCmd token callback

This produces following HTTP request:

http://[...]/collections?id=eq.780e27ac-7834-468e-978a-7e097254af52&limit=1&select=decks(id,name,content-%3Etitle),*

Please note the select query parameter. In my case this results in a 400 response with the following body:

{
    "hint":null,
    "details":null,
    "code":"42702",
    "message":"column reference \"id\" is ambiguous"
}

The following is a workaround:

  deleteCollection : (Result Postgrest.Error CollectionId -> msg) -> Postgrest.JWT -> CollectionId -> Cmd msg
  deleteCollection callback token id =
      id
          |> Postgrest.deleteByPrimaryKey collections collectionKey
+         |> Postgrest.setParams [ [ "id" ] |> Postgrest.attributes |> Postgrest.select ]
          |> Postgrest.toCmd token callback

Without looking much into it I would guess that the deleteByPrimaryKey should override the defaultSelect or maybe even the select param and only select the primary key.

The error from PostgreSQL server
2020-08-21 21:08:05.374 UTC [1919] ERROR:  column reference "id" is ambiguous at character 771
2020-08-21 21:08:05.374 UTC [1919] STATEMENT:
	      WITH
	      pgrst_source AS (WITH pgrst_ignored_body AS (SELECT $1::text)  DELETE FROM  "api"."collections" WHERE  "api"."collections"."id" = '780e27ac-7834-468e-978a-7e097254af52'::unknown  RETURNING "api"."collections".*, "api"."collections"."id")
	      SELECT
	        '' AS total_result_set,
	        pg_catalog.count(_postgrest_t) AS page_total,
	        array[]::text[] AS header,
	        coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
	        coalesce(nullif(current_setting('response.headers', true), ''), '[]') AS response_headers
	      FROM (SELECT "pgrst_source".*, COALESCE ((SELECT json_agg("pgrst_source".*) FROM (SELECT "api"."decks"."id", "api"."decks"."name", "api"."decks"."content"->'title' AS "title" FROM "api"."decks"  WHERE "pgrst_source"."id" = "api"."decks"."collection"  ) "pgrst_source"), '[]') AS "decks" FROM "pgrst_source"    LIMIT 1 OFFSET 0) _postgrest_t 2020-08-21 21:08:05.374 UTC [1919] ERROR:  column reference "id" is ambiguous at character 771
2020-08-21 21:08:05.374 UTC [1919] STATEMENT:
	      WITH
	      pgrst_source AS (WITH pgrst_ignored_body AS (SELECT $1::text)  DELETE FROM  "api"."collections" WHERE  "api"."collections"."id" = '780e27ac-7834-468e-978a-7e097254af52'::unknown  RETURNING "api"."collections".*, "api"."collections"."id")
	      SELECT
	        '' AS total_result_set,
	        pg_catalog.count(_postgrest_t) AS page_total,
	        array[]::text[] AS header,
	        coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
	        coalesce(nullif(current_setting('response.headers', true), ''), '[]') AS response_headers
	      FROM (SELECT "pgrst_source".*, COALESCE ((SELECT json_agg("pgrst_source".*) FROM (SELECT "api"."decks"."id", "api"."decks"."name", "api"."decks"."content"->'title' AS "title" FROM "api"."decks"  WHERE "pgrst_source"."id" = "api"."decks"."collection"  ) "pgrst_source"), '[]') AS "decks" FROM "pgrst_source"    LIMIT 1 OFFSET 0) 
2020-08-21 21:08:05.374 UTC [1919] ERROR:  column reference "id" is ambiguous at character 771
2020-08-21 21:08:05.374 UTC [1919] STATEMENT:
 	      WITH
 	      pgrst_source AS (WITH pgrst_ignored_body AS (SELECT $1::text)  DELETE FROM  "api"."collections" WHERE  "api"."collections"."id" = '780e27ac-7834-468e-978a-7e097254af52'::unknown  RETURNING "api"."collections".*, "api"."collections"."id")
	      SELECT
	        '' AS total_result_set,
	        pg_catalog.count(_postgrest_t) AS page_total,
	        array[]::text[] AS header,
	        coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
	        coalesce(nullif(current_setting('response.headers', true), ''), '[]') AS response_headers
	      FROM (SELECT "pgrst_source".*, COALESCE ((SELECT json_agg("pgrst_source".*) FROM (SELECT "api"."decks"."id", "api"."decks"."name", "api"."decks"."content"->'title' AS "title" FROM "api"."decks"  WHERE "pgrst_source"."id" = "api"."decks"."collection"  ) "pgrst_source"), '[]') AS "decks" FROM "pgrst_source"    LIMIT 1 OFFSET 0) 
2020-08-21 21:08:05.374 UTC [1919] ERROR:  column reference "id" is ambiguous at character 771
2020-08-21 21:08:05.374 UTC [1919] STATEMENT:
	      WITH
	      pgrst_source AS (WITH pgrst_ignored_body AS (SELECT $1::text)  DELETE FROM  "api"."collections" WHERE  "api"."collections"."id" = '780e27ac-7834-468e-978a-7e097254af52'::unknown  RETURNING "api"."collections".*, "api"."collections"."id")
	      SELECT
	        '' AS total_result_set,
	        pg_catalog.count(_postgrest_t) AS page_total,
	        array[]::text[] AS header,
	        coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
	        coalesce(nullif(current_setting('response.headers', true), ''), '[]') AS response_headers
	      FROM (SELECT "pgrst_source".*, COALESCE ((SELECT json_agg("pgrst_source".*) FROM (SELECT "api"."decks"."id", "api"."decks"."name", "api"."decks"."content"->'title' AS "title" FROM "api"."decks"  WHERE "pgrst_source"."id" = "api"."decks"."collection"  ) "pgrst_source"), '[]') AS "decks" FROM "pgrst_source"    LIMIT 1 OFFSET 0) _postgrest_t

tad-lispy avatar Aug 21 '20 21:08 tad-lispy

Thanks for the bug report @tad-lispy.

Could you provide your postgrest/postgres versions? Also is collections a table or a view? If it's a view could you provide the SQL for it?

I'm surprised the request is failing because I believe postgrest allows you to return the deleted entity, although perhaps the issue is with the nested resource.

alex-tan avatar Aug 29 '20 07:08 alex-tan

Hey! Thanks for getting back to me.

Yes, I guess it's because of the nested resource. Potentially related issue: https://github.com/PostgREST/postgrest/issues/1576.

I'm on latest stable: PostgREST 7.0.1 and Postgres 12.4.

Both collections and decks are views in the api schema becked by private schema containing tables. I believe the relevant SQL at the time was like this:

Create table if not exists private.collections (
    id uuid primary key default uuid_generate_v4(),
    owner uuid not null references private.teachers(id) default current_setting('request.jwt.claim.teacher')::uuid,
    name citext not null unique,
    title text not null,

    constraint filename check (name ~* '^[a-z0-9\-\_\.\(\)]+$')
);

Create table if not exists private.decks (
    id uuid primary key default uuid_generate_v4(),
    collection uuid not null references private.collections(id),
    name citext not null,
    content jsonb not null,

    constraint filename check (name ~* '^[a-z0-9\-\_\.\(\)]+$')
);
Create unique index collection_file on private.decks (
    name,
    collection
);

Create view api.collections as
select
    collections.id,
    collections.owner,
    collections.name,
    collections.title
from
    private.collections
;

Create or replace function delete_collection_function()
returns trigger as
$$
<<variables>>
Declare
    owner uuid;
Begin
    Select private.collections.owner
    from private.collections
    where name = old.name
    into variables.owner;

    If variables.owner is null
    then
        raise sqlstate 'PT404'
        using
            message = 'This collection is not registered',
            detail = old.name,
            hint = 'Cannot remove a non-existent collection. A typo?'
        ;
    end if;

    If not variables.owner = current_setting('request.jwt.claim.teacher')::uuid
    then
        raise insufficient_privilege
        using
            hint = 'You are not the owner of this collection',
            detail = old.name
        ;
    end if;

    Delete from private.collections
    where private.collections.name = old.name;

    return null;
end;
$$
language plpgsql
security definer;

Create trigger delete_collection_trigger
instead of delete on api.collections
for each row
execute function delete_collection_function();


Create view api.decks as
select
    decks.id,
    decks.collection,
    decks.name,
    decks.content
from
    private.decks
;

One controversial design decision I made early and abandoned since was to name all primary keys id (e.g. collections.id, decks.id etc). That may have contribute to the confusion. So since I posted the issue I changed that and now all keys are prefixed, like collections.collection_id, decks.deck_id, etc). But I believe it had no effect on the issue.

tad-lispy avatar Aug 31 '20 07:08 tad-lispy