src/models/m_edge.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2019 Marc Worrell
%% Date: 2009-04-09
%%
%% Copyright 2009-2019 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).
-author("Marc Worrell <marc@worrell.nl").

-behaviour(zotonic_model).

%% interface functions
-export([
    m_get/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,
    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
]).


%% @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([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}.


%% @doc Get the complete edge with the id
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(pos_integer(), #context{}) -> {m_rsc:resource_id(), atom(), m_rsc:resource_id()}.
get_triple(Id, Context) ->
    {SubjectId, Predicate, ObjectId} = 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),
    {SubjectId, z_convert:to_atom(Predicate), ObjectId}.

%% @doc Get the edge id of a subject/pred/object combination
-spec get_id(m_rsc:resource(), m_rsc:resource(), m_rsc:resource(), #context{}) -> pos_integer() | undefined.
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
get_edges(SubjectId, Context) ->
    case m_rsc:rid(SubjectId, Context) of
        undefined ->
            [];
        SubjectId1 ->
            case z_depcache:get({edges, SubjectId1}, 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", [SubjectId1], Context),
                    Edges1 = z_utils:group_proplists(name, Edges),
                    Edges2 = [ {z_convert:to_atom(Pred), Es} || {Pred, Es} <- Edges1 ],
                    z_depcache:set({edges, SubjectId1}, Edges2, ?DAY, [SubjectId1], Context),
                    Edges2
            end
    end.

%% @doc Insert a new edge
-spec insert(m_rsc:resource(), m_rsc:resource(), m_rsc:resource(), z:context()) ->
    {ok, EdgeId :: pos_integer()} | {error, term()}.
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 ->
            F = fun(Ctx) ->
                SeqOpt = maybe_seq_opt(Opts, SubjectId, PredId, Ctx),
                CreatedOpt = case proplists:get_value(created, Opts) of
                                 DT when is_tuple(DT) -> [{created, DT}];
                                 undefined -> []
                             end,
                EdgeProps = [
                    {subject_id, SubjectId},
                    {object_id, ObjectId},
                    {predicate_id, PredId},
                    {creator_id, case proplists:get_value(creator_id, Opts) of
                                     undefined -> z_acl:user(Ctx);
                                     CreatorId -> CreatorId
                                 end}
                    | (SeqOpt ++ CreatedOpt)
                ],
                z_db:insert(edge, EdgeProps, Ctx)
            end,
            {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 ->
                    {ok, EdgeId} = z_db:transaction(F, Context),
                    z_edge_log_server:check(Context),
                    {ok, EdgeId};
                AclError ->
                    {error, {acl, AclError}}
            end;
        EdgeId ->
            % Edge exists - skip
            {ok, EdgeId}
end.

maybe_seq_opt(Opts, SubjectId, PredId, Context) ->
    case proplists:get_value(seq, Opts) of
        S when is_integer(S) ->
            [ {seq, S} ];
        _ ->
            case z_convert:to_bool( m_rsc:p_no_acl(PredId, is_insert_before, Context) )
                orelse z_convert:to_bool( proplists:get_value(is_insert_before, Opts) )
            of
                true ->
                    case z_db:q1("
                        select min(seq)
                        from edge
                        where subject_id = $1
                          and predicate_id = $2",
                        [SubjectId, PredId],
                        Context)
                    of
                        undefined -> [];
                        N -> [ {seq, N-1} ]
                    end;
                false ->
                    []
            end
    end.

%% @doc Delete an edge by Id
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
-spec delete(m_rsc:resource(), m_rsc:resource(), m_rsc:resource(), z:context()) -> ok | {error, atom()}.
delete(SubjectId, Pred, ObjectId, Context) ->
    delete(SubjectId, Pred, ObjectId, [], Context).

-spec delete(m_rsc:resource(), m_rsc:resource(), m_rsc:resource(), list(), z:context()) -> ok | {error, atom()}.
delete(SubjectId, Pred, ObjectId, _Options, Context) ->
    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 ->
                    F = fun(Ctx) ->
                        z_db:q(
                            "delete from edge where subject_id = $1 and object_id = $2 and predicate_id = $3",
                            [SubjectId, ObjectId, PredId],
                            Ctx
                        )
                    end,

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

to_predicate(Id, Context) ->
    case m_rsc:rid(Id, Context) of
        undefined ->
            {error, enoent};
        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
delete_multiple(SubjectId, Preds, ObjectId, Context) ->
    PredIds = lists:map(
        fun(Predicate) ->
            {ok, Id} = m_predicate:name_to_id(Predicate, Context),
            Id
        end,
        Preds
    ),
    PredNames = [m_predicate:id_to_name(PredId, Context) || PredId <- PredIds],
    Allowed = [z_acl:is_allowed(
        delete,
        #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
        Context
    ) || {ok, PredName} <- PredNames],
    case is_allowed(Allowed) of
        true ->
            F = fun(Ctx) ->
                z_db:q("delete
                        from edge
                        where subject_id = $1
                          and object_id = $2
                          and predicate_id in (SELECT(unnest($3::int[])))",
                    [SubjectId, ObjectId, PredIds], Ctx)
            end,

            case z_db:transaction(F, Context) of
                0 ->
                    ok;
                N when is_integer(N) ->
                    z_edge_log_server:check(Context),
                    ok;
                Error ->
                    Error
            end;
        AclError ->
            {error, {acl, AclError}}
    end.

is_allowed([]) -> true;
is_allowed([true | Rest]) -> is_allowed(Rest);
is_allowed([Error | _]) -> Error.


%% @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) ->
    case m_predicate:is_predicate(PredId, Context) of
        true -> replace1(SubjectId, PredId, NewObjects, Context);
        false -> {error, {unknown_predicate, PredId}}
    end;
replace(SubjectId, Pred, NewObjects, Context) ->
    {ok, PredId} = m_predicate:name_to_id(Pred, Context),
    replace1(SubjectId, PredId, NewObjects, Context).


replace1(SubjectId, PredId, NewObjects, Context) ->
    {ok, PredName} = m_predicate:id_to_name(PredId, Context),
    case objects(SubjectId, PredId, Context) of
        NewObjects ->
            ok;

        CurrObjects ->
            % Check the ACL
            Allowed1 = [z_acl:is_allowed(delete,
                #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
                Context)
                || ObjectId <- CurrObjects -- NewObjects],
            Allowed2 = [z_acl:is_allowed(insert,
                #acl_edge{subject_id = SubjectId, predicate = PredName, object_id = ObjectId},
                Context)
                || ObjectId <- NewObjects -- CurrObjects],

            case is_allowed(Allowed1) andalso is_allowed(Allowed2) 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 id to another id. Skip all edges that give
%% ACL errors.
-spec duplicate(m_rsc:resource(), m_rsc:resource(), #context{}) ->
    ok | {error, {atom(), m_rsc:resource_id()}}.
duplicate(Id, ToId, Context) ->
    case z_acl:rsc_editable(Id, 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(Id, 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, Id}}
    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( m_rsc:resource_id(), m_rsc:resource(), m_rsc:resource_id(), integer(), z:context() )
    -> {ok, pos_integer()} | {error, eacces|enoent}.
update_nth(SubjectId, Predicate, Nth, ObjectId, Context) ->
    {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.


%% @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 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 ->
            case z_acl:rsc_editable(SubjectId, Context) of
                true ->
                    {ok, PredId} = m_predicate:name_to_id(Pred, Context),
                    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),
                    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),
                            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),
                    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].