src/models/m_edge.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2026 Marc Worrell
%% @doc Model for accessing and manipulating edges between resources.
%% @end

%% Copyright 2009-2026 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(m_edge).
-moduledoc("
Access information about page connections.

Edges represent the connections between resources. They are implemented as tuples `{EdgeId, SubjectId, PredicateId, ObjectId,
OrderNr}`. The edge id is a unique id representing the edge, it can be used with edit actions. The OrderNr defines the
order of the edges with respect to the subject.

Most edge information is accessed using the [m_rsc](/id/doc_model_model_rsc) model, but some information can only
accessed with the m_edge model.

This model implements two template accessible options. They are mainly used to obtain the edge's id for edit pages.

The following m_edge model properties are available in templates:

| Property  | Description                                                                      | Example value                                                                    |
| --------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| o         | Returns a function that accepts a page  id and a predicate. The end result is a list of tuples {PageId, EdgeId} which are objects of the page. Example usage: `m.edge.o[id].author` | `[{204,13},{510,14}, {508,15}]`                                                  |
| s         | Identical to the \"o\" property, except that this function returns the subject edges. |                                                                                  |
| o_props | Similar to `m.edge.o[id].author` above, but returns a property list for the edges instead of the 2-tuple. | `> [ > {id, 86062}, > {subject_id, 10635}, > {predicate_id, 304}, > {object_id, 57577}, > {seq, 1}, > {creator_id, 1}, > {created, { > {2015,11,17}, > {11,23,32} > }} > ] > ]` |
| s_props | Similar to `m.edge.s[id].author` above, but returns a property list for the edges instead of the 2-tuple. |                                                                                  |
| edges | Returns a function that accepts a page id. The end result is a list of edges per predicate where the predicate is an atom and the edges are property lists. Example usage: `m.edge[10635]` | `See example below.` |
| id | Look up an edge id by a subject/predicate/object triple. Example usage:   ```erlang m.edge.id[subject_id].relation[object_id] ```  or:   ```erlang m.edge.id[subject_id][predicate_name][object_id] ```  Returns `undefined` if the edge does not exist; otherwise returns an integer. | `213` |

Example return value for `{% print m.edge[10635] %}`:


```erlang
[{about,[[{id,86060},
          {subject_id,10635},
          {predicate_id,300},
          {name,<<\"about\">>},
          {object_id,17433},
          {seq,1},
          {created,{{2015,11,17},{11,22,11}}},
          {creator_id,1}]]},
 {author,[[{id,6},
           {subject_id,10635},
           {predicate_id,301},
           {name,<<\"author\">>},
           {object_id,10634},
           {seq,1000000},
           {created,{{2015,2,3},{16,23,20}}},
           {creator_id,1}]]}]
```



Other Topics
------------

`model/edge/post/o/+subject/+predicate/+object` or `model/edge/post/s/+object/+predicate/+subject` inserts a new edge between resources.

The posted message can optionally include the name or id of the object, predicate and subject.


```javascript
cotonic.broker.publish(\"bridge/origin/edge/post/o/4312\",
                       {
                         predicate: \"author\",
                         subject: 7575
                       });
```

It is also possible to insert edges via cotonics onclick topics.


```django
<div data-onclick-topic=\"bridge/origin/model/edge/post/o/{{ id }}/?/{{ m.acl.user }}\">
   <button data-edge-predicate=\"is_going\">Is Going</button>
   <button data-edge-predicate=\"is_interested\">Might Go</button>
</div>
```

When a user clicks on a button, the model retrieves the predicate name (or id) from the `data-edge-predicate` attribute.
This is also possible by for the object and subject attributes of the edge. When there is a `?` in the topic path, the
value can be retrieved from a data attribute. The attribute value for object is: `data-edge-object`. For subject it is: `data-edge-subject`.
If all path parts from the first `?` to the end are `?`, then those trailing `?` parts may be omitted from the topic path.

`model/edge/delete/o/+subject/+predicate/+object`, `model/edge/delete/s/+object/+predicate/+subject` or `model/edge/delete/edge/+edge_id` deletes the specified edge.


```javascript
cotonic.broker.publish(\"bridge/origin/edge/delete/edge/6776\");
```

Or via a onclick topic.


```django
<button data-onclick-topic=\"bridge/origin/edge/delete/{{ edge_id }}\">Delete</button
```

Available Model API Paths
-------------------------

| Method | Path pattern | Description |
| --- | --- | --- |
| `get` | `/o/+id/+pred/...` | Return outgoing edge tuples `{ObjectId, EdgeId}` from subject `+id` for predicate `+pred` (subject must be visible). |
| `get` | `/o_props/+id/+pred/...` | Return outgoing edge rows (id/subject/predicate/object/seq/created/creator) from subject `+id` for predicate `+pred`. |
| `get` | `/s/+id/+pred/...` | Return incoming edge tuples `{SubjectId, EdgeId}` pointing to object `+id` for predicate `+pred` (object must be visible). |
| `get` | `/s_props/+id/+pred/...` | Return incoming edge rows (id/subject/predicate/object/seq/created/creator) for object `+id` and predicate `+pred`. |
| `get` | `/edges/+id/...` | Return all outgoing edges of resource `+id` grouped by predicate name. |
| `get` | `/id/+subjectid/+pred/+objectid/...` | Return edge id for (`+subjectid`, `+pred`, `+objectid`) or `undefined` if absent (subject must be visible). |
| `get` | `/graph/...` | Return graph map (`nodes`, `edges`, `is_truncated`) for payload `ids`, with optional payload options `limit` and `unescape`. |
| `get` | `/+id` | Return same grouped outgoing-edge data as `/edges/+id` for resource `+id`. No further lookups. |
| `post` | `/o/...` | Insert one edge using object-oriented argument order `(subject, predicate, object)`; values come from path segments, payload keys, `message.data-edge-*`, or query args. |
| `post` | `/s/...` | Insert one edge using subject-oriented path order `(object, predicate, subject)`; stored edge is still `(subject, predicate, object)`. |
| `delete` | `/o/...` | Delete edge(s) using object-oriented order `(subject, predicate, object)`. |
| `delete` | `/s/...` | Delete edge(s) using subject-oriented path order `(object, predicate, subject)`; this maps to stored triple `(subject, predicate, object)`. |
| `delete` | `/edge/+edge` | Delete a single edge by edge id `+edge` (invalid id returns `enoent`). No further lookups. |

`/+name` marks a variable path segment. A trailing `/...` means extra path segments are accepted for further lookups.


Post Method Examples
--------------------

Path orientation notes:

- Object-oriented path (`/o/...`) uses order: `subject / predicate / object`.
- Subject-oriented path (`/s/...`) uses order: `object / predicate / subject`.
- Both forms create or delete the same edge triple in storage: `(subject, predicate, object)`.
- Trailing wildcard parts can be omitted: if all path parts from the first `?` to the end are `?`, you may stop the path at that first `?`.

Insert via object-oriented path:

```javascript
cotonic.broker.call(
    \"bridge/origin/model/edge/post/o/7575/author/4312\",
    {}
);
```

Insert via wildcard path values (`?`) resolved from payload:

```javascript
cotonic.broker.call(
    \"bridge/origin/model/edge/post/o/?/?/?\",
    {
        subject: 7575,
        predicate: \"author\",
        object: 4312
    }
);
```

Insert with trailing wildcards omitted (only subject in path, predicate/object from payload):

```javascript
cotonic.broker.call(
    \"bridge/origin/model/edge/post/o/7575\",
    {
        predicate: \"author\",
        object: 4312
    }
);
```

Insert with all values from payload (shortest path):

```javascript
cotonic.broker.call(
    \"bridge/origin/model/edge/post/o\",
    {
        subject: 7575,
        predicate: \"author\",
        object: 4312
    }
);
```

Insert via subject-oriented path:

```javascript
cotonic.broker.call(
    \"bridge/origin/model/edge/post/s/4312/author/7575\",
    {}
);
```

See also

[m_rsc](/id/doc_model_model_rsc), [m_media](/id/doc_model_model_media)").
-author("Marc Worrell <marc@worrell.nl").

-behaviour(zotonic_model).

%% interface functions
-export([
    m_get/3,
    m_post/3,
    m_delete/3,

    get_graph/3,
    get/2,
    get_triple/2,
    get_id/4,
    get_edges/2,
    insert/4,
    insert/5,
    delete/2,
    delete/4,
    delete/5,
    delete_multiple/4,
    replace/4,
    duplicate/3,
    merge/3,
    update_nth/5,
    object/4,
    subject/4,
    objects/3,
    subjects/3,
    objects/2,
    subjects/2,
    has_objects/2,
    has_subjects/2,
    object_edge_ids/3,
    subject_edge_ids/3,
    object_edge_props/3,
    subject_edge_props/3,
    update_sequence/4,
    set_sequence/4,
    update_sequence_edge_ids/4,
    object_predicates/2,
    subject_predicates/2,
    object_predicate_ids/2,
    subject_predicate_ids/2
]).

-include_lib("zotonic.hrl").

-type insert_options() :: [ insert_option() ].

-type insert_option() :: is_insert_before
                       | no_touch
                       | {seq, integer()}
                       | {creator_id, m_rsc:resource_id()}
                       | {created, calendar:datetime()}.

-export_type([
    insert_options/0,
    insert_option/0
]).

%% Default limit for the number of edges returned in the graph function.
%% This is needed to prevent performance issues when a resource has a very
%% large number of incoming and/or outgoing edges.
-define(DEFAULT_GRAPH_EDGE_LIMIT, 5000).


%% @doc Fetch all object/edge ids for a subject/predicate
-spec m_get( list(), zotonic_model:opt_msg(), z:context()) -> zotonic_model:return().
m_get([ <<"o">>, Id, Pred | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {object_edge_ids(Id, Pred, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"o_props">>, Id, Pred | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {object_edge_props(Id, Pred, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"s">>, Id, Pred | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {subject_edge_ids(Id, Pred, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"s_props">>, Id, Pred | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {subject_edge_props(Id, Pred, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"edges">>, Id | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {get_edges(Id, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"id">>, SubjectId, Pred, ObjectId | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(SubjectId, Context) of
        true ->
            % m.edge.id[subject_id].predicatename[object_id] returns the
            % corresponding edge id or undefined.
            V = z_depcache:memo(
                fun() ->
                    get_id(SubjectId, Pred, ObjectId, Context)
                end,
                {get_id, SubjectId, Pred, ObjectId}, ?DAY, [SubjectId], Context),
            {ok, {V, Rest}};
        false ->
            {error, eacces}
    end;
m_get([ <<"graph">> | Rest ], #{ payload := #{ <<"ids">> := Ids } = Payload }, Context) ->
    Options = maps:fold(
        fun
            (<<"limit">>, Limit, Acc) ->
                [ {limit, z_convert:to_integer(Limit)} | Acc ];
            (<<"unescape">>, Unescape, Acc) ->
                case z_convert:to_bool(Unescape) of
                    true -> [ unescape | Acc ];
                    false -> Acc
                end;
            (_K, _V, Acc) -> Acc
        end,
        [],
        Payload),
    {ok, Graph} = get_graph(Ids, Options, Context),
    {ok, {Graph, Rest}};
m_get([Id], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {get_edges(Id, Context), []}};
        false -> {error, eacces}
    end;
m_get(_Vs, _Msg, _Context) ->
    {error, unknown_path}.

-spec m_post( list(), zotonic_model:opt_msg(), z:context()) -> zotonic_model:post_return().
m_post([<<"o">> | Path ], #{ payload := Payload }, Context) ->
    {Subject, Predicate, Object} = get_spo_o(Path, Payload, Context),
    do_post_insert(Subject, Predicate, Object, Context);
m_post([<<"s">> | Path ], #{ payload := Payload }, Context) ->
    {Subject, Predicate, Object} = get_spo_s(Path, Payload, Context),
    do_post_insert(Subject, Predicate, Object, Context).

-spec m_delete( list(), zotonic_model:opt_msg(), z:context()) -> zotonic_model:delete_return().
m_delete([<<"o">> | Path], #{payload := Payload}, Context) ->
    {Subject, Predicate, Object} = get_spo_o(Path, Payload, Context),
    delete(Subject, Predicate, Object, Context);
m_delete([<<"s">> | Path], #{payload := Payload}, Context) ->
    {Subject, Predicate, Object} = get_spo_s(Path, Payload, Context),
    delete(Subject, Predicate, Object, Context);
m_delete([<<"edge">>, Edge], _Msg, Context) ->
    case z_convert:to_integer(Edge) of
        undefined ->
            {error, enoent};
        EdgeId ->
            delete(EdgeId, Context)
    end.


%% @doc Get the complete graph for the given resource ids. Returns a map
%% with nodes and edges. The function accepts two options, 'limit' and
%% 'unescape'. The 'limit' option limits the number of edges returned for
%% each direction (in and out). The 'unescape' option unescapes the labels of
%% the nodes and edges.
-spec get_graph(Ids, Options, Context) -> {ok, Graph} when
    Ids :: [ m_rsc:resource() ],
    Context :: z:context(),
    Graph :: #{
        nodes => [Node],
        edges => [Edge],
        is_truncated => boolean()
    },
    Options :: [ Option ],
    Option :: unescape
            | {limit, pos_integer()},
    Node :: #{
        id => m_rsc:resource_id(),
        label => binary(),
        category => binary(),
        category_name => binary(),
        category_id => m_rsc:resource_id(),
        edgesLoaded => boolean()
    },
    Edge :: #{
        id => pos_integer(),
        from => m_rsc:resource_id(),
        to => m_rsc:resource_id(),
        label => binary(),
        predicate_id => m_rsc:resource_id()
    }.
get_graph(Ids, Options, Context) ->
    Ids1 = lists:filtermap(
        fun(Id) ->
            case m_rsc:rid(Id, Context) of
                undefined -> false;
                RId ->
                    case z_acl:rsc_visible(RId, Context) of
                        true -> {true, RId};
                        false -> false
                    end
            end
        end,
        Ids),
    Limit = proplists:get_value(limit, Options, ?DEFAULT_GRAPH_EDGE_LIMIT),
    Unescape = proplists:get_bool(unescape, Options),
    Out = z_db:q("
        select id, subject_id, predicate_id, object_id
        from edge
        where subject_id = any($1)
        order by id desc
        limit $2",
        [ Ids1, Limit ],
        Context),
    In = z_db:q("
        select id, subject_id, predicate_id, object_id
        from edge
        where object_id = any($1)
        order by id desc
        limit $2",
        [ Ids1, Limit ],
        Context),
    IsTruncated = length(Out) >= Limit orelse length(In) >= Limit,
    OutRscIds = [ ObjId || {_, _, _, ObjId} <- Out ],
    InRscIds = [ SubjId || {_, SubjId, _, _} <- In ],
    OutSet = sets:from_list(OutRscIds),
    InSet = sets:from_list(InRscIds),
    RootSet = sets:from_list(Ids1),
    All = sets:union([ OutSet, InSet, RootSet ]),
    AllIds = sets:to_list(All),
    Nodes = lists:foldl(
        fun(Id, Acc) ->
            case z_acl:rsc_visible(Id, Context) of
                true ->
                    CatId = m_rsc:p_no_acl(Id, <<"category_id">>, Context),
                    Acc#{
                        Id => #{
                            id => Id,
                            label => title(Id, Unescape, Context),
                            category => title(CatId, Unescape, Context),
                            category_name => name(CatId, Context),
                            category_id => CatId,
                            edgesLoaded => sets:is_element(Id, RootSet)
                        }
                    };
                false ->
                    Acc
            end
        end,
        #{},
        AllIds),
    Edges = lists:foldl(
        fun({EdgeId, SubjId, PredId, ObjId}, Acc) ->
            case not maps:is_key(EdgeId, Acc)
                andalso maps:is_key(SubjId, Nodes)
                andalso maps:is_key(ObjId, Nodes)
            of
                true ->
                    Acc#{
                        EdgeId => #{
                            id => EdgeId,
                            from => SubjId,
                            to => ObjId,
                            label => title(PredId, Unescape, Context),
                            predicate_id => PredId
                        }
                    };
                false ->
                    Acc
            end
        end,
        #{},
        Out),
    Edges1 = lists:foldl(
        fun({EdgeId, SubjId, PredId, ObjId}, Acc) ->
            case not maps:is_key(EdgeId, Acc)
                andalso maps:is_key(SubjId, Nodes)
                andalso maps:is_key(ObjId, Nodes)
            of
                true ->
                    Acc#{
                        EdgeId => #{
                            id => EdgeId,
                            from => SubjId,
                            to => ObjId,
                            label => title(PredId, Unescape, Context),
                            predicate_id => PredId
                        }
                    };
                false ->
                    Acc
            end
        end,
        Edges,
        In),
    {ok, #{
        nodes => maps:values(Nodes),
        edges => maps:values(Edges1),
        is_truncated => IsTruncated
    }}.

title(Id, Unescape, Context) ->
    case z_memo:get({title, Id}) of
        undefined ->
            T = z_trans:lookup_fallback(m_rsc:p_no_acl(Id, <<"title">>, Context), Context),
            T1 = case z_utils:is_empty(T) of
                true -> <<"(", (integer_to_binary(Id))/binary, ")">>;
                false -> T
            end,
            T2 = case Unescape of
                true -> z_html:unescape(T1);
                false -> T1
            end,
            z_memo:set({title, Id}, T2),
            T2;
        Title ->
            Title
    end.

name(Id, Context) ->
    case m_rsc:p(Id, <<"name">>, Context) of
        undefined -> <<>>;
        Name -> Name
    end.

%% @doc Get the complete edge with the id
-spec get(EdgeId, Context) -> proplists:proplist() | undefined when
    EdgeId :: integer(),
    Context :: z:context().
get(Id, Context) ->
    z_db:assoc_row("select * from edge where id = $1", [Id], Context).

%% @doc Get the edge as a triple {subject_id, predicate, object_id}
-spec get_triple(EdgeId, Context) -> {SubjectId, Predicate, ObjectId} | undefined when
    EdgeId :: integer(),
    Context :: z:context(),
    SubjectId :: m_rsc:resource_id(),
    Predicate :: atom(),
    ObjectId :: m_rsc:resource_id().
get_triple(Id, Context) ->
    case z_db:q_row("
            select e.subject_id, r.name, e.object_id
            from edge e join rsc r on e.predicate_id = r.id
            where e.id = $1", [Id], Context)
    of
        {SubjectId, Predicate, ObjectId} ->
            {SubjectId, z_convert:to_atom(Predicate), ObjectId};
        undefined ->
            undefined
    end.

%% @doc Get the edge id of a subject/pred/object combination
-spec get_id(Subject, Predicate, Object, Context) -> EdgeId | undefined when
    Subject :: m_rsc:resource(),
    Predicate :: m_rsc:resource(),
    Object :: m_rsc:resource(),
    Context :: z:context(),
    EdgeId :: pos_integer().
get_id(SubjectId, PredId, ObjectId, Context)
    when is_integer(SubjectId), is_integer(PredId), is_integer(ObjectId) ->
    z_db:q1(
        "select id from edge where subject_id = $1 and object_id = $3 and predicate_id = $2",
        [SubjectId, PredId, ObjectId],
        Context
    );
get_id(undefined, _PredId, _ObjectId, _Context) -> undefined;
get_id(_SubjectId, undefined, _ObjectId, _Context) -> undefined;
get_id(_SubjectId, _PredId, undefined, _Context) -> undefined;
get_id(Subject, Pred, ObjectId, Context) when not is_integer(Subject) ->
    get_id(m_rsc:rid(Subject, Context), Pred, ObjectId, Context);
get_id(SubjectId, Pred, ObjectId, Context) when not is_integer(Pred) ->
    case m_predicate:name_to_id(Pred, Context) of
        {ok, PredId} -> get_id(SubjectId, PredId, ObjectId, Context);
        {error, _} -> undefined
    end;
get_id(SubjectId, Pred, Object, Context) when not is_integer(Object) ->
    get_id(SubjectId, Pred, m_rsc:rid(Object, Context), Context).

%% @doc Return the full description of all edges from a subject, grouped by predicate
-spec get_edges(Subject, Context) -> PredicateEdges when
    Subject :: m_rsc:resource(),
    Context :: z:context(),
    PredicateEdges :: list( {Predicate, Edges}),
    Predicate :: atom(),
    Edges :: proplists:proplist().
get_edges(Subject, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            [];
        SubjectId ->
            case z_depcache:get({edges, SubjectId}, Context) of
                {ok, Edges} ->
                    Edges;
                undefined ->
                    Edges = z_db:assoc("
                        select e.id, e.subject_id, e.predicate_id, p.name, e.object_id,
                               e.seq, e.created, e.creator_id
                        from edge e join rsc p on p.id = e.predicate_id
                        where e.subject_id = $1
                        order by e.predicate_id, e.seq, e.id", [SubjectId], Context),
                    Edges1 = z_utils:group_proplists(name, Edges),
                    Edges2 = [ {z_convert:to_atom(Pred), Es} || {Pred, Es} <- Edges1 ],
                    z_depcache:set({edges, SubjectId}, Edges2, ?DAY, [SubjectId], Context),
                    Edges2
            end
    end.

%% @doc Insert a new edge. If the edge exists then the edge-id of te existing edge
%% is returned.
-spec insert(SubjectId, Predicate, ObjectId, Context) -> {ok, EdgeId} | {error, Reason} when
    SubjectId :: m_rsc:resource(),
    Predicate :: m_rsc:resource(),
    ObjectId :: m_rsc:resource(),
    Context :: z:context(),
    EdgeId :: pos_integer(),
    Reason :: {unknown_predicate, m_rsc:resource()} | object | subject | eacces | unknown.
insert(Subject, Pred, Object, Context) ->
    insert(Subject, Pred, Object, [], Context).

-spec insert(m_rsc:resource(), m_rsc:resource(), m_rsc:resource(), insert_options(), z:context()) ->
        {ok, EdgeId :: pos_integer()} | {error, term()}.
insert(SubjectId, PredId, ObjectId, Opts, Context)
    when is_integer(SubjectId), is_integer(PredId), is_integer(ObjectId) ->
    case m_predicate:is_predicate(PredId, Context) of
        true -> insert1(SubjectId, PredId, ObjectId, Opts, Context);
        false -> {error, {unknown_predicate, PredId}}
    end;
insert(SubjectId, Pred, ObjectId, Opts, Context)
    when is_integer(SubjectId), is_integer(ObjectId) ->
    {ok, PredId} = m_predicate:name_to_id(Pred, Context),
    insert1(SubjectId, PredId, ObjectId, Opts, Context);
insert(SubjectId, Pred, Object, Opts, Context) when is_integer(SubjectId) ->
    case m_rsc:rid(Object, Context) of
        undefined -> {error, object};
        Id -> insert(SubjectId, Pred, Id, Opts, Context)
    end;
insert(Subject, Pred, Object, Opts, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined -> {error, subject};
        Id -> insert(Id, Pred, Object, Opts, Context)
    end.

insert1(SubjectId, PredId, ObjectId, Opts, Context) ->
    case z_db:q1("
        select id
        from edge
        where subject_id = $1
          and object_id = $2
          and predicate_id = $3",
        [SubjectId, ObjectId, PredId],
        Context)
    of
        undefined ->
            {ok, PredName} = m_predicate:id_to_name(PredId, Context),
            case z_acl:is_allowed(insert,
                                  #acl_edge{subject_id=SubjectId, predicate=PredName, object_id=ObjectId},
                                  Context)
            of
                true ->
                    Created = case proplists:get_value(created, Opts) of
                        DT when is_tuple(DT) -> DT;
                        undefined -> undefined
                    end,
                    CreatorId = case proplists:get_value(creator_id, Opts) of
                        undefined -> z_acl:user(Context);
                        CId -> CId
                    end,
                    Transaction = fun(Ctx) ->
                        SeqOpt = maybe_seq_opt(Opts, SubjectId, PredId, Ctx),
                        insert_edge_1(SubjectId, ObjectId, PredId, SeqOpt, CreatorId, Created, Ctx)
                    end,
                    case z_db:transaction(Transaction, Context) of
                        {ok, 1, _, [{EdgeId}]} ->
                            z_edge_log_server:check(Context),
                            {ok, EdgeId};
                        {ok, 0, _, []} ->
                            % Race condition, edge might have been inserted by another transaction
                            case z_db:q1("
                                select id
                                from edge
                                where subject_id = $1
                                  and object_id = $2
                                  and predicate_id = $3",
                                [SubjectId, ObjectId, PredId],
                                Context)
                            of
                                undefined ->
                                    % Some other error during insert -- should not happen
                                    ?LOG_ERROR(#{
                                        in => zotonic_core,
                                        text => <<"Error inserting edge">>,
                                        result => error,
                                        reason => unknown,
                                        subject_id => SubjectId,
                                        predicate_id => PredId,
                                        object_id => ObjectId,
                                        creator_id => CreatorId,
                                        created => Created
                                    }),
                                    {error, unknown};
                                EdgeId ->
                                    {ok, EdgeId}
                            end;
                        {error, Reason} ->
                            ?LOG_ERROR(#{
                                in => zotonic_core,
                                text => <<"Error inserting edge">>,
                                result => error,
                                reason => Reason,
                                subject_id => SubjectId,
                                predicate_id => PredId,
                                object_id => ObjectId,
                                creator_id => CreatorId,
                                created => Created
                            }),
                            {error, enoent}
                    end;
                false ->
                    {error, eacces}
            end;
        EdgeId ->
            % Edge exists - skip
            {ok, EdgeId}
end.

insert_edge_1(SubjectId, ObjectId, PredId, undefined, CreatorId, undefined, Context) ->
    z_db:equery("
        insert into edge
            (subject_id, object_id, predicate_id, creator_id)
        values
            ($1, $2, $3, $4)
        on conflict do nothing
        returning id
        ",
        [
            SubjectId,
            ObjectId,
            PredId,
            CreatorId
        ],
        Context);
insert_edge_1(SubjectId, ObjectId, PredId, Seq, CreatorId, undefined, Context) ->
    z_db:equery("
        insert into edge
            (subject_id, object_id, predicate_id, seq, creator_id)
        values
            ($1, $2, $3, $4, $5)
        on conflict do nothing
        returning id
        ",
        [
            SubjectId,
            ObjectId,
            PredId,
            Seq,
            CreatorId
        ],
        Context);
insert_edge_1(SubjectId, ObjectId, PredId, undefined, CreatorId, Created, Context) ->
    z_db:equery("
        insert into edge
            (subject_id, object_id, predicate_id, creator_id, created)
        values
            ($1, $2, $3, $4, $5)
        on conflict do nothing
        returning id
        ",
        [
            SubjectId,
            ObjectId,
            PredId,
            CreatorId,
            Created
        ],
        Context);
insert_edge_1(SubjectId, ObjectId, PredId, SeqOpt, CreatorId, Created, Context) ->
    z_db:equery("
        insert into edge
            (subject_id, object_id, predicate_id, seq, creator_id, created)
        values
            ($1, $2, $3, $4, $5, $6)
        on conflict do nothing
        returning id
        ",
        [
            SubjectId,
            ObjectId,
            PredId,
            SeqOpt,
            CreatorId,
            Created
        ],
        Context).

%% @doc Determine the sequence number for the new edge. Needed for imports with
%% specific sequence order, or if the "is_insert_before" option is used or set for
%% the predicate.
maybe_seq_opt(Opts, SubjectId, PredId, Context) ->
    case proplists:get_value(seq, Opts) of
        S when is_integer(S) ->
            S;
        _ ->
            case z_convert:to_bool( proplists:get_value(is_insert_before, Opts) )
                orelse z_convert:to_bool( m_rsc:p_no_acl(PredId, <<"is_insert_before">>, Context) )
            of
                true ->
                    case z_db:q1("
                        select min(seq)
                        from edge
                        where subject_id = $1
                          and predicate_id = $2",
                        [SubjectId, PredId],
                        Context)
                    of
                        undefined -> undefined;
                        N -> N-1
                    end;
                false ->
                    undefined
            end
    end.

%% @doc Delete an edge by Id. If the edge doesn't exist then 'ok' is returned.
-spec delete(EdgeId, Context) -> ok | {error, Reason} when
    EdgeId :: integer(),
    Context :: z:context(),
    Reason :: eacces.
delete(Id, Context) ->
    {SubjectId, PredName, ObjectId} = get_triple(Id, Context),
    case z_acl:is_allowed(
        delete,
        #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
        Context
    ) of
        true ->
            F = fun(Ctx) ->
                z_db:delete(edge, Id, Ctx)
            end,

            z_db:transaction(F, Context),
            z_edge_log_server:check(Context),
            ok;
        false ->
            {error, eacces}
    end.

%% @doc Delete an edge by subject, object and predicate id. Returns ok
%% if the edge is deleted or didn't exist.
-spec delete(Subject, Predicate, Object, Context) -> ok | {error, Reason} when
    Subject :: m_rsc:resource(),
    Predicate :: m_rsc:resource(),
    Object :: m_rsc:resource(),
    Context :: z:context(),
    Reason :: eacces | object | subject | predicate.
delete(Subject, Predicate, Object, Context) ->
    delete(Subject, Predicate, Object, [], Context).

%% @doc Delete an edge by subject, object and predicate id. Options are ignored. Returns ok
%% if the edge is deleted or didn't exist.
-spec delete(Subject, Predicate, Object, Options, Context) -> ok | {error, Reason} when
    Subject :: m_rsc:resource(),
    Predicate :: m_rsc:resource(),
    Object :: m_rsc:resource(),
    Options :: list(),
    Context :: z:context(),
    Reason :: eacces | object | subject | predicate.
delete(SubjectId, Pred, ObjectId, _Options, Context) when is_integer(SubjectId), is_integer(ObjectId) ->
    case to_predicate(Pred, Context) of
        {ok, PredId} ->
            {ok, PredName} = m_predicate:id_to_name(PredId, Context),
            case z_acl:is_allowed(
                delete,
                #acl_edge{ subject_id = SubjectId, predicate = PredName, object_id = ObjectId },
                Context
            ) of
                true ->
                    _ = z_db:q("
                        delete from edge
                        where subject_id = $1
                          and object_id = $2
                          and predicate_id = $3
                        ",
                        [SubjectId, ObjectId, PredId],
                        Context),
                    z_edge_log_server:check(Context),
                    ok;
                false ->
                    {error, eacces}
            end;
        {error, _} = Error ->
            Error
    end;
delete(SubjectId, Pred, Object, Options, Context) when not is_integer(Object) ->
    case m_rsc:rid(Object, Context) of
        undefined -> {error, object};
        Id -> delete(SubjectId, Pred, Id, Options, Context)
    end;
delete(Subject, Pred, Object, Options, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined -> {error, subject};
        Id -> delete(Id, Pred, Object, Options, Context)
    end.

to_predicate(Id, Context) ->
    case m_rsc:rid(Id, Context) of
        undefined ->
            {error, predicate};
        RId ->
            case m_rsc:is_a(RId, predicate, Context) of
                true ->
                    {ok, RId};
                false ->
                    {error, predicate}
            end
    end.



%% @doc Delete multiple edges between the subject and the object. Invalid predicates are
%% ignored.
-spec delete_multiple(Subject, Predicates, Object, Context) -> ok | {error, Reason} when
    Subject :: m_rsc:resource(),
    Predicates :: [ m_rsc:resource() ],
    Object :: m_rsc:resource(),
    Context :: z:context(),
    Reason :: eacces | subject | object.
delete_multiple(SubjectId, Predicates, ObjectId, Context) when is_integer(SubjectId), is_integer(ObjectId) ->
    PredIds = lists:map(
        fun(Predicate) ->
            {ok, Id} = m_predicate:name_to_id(Predicate, Context),
            Id
        end,
        Predicates),
    PredNames = [ m_predicate:id_to_name(PredId, Context) || PredId <- PredIds ],
    IsAllowed = lists:all(
        fun
            ({ok, PredName}) ->
                z_acl:is_allowed(
                    delete,
                    #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
                    Context);
            ({error, _}) ->
                true
        end,
        PredNames),
    case IsAllowed of
        true ->
            Count = z_db:q("delete
                    from edge
                    where subject_id = $1
                      and object_id = $2
                      and predicate_id = any($3::int[])",
                [SubjectId, ObjectId, PredIds], Context),

            case Count of
                0 ->
                    ok;
                N when is_integer(N) ->
                    z_edge_log_server:check(Context),
                    ok
            end;
        false ->
            {error, eacces}
    end;
delete_multiple(SubjectId, Pred, Object, Context) when is_integer(SubjectId) ->
    case m_rsc:rid(Object, Context) of
        undefined -> {error, object};
        Id -> delete_multiple(SubjectId, Pred, Id, Context)
    end;
delete_multiple(Subject, Pred, Object, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined -> {error, subject};
        Id -> delete_multiple(Id, Pred, Object, Context)
    end.


%% @doc Replace the objects with the new list
-spec replace(m_rsc:resource(), m_rsc:resource(), [ m_rsc:resource() ], z:context()) -> ok | {error, atom()}.
replace(SubjectId, PredId, NewObjects, Context) when is_integer(PredId), is_integer(SubjectId) ->
    case m_predicate:is_predicate(PredId, Context) of
        true -> replace1(SubjectId, PredId, NewObjects, Context);
        false -> {error, {unknown_predicate, PredId}}
    end;
replace(SubjectId, Predicate, NewObjects, Context) when not is_integer(Predicate) ->
    {ok, PredId} = m_predicate:name_to_id(Predicate, Context),
    replace1(SubjectId, PredId, NewObjects, Context);
replace(Subject, Predicate, NewObjects, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined -> {error, subject};
        Id -> replace(Id, Predicate, NewObjects, Context)
    end.

replace1(SubjectId, PredId, NewObjects0, Context) ->
    NewObjects = lists:sort(lists:filtermap(
        fun(Obj) ->
            case m_rsc:rid(Obj, Context) of
                undefined -> false;
                OId -> {true, OId}
            end
        end,
        NewObjects0)),
    {ok, PredName} = m_predicate:id_to_name(PredId, Context),
    case lists:sort(objects(SubjectId, PredId, Context)) of
        NewObjects ->
            ok;

        CurrObjects ->
            % Check the ACL for insertion and deletion
            IsAllowed1 = lists:all(
                fun(ObjectId) ->
                    z_acl:is_allowed(
                        delete,
                        #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
                        Context)
                end,
                CurrObjects -- NewObjects),
            IsAllowed2 = lists:all(
                fun(ObjectId) ->
                    z_acl:is_allowed(insert,
                        #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
                        Context)
                end,
                NewObjects -- CurrObjects),

            case IsAllowed1 andalso IsAllowed2 of
                true ->
                    Result = set_sequence(SubjectId, PredId, NewObjects, Context),
                    z_edge_log_server:check(Context),
                    Result;
                false ->
                    {error, eacces}
            end
    end.

%% @doc Duplicate all edges from one subject to another subject. Skip all edges that give
%% ACL errors. Return eacces if the source or target subject is not editable.
-spec duplicate(FromId, ToId, Context) -> ok | {error, Reason} when
    FromId :: m_rsc:resource(),
    ToId :: m_rsc:resource(),
    Context :: z:context(),
    Reason :: eacces.
duplicate(FromId, ToId, Context) ->
    case z_acl:rsc_editable(FromId, Context) andalso z_acl:rsc_editable(ToId, Context) of
        true ->
            F = fun(Ctx) ->
                FromEdges = z_db:q("
                                select predicate_id, object_id, seq
                                from edge
                                where subject_id = $1
                                order by seq, id",
                    [m_rsc:rid(FromId, Context)],
                    Ctx),
                ToEdges = z_db:q(
                    "select predicate_id, object_id from edge where subject_id = $1",
                    [m_rsc:rid(ToId, Context)],
                    Ctx
                ),
                FromEdges1 = lists:filter(
                    fun({PredId, ObjectId, _Seq}) ->
                        Pair = {PredId, ObjectId},
                        not lists:member(Pair, ToEdges)
                    end,
                    FromEdges),
                FromEdges2 = lists:filter(
                    fun({PredId, ObjectId, _Seq}) ->
                        {ok, PredName} = m_predicate:id_to_name(PredId, Context),
                        z_acl:is_allowed(
                            insert,
                            #acl_edge{subject_id = ToId, predicate = PredName, object_id = ObjectId},
                            Context)
                    end,
                    FromEdges1),
                UserId = z_acl:user(Ctx),
                lists:foreach(
                    fun({PredId, ObjectId, Seq}) ->
                        z_db:insert(
                            edge,
                            [{subject_id, m_rsc:rid(ToId, Context)},
                                {predicate_id, PredId},
                                {object_id, ObjectId},
                                {seq, Seq},
                                {creator_id, UserId}],
                            Ctx)
                    end,
                    FromEdges2)
            end,
            z_db:transaction(F, Context),
            z_edge_log_server:check(Context),
            ok;
        false ->
            {error, eacces}
    end.

%% @doc Move all edges from one id to another id, part of m_rsc:merge_delete/3
merge(WinnerId, LoserId, Context) ->
    case {z_acl:rsc_editable(WinnerId, Context), z_acl:rsc_deletable(LoserId, Context)} of
        {true, true} ->
            F = fun(Ctx) ->
                %% Edges outgoing from the looser
                LoserOutEdges = z_db:q("select predicate_id, object_id, id
                                         from edge
                                         where subject_id = $1",
                    [LoserId],
                    Ctx),
                WinnerOutEdges = z_db:q("select predicate_id, object_id
                                         from edge
                                         where subject_id = $1",
                    [WinnerId],
                    Ctx),
                LoserOutEdges1 = lists:filter(
                                fun({PredId, ObjectId, _EdgeId}) ->
                                    not lists:member({PredId, ObjectId}, WinnerOutEdges)
                                end,
                                LoserOutEdges),
                % TODO: discuss if we should enact these extra ACL checks
                % LoserOutEdges2 = lists:filter(
                %                 fun({PredId, ObjectId, _EdgeId}) ->
                %                     {ok, PredName} = m_predicate:id_to_name(PredId, Context),
                %                     z_acl:is_allowed(
                %                         insert,
                %                         #acl_edge{subject_id=WinnerId, predicate=PredName, object_id=ObjectId},
                %                         Context)
                %                 end,
                %                 LoserOutEdges1),
                lists:foreach(
                        fun({_PredId, _ObjId, EdgeId}) ->
                            z_db:equery("
                                insert into edge
                                    (subject_id, predicate_id, object_id, created, creator_id)
                                select $1, e.predicate_id, e.object_id, e.created, e.creator_id
                                from edge e
                                where e.id = $2",
                                [ WinnerId, EdgeId ],
                                Context)
                        end,
                        LoserOutEdges1),

                %% Edges incoming to the looser
                LoserInEdges = z_db:q("select predicate_id, subject_id, id
                                        from edge
                                        where object_id = $1",
                                       [LoserId],
                                       Ctx),
                WinnerInEdges = z_db:q("select predicate_id, subject_id
                                        from edge
                                        where object_id = $1",
                                       [WinnerId],
                                       Ctx),
                LoserInEdges1 = lists:filter(
                                    fun({PredId, SubjectId, _EdgeId}) ->
                                        not lists:member({PredId, SubjectId}, WinnerInEdges)
                                    end,
                                    LoserInEdges),
                lists:foreach(
                        fun({_PredId, _SubjId, EdgeId}) ->
                            z_db:equery("
                                insert into edge
                                    (subject_id, predicate_id, object_id, created, creator_id)
                                select e.subject_id, e.predicate_id, $1, e.created, e.creator_id
                                from edge e
                                where e.id = $2",
                                [ WinnerId, EdgeId ],
                                Context)
                        end,
                        LoserInEdges1),

                z_db:q("update edge set creator_id = $1 where creator_id = $2",
                       [WinnerId, LoserId],
                       Context)
            end,
            z_db:transaction(F, Context),
            z_edge_log_server:check(Context),
            ok;
        {false, _} ->
            {error, {eacces, WinnerId}};
        {_, false} ->
            {error, {eacces, WinnerId}}
    end.


%% @doc Update the nth edge of a subject.  Set a new object, keep the predicate.
%% If there are not enough edges then an error is returned. The first edge is nr 1.
-spec update_nth(SubjectId, Predicate, Nth, ObjectId, Context) -> {ok, EdgeId} | {error, Reason} when
    SubjectId :: m_rsc:resource_id(),
    Predicate :: m_rsc:resource(),
    Nth :: integer(),
    ObjectId :: m_rsc:resource_id(),
    EdgeId :: pos_integer(),
    Context :: z:context(),
    Reason :: eacces | enoent | term().
update_nth(SubjectId, Predicate, Nth, ObjectId, Context) when is_integer(SubjectId), is_integer(ObjectId) ->
    {ok, PredId} = m_predicate:name_to_id(Predicate, Context),
    {ok, PredName} = m_predicate:id_to_name(PredId, Context),
    F = fun(Ctx) ->
        case z_db:q(
            "select id, object_id, seq from edge "
            "where subject_id = $1 and predicate_id = $2 "
            "order by seq,id limit 1 offset $3",
            [SubjectId, PredId, Nth - 1],
            Ctx
        ) of
            [] ->
                {error, enoent};
            [ {EdgeId, ObjectId, _SeqNr}] ->
                {ok, EdgeId};
            [ {EdgeId, OldObjectId, SeqNr} ] ->
                case z_acl:is_allowed(delete, #acl_edge{subject_id=SubjectId, predicate=PredName, object_id=OldObjectId}, Ctx) of
                    true ->
                        1 = z_db:q("delete from edge where id = $1", [EdgeId], Ctx),
                        z_db:insert(
                            edge,
                            [
                                {subject_id, SubjectId},
                                {predicate_id, PredId},
                                {object_id, ObjectId},
                                {seq, SeqNr}
                            ],
                            Ctx);
                    false ->
                        {error, eacces}
                end
        end
    end,
    case z_acl:is_allowed(
        insert,
        #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
        Context
    ) of
        true ->
            case z_db:transaction(F, Context) of
                {ok, EdgeId} ->
                    z_edge_log_server:check(Context),
                    {ok, EdgeId};
                {error, Reason} ->
                    {error, Reason}
            end;
        false ->
            {error, eacces}
    end;
update_nth(Subject, Predicate, Nth, ObjectId, Context) when not is_integer(Subject) ->
    case m_rsc:rid(Subject, Context) of
        undefined -> {error, subject};
        Id -> update_nth(Id, Predicate, Nth, ObjectId, Context)
    end;
update_nth(SubjectId, Predicate, Nth, Object, Context) ->
    case m_rsc:rid(Object, Context) of
        undefined -> {error, object};
        Id -> update_nth(SubjectId, Predicate, Nth, Id, Context)
    end.


%% @doc Return the Nth object with a certain predicate of a subject.
object(Id, Pred, N, Context) ->
    Ids = objects(Id, Pred, Context),
    try
        lists:nth(N, Ids)
    catch
        _:_ -> undefined
    end.

%% @doc Return the Nth subject with a certain predicate of an object.
subject(Id, Pred, N, Context) ->
    Ids = subjects(Id, Pred, Context),
    try
        lists:nth(N, Ids)
    catch
        _:_ -> undefined
    end.

%% @doc Return all object ids of an id with a certain predicate. The order of the ids is deterministic.
-spec objects(SubjectId::m_rsc:resource(), Predicate::m_rsc:resource(), z:context()) -> list( m_rsc:resource_id() ).
objects(_Id, undefined, _Context) ->
    [];
objects(Id, Pred, Context) when is_integer(Pred) ->
    case m_rsc:rid(Id, Context) of
        undefined ->
            [];
        SubjectId ->
            case z_depcache:get({objects, Pred, SubjectId}, Context) of
                {ok, Objects} ->
                    Objects;
                undefined ->
                    Ids = z_db:q(
                        "select object_id from edge "
                        "where subject_id = $1 and predicate_id = $2 "
                        "order by seq,id",
                        [SubjectId, Pred],
                        Context
                    ),
                    Objects = [ObjId || {ObjId} <- Ids],
                    z_depcache:set({objects, Pred, SubjectId}, Objects, ?DAY, [SubjectId], Context),
                    Objects
            end
    end;
objects(Id, Pred, Context) ->
    case m_predicate:name_to_id(Pred, Context) of
        {error, _} -> [];
        {ok, PredId} -> objects(Id, PredId, Context)
    end.


%% @doc Return all subject ids of an object id with a certain predicate.
%% The order of the ids is deterministic.
-spec subjects(ObjectId::m_rsc:resource(), Predicate::m_rsc:resource(), z:context()) -> list( m_rsc:resource_id() ).
subjects(_Id, undefined, _Context) ->
    [];
subjects(Id, Pred, Context) when is_integer(Pred) ->
    case m_rsc:rid(Id, Context) of
        undefined ->
            [];
        ObjectId ->
            case z_depcache:get({subjects, Pred, ObjectId}, Context) of
                {ok, Objects} ->
                    Objects;
                undefined ->
                    Ids = z_db:q(
                        "select subject_id from edge "
                        "where object_id = $1 and predicate_id = $2 "
                        "order by id",
                        [ObjectId, Pred],
                        Context
                    ),
                    Subjects = [SubjId || {SubjId} <- Ids],
                    z_depcache:set({subjects, Pred, ObjectId}, Subjects, ?HOUR, [ObjectId], Context),
                    Subjects
            end
    end;
subjects(Id, Pred, Context) ->
    case m_predicate:name_to_id(Pred, Context) of
        {error, _} -> [];
        {ok, PredId} -> subjects(Id, PredId, Context)
    end.


%% @doc Return all object ids of the resource
-spec objects( m_rsc:resource(), z:context() ) -> [ m_rsc:resource_id() ].
objects(Subject, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            [];
        SubjectId ->
            F = fun() ->
                Ids = z_db:q(
                    "select object_id from edge "
                    "where subject_id = $1 "
                    "order by predicate_id, seq, id",
                    [SubjectId],
                    Context
                ),
                [ObjId || {ObjId} <- Ids]
            end,
            z_depcache:memo(F, {objects, SubjectId}, ?DAY, [SubjectId], Context)
    end.

%% @doc Return all subject ids of the resource
-spec subjects( m_rsc:resource(), z:context() ) -> [ m_rsc:resource_id() ].
subjects(Object, Context) ->
    case m_rsc:rid(Object, Context) of
        undefined ->
            [];
        ObjectId ->
            F = fun() ->
                Ids = z_db:q(
                    "select subject_id from edge "
                    "where object_id = $1 order by predicate_id, id",
                    [ObjectId],
                    Context
                ),
                [SubjId || {SubjId} <- Ids]
            end,
            z_depcache:memo(F, {subjects, ObjectId}, ?HOUR, [ObjectId], Context)
    end.

%% @doc Check if a resource has object edges, no caching is done.
-spec has_objects(m_rsc:resource(), z:context()) -> boolean().
has_objects(Subject, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            false;
        SubjectId ->
            is_integer(
                z_db:q1("
                    select id from edge
                    where subject_id = $1
                    limit 1",
                    [SubjectId],
                    Context))
    end.

%% @doc Check if a resource has subject edges, no caching is done.
-spec has_subjects(m_rsc:resource(), z:context()) -> boolean().
has_subjects(Object, Context) ->
    case m_rsc:rid(Object, Context) of
        undefined ->
            false;
        ObjectId ->
            is_integer(
                z_db:q1("
                    select id from edge
                    where object_id = $1
                    limit 1",
                    [ObjectId],
                    Context))
    end.

%% @doc Return all object ids with the edge id for a predicate/subject_id
-spec object_edge_ids( m_rsc:resource(), m_rsc:resource(), z:context() ) -> [ {m_rsc:resource_id(), integer()} ].
object_edge_ids(Subject, Predicate, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            [];
        SubjectId ->
            case m_predicate:name_to_id(Predicate, Context) of
                {ok, PredId} ->
                    F = fun() ->
                        z_db:q(
                            "select object_id, id from edge "
                            "where subject_id = $1 and predicate_id = $2 "
                            "order by seq, id",
                            [SubjectId, PredId],
                            Context
                        )
                    end,
                    z_depcache:memo(F, {object_edge_ids, SubjectId, PredId}, ?DAY, [SubjectId], Context);
                {error, _} ->
                    []
            end
    end.


%% @doc Return all subject ids with the edge id for a predicate/object_id
-spec subject_edge_ids( m_rsc:resource(), m_rsc:resource(), z:context() ) -> [ {m_rsc:resource_id(), integer()} ].
subject_edge_ids(Object, Predicate, Context) ->
    case m_rsc:rid(Object, Context) of
        undefined ->
            [];
        ObjectId ->
            case m_predicate:name_to_id(Predicate, Context) of
                {ok, PredId} ->
                    F = fun() ->
                        z_db:q(
                            "select subject_id, id from edge "
                            "where object_id = $1 and predicate_id = $2 "
                            "order by seq, id",
                            [ObjectId, PredId],
                            Context
                        )
                    end,
                    z_depcache:memo(F, {subject_edge_ids, ObjectId, PredId}, ?DAY, [ObjectId], Context);
                {error, _} ->
                    []
            end
    end.


%% @doc Return all object ids with edge properties
-spec object_edge_props(m_rsc:resource(), m_rsc:resource(), z:context()) -> list( m_rsc:resource_id() ).
object_edge_props(Subject, Predicate, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            [];
        SubjectId ->
            case m_predicate:name_to_id(Predicate, Context) of
                {ok, PredId} ->
                    F = fun() ->
                        z_db:assoc("
                                select *
                                from edge
                                where subject_id = $1
                                  and predicate_id = $2
                                order by seq, id",
                            [SubjectId, PredId],
                            Context)
                    end,
                    z_depcache:memo(F, {object_edge_props, SubjectId, PredId}, ?DAY, [SubjectId], Context);
                {error, _} ->
                    []
            end
    end.

%% @doc Return all subject ids with the edge properties
-spec subject_edge_props(m_rsc:resource(), m_rsc:resource(), z:context()) -> list( m_rsc:resource_id() ).
subject_edge_props(Object, Predicate, Context) ->
    case m_rsc:rid(Object, Context) of
        undefined ->
            [];
        ObjectId ->
            case m_predicate:name_to_id(Predicate, Context) of
                {ok, PredId} ->
                    F = fun() ->
                        z_db:assoc("
                                select *
                                from edge
                                where object_id = $1
                                  and predicate_id = $2
                                order by seq, id",
                            [ObjectId, PredId],
                            Context)
                    end,
                    z_depcache:memo(F, {subject_edge_props, ObjectId, PredId}, ?DAY, [ObjectId], Context);
                {error, _} ->
                    []
            end
    end.


%% @doc Reorder the edges so that the mentioned ids are in front, in the listed order.
-spec update_sequence( m_rsc:resource(), m_rsc:resource(), [ m_rsc:resource_id() ], z:context() ) ->
    ok | {error, term()}.
update_sequence(Subject, Pred, ObjectIds, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            {error, enoent};
        SubjectId ->
            {ok, PredId} = m_predicate:name_to_id(Pred, Context),
            case z_acl:rsc_editable(SubjectId, Context) of
                true ->
                    F = fun(Ctx) ->
                        All = z_db:q("
                                    select object_id, id
                                    from edge
                                    where predicate_id = $1
                                      and subject_id = $2", [PredId, SubjectId], Ctx),

                        MissingIds = lists:foldl(
                            fun({OId, _}, Acc) ->
                                case lists:member(OId, ObjectIds) of
                                    true -> Acc;
                                    false -> [OId | Acc]
                                end
                            end,
                            [],
                            All),

                        lists:map(
                            fun(OId) ->
                                case lists:keymember(OId, 1, All) of
                                    true ->
                                        ok;
                                    false ->
                                        z_db:q("
                                            insert into edge (subject_id, predicate_id, object_id)
                                            values ($1, $2, $3)",
                                            [ SubjectId, PredId, OId ],
                                            Context)
                                end
                            end,
                            ObjectIds),

                        SortedIds = ObjectIds ++ lists:reverse(MissingIds),
                        SortedEdgeIds = [proplists:get_value(OId, All, -1) || OId <- SortedIds],
                        z_db:update_sequence(edge, SortedEdgeIds, Ctx),
                        ok
                    end,

                    Result = z_db:transaction(F, Context),
                    z_depcache:flush(SubjectId, Context),
                    z_depcache:flush({predicate, PredId}, Context),
                    z_edge_log_server:check(Context),
                    Result;
                false ->
                    {error, eacces}
            end
    end.

%% @doc Set edges order so that the specified object ids are in given order.
%% Any extra edges not specified will be deleted, and any missing edges will be inserted.
-spec set_sequence( m_rsc:resource(), m_rsc:resource(), [ m_rsc:resource_id() ], z:context()) -> ok | {error, term()}.
set_sequence(Subject, Pred, ObjectIds, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            {error, enoent};
        SubjectId ->
            case z_acl:rsc_editable(SubjectId, Context) of
                true ->
                    case m_predicate:name_to_id(Pred, Context) of
                        {ok, PredId} ->
                            F = fun(Ctx) ->
                                All = z_db:q("
                                            select object_id, id
                                            from edge
                                            where predicate_id = $1
                                              and subject_id = $2", [PredId, SubjectId], Ctx),

                                % Delete edges not in ObjectIds
                                lists:foreach(
                                    fun({ObjectId, EdgeId}) ->
                                        case lists:member(ObjectId, ObjectIds) of
                                            true ->
                                                ok;
                                            false ->
                                                delete(EdgeId, Context)
                                        end
                                    end,
                                    All),

                                % Add new edges not yet in the db
                                NewEdges = lists:filtermap(
                                    fun(ObjectId) ->
                                        case lists:member(ObjectId, All) of
                                            true -> false;
                                            false ->
                                                case insert(SubjectId, Pred, ObjectId, Context) of
                                                    {ok, EdgeId} ->
                                                        {true, {ObjectId, EdgeId}};
                                                    {error, _} ->
                                                        false
                                                end
                                        end
                                    end,
                                    ObjectIds),

                                % Force order of all edges
                                AllEdges = All ++ NewEdges,
                                SortedEdgeIds = [
                                    proplists:get_value(OId, AllEdges, -1) || OId <- ObjectIds
                                ],
                                z_db:update_sequence(edge, SortedEdgeIds, Ctx),
                                ok
                            end,

                            Result = z_db:transaction(F, Context),
                            z_depcache:flush(SubjectId, Context),
                            z_depcache:flush({predicate, PredId}, Context),
                            z_edge_log_server:check(Context),
                            Result;
                        {error, _} = Error ->
                            Error
                    end;
                false ->
                    {error, eacces}
            end
    end.


%% @doc Update the sequence for the given edge ids.  Optionally rename the predicate on the edge.
-spec update_sequence_edge_ids( m_rsc:resource_id(), m_rsc:resource(), [ integer() ], z:context() ) -> ok | {error, term()}.
update_sequence_edge_ids(Subject, Pred, EdgeIds, Context) ->
    case m_rsc:rid(Subject, Context) of
        undefined ->
            {error, enoent};
        Id ->
            case z_acl:rsc_editable(Id, Context) of
                true ->
                    {ok, PredId} = m_predicate:name_to_id(Pred, Context),
                    F = fun(Ctx) ->
                        % Figure out which edge ids need to be renamed to this predicate.
                        Current = z_db:q("
                                    select id
                                    from edge
                                    where predicate_id = $1
                                      and subject_id = $2", [PredId, Id], Ctx),
                        CurrentIds = [EdgeId || {EdgeId} <- Current],

                        WrongPred = lists:foldl(
                            fun(EdgeId, Acc) ->
                                case lists:member(EdgeId, CurrentIds) of
                                    true -> Acc;
                                    false -> [EdgeId | Acc]
                                end
                            end,
                            [],
                            EdgeIds),

                        %% Remove the edges where we don't have permission to remove the
                        %% old predicate and insert the new predicate.
                        {ok, Pred} = m_predicate:id_to_name(PredId, Ctx),
                        WrongPredAllowed = lists:filter(
                            fun(EdgeId) ->
                                {Id, EdgePredName, EdgeObjectId} = get_triple(EdgeId, Ctx),
                                case z_acl:is_allowed(delete,
                                    #acl_edge{subject_id = Id, predicate = EdgePredName, object_id = EdgeObjectId},
                                    Ctx
                                ) of
                                    true ->
                                        case z_acl:is_allowed(insert,
                                            #acl_edge{subject_id = Id, predicate = Pred, object_id = EdgeObjectId},
                                            Ctx
                                        ) of
                                            true -> true;
                                            _ -> false
                                        end;
                                    _ ->
                                        false
                                end
                            end, WrongPred),

                        % Update the predicates on the edges that don't have the correct predicate.
                        % We have to make sure that the "wrong" edges do have the correct subject_id
                        Extra = lists:foldl(
                            fun(EdgeId, Acc) ->
                                case z_db:q(
                                    "update edge set predicate_id = $1 "
                                    "where id = $2 and subject_id = $3",
                                    [PredId, EdgeId, Id],
                                    Ctx
                                ) of
                                    1 -> [EdgeId | Acc];
                                    0 -> Acc
                                end
                            end,
                            [],
                            WrongPredAllowed),
                        All = CurrentIds ++ Extra,

                        %% Extract all edge ids that are not in our sort list, they go to the end of the new sequence
                        AppendToEnd = lists:foldl(
                            fun(EdgeId, Acc) ->
                                case lists:member(EdgeId, EdgeIds) of
                                    true -> Acc;
                                    false -> [EdgeId | Acc]
                                end
                            end,
                            [],
                            All),
                        SortedEdgeIds = EdgeIds ++ lists:reverse(AppendToEnd),
                        z_db:update_sequence(edge, SortedEdgeIds, Ctx),
                        ok
                    end,

                    Result = z_db:transaction(F, Context),
                    z_depcache:flush(Id, Context),
                    z_depcache:flush({predicate, PredId}, Context),
                    z_edge_log_server:check(Context),
                    Result;
                false ->
                    {error, eacces}
            end
    end.


%% @doc Return the list of predicates in use by edges to objects from the id
-spec object_predicates( m_rsc:resource_id(), z:context() ) -> list( atom() ).
object_predicates(Id, Context) when is_integer(Id) ->
    F = fun() ->
        Ps = z_db:q(
            "select distinct p.name from edge e join rsc p on e.predicate_id = p.id where e.subject_id = $1 "
            "order by name", [Id], Context),
        [list_to_atom(binary_to_list(P)) || {P} <- Ps]
    end,
    z_depcache:memo(F, {object_preds, Id}, ?DAY, [Id], Context).

%% @doc Return the list of predicates is use by edges from subjects to the id
-spec subject_predicates( m_rsc:resource_id(), z:context() ) -> list( atom() ).
subject_predicates(Id, Context) when is_integer(Id) ->
    F = fun() ->
        Ps = z_db:q(
            "select distinct p.name from edge e join rsc p on e.predicate_id = p.id "
            "where e.object_id = $1 order by name",
            [Id],
            Context
        ),
        [list_to_atom(binary_to_list(P)) || {P} <- Ps]
    end,
    z_depcache:memo(F, {subject_preds, Id}, ?DAY, [Id], Context).

%% @doc Return the list of predicate ids in use by edges to objects from the id
-spec object_predicate_ids( m_rsc:resource_id(), z:context() ) -> list( m_rsc:resource_id() ).
object_predicate_ids(Id, Context) when is_integer(Id) ->
    Ps = z_db:q("select distinct predicate_id from edge where subject_id = $1", [Id], Context),
    [P || {P} <- Ps].

%% @doc Return the list of predicates is use by edges from subjects to the id
-spec subject_predicate_ids( m_rsc:resource_id(), z:context() ) -> list( m_rsc:resource_id() ).
subject_predicate_ids(Id, Context) when is_integer(Id) ->
    Ps = z_db:q("select distinct predicate_id from edge where object_id = $1", [Id], Context),
    [P || {P} <- Ps].

%%
%% Helpers
%%

do_post_insert(Subject, Predicate, Object, Context) ->
    case insert(Subject, Predicate, Object, Context) of
        {ok, Id} ->
            {ok, #{ id => Id }};
        {error, _}=Error ->
            Error
    end.

get_spo_o(Path, Payload, Context) ->
    Subject = get_from_path_or_payload(1, <<"subject">>, Path, Payload, Context),
    Predicate = get_from_path_or_payload(2, <<"predicate">>, Path, Payload, Context),
    Object = get_from_path_or_payload(3, <<"object">>, Path, Payload, Context),
    {Subject, Predicate, Object}.

get_spo_s(Path, Payload, Context) ->
    Object = get_from_path_or_payload(1, <<"object">>, Path, Payload, Context),
    Predicate = get_from_path_or_payload(2, <<"predicate">>, Path, Payload, Context),
    Subject = get_from_path_or_payload(3, <<"subject">>, Path, Payload, Context),
    {Subject, Predicate, Object}.

get_from_path_or_payload(1, _What, [Thing | _Rest ], _Payload, _Context) when Thing =/= <<"?">> -> Thing;
get_from_path_or_payload(2, _What, [_, Thing | _Rest ], _Payload, _Context) when Thing =/= <<"?">> -> Thing;
get_from_path_or_payload(3, _What, [_, _, Thing | _Rest ], _Payload, _Context) when Thing =/= <<"?">> -> Thing;
get_from_path_or_payload(_, What, _Path, Payload, Context) ->
    get_q(What, Payload, Context).

get_q(Name, Payload, Context) ->
    case maps:get(Name, Payload, undefined) of
        undefined ->
            case maps:get(<<"data-edge-", Name/binary>>, maps:get(<<"message">>, Payload, #{}), undefined) of
                undefined ->
                    z_context:get_q(Name, Context);
                Value ->
                    Value
            end;
        Value ->
            Value
    end.