-module(constraint). -include("../include/operations.hrl"). -export([ less_than/2, less_than_or_equal/2, greater_than/2, greater_than_or_equal/2, equal/2, not_equal/2, plus/2, times/2, minus/2, divide/2, member/2, member/3, 'not'/2, 'and'/2, 'or'/2, resolve/4, validate_tuple/3, validate_tuple_against_schema/3, build_membership_criteria/2, lt/2, lte/2, gt/2, gte/2, eq/2, neq/2, between/2, example_supertype_exclusive_constraints/0, example_order_item_references_constraints/0, example_manager_constraints/0, example_ticket_status_exclusive_constraints/0, example_weak_entity_dependency_constraints/0, example_department_has_employee_constraints/0, example_symmetry_antisymmetry_constraints/0, run_example_supertype_exclusive/0, run_example_order_item_references/0, run_example_manager_constraints/0, run_example_ticket_status_exclusive/0, run_example_weak_entity_dependency/0, run_example_department_has_employee/0, run_example_symmetry_antisymmetry/0, run_all_examples/0, empty_constraints/0, merge_constraints/2, filter_constraints/2, rename_constraint_attrs/2, infer_constraints_from_pred/2, get_domain_from_db/2 ]). -type relational_constraint() :: {member_of, relation_name() | #relation{} | #domain{}, binding()} | {'not', relational_constraint()} | {'not', relational_constraint(), relation_name()} | {'and', [relational_constraint()]} | {'or', [relational_constraint()]} | {exists, atom(), relation_name(), relational_constraint()} | {forall, atom(), relation_name(), relational_constraint()}. -type relation_name() :: atom(). -type binding() :: #{atom() => term() | {var, atom()} | {const, term()}}. -export_type([relational_constraint/0, binding/0]). -spec less_than(atom(), cardinality()) -> #relation{}. less_than(DomainName, Cardinality) -> #relation{ hash = undefined, name = less_than, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '<', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L < R end }, provenance = undefined, lineage = {base, less_than} }. -spec less_than_or_equal(atom(), cardinality()) -> #relation{}. less_than_or_equal(DomainName, Cardinality) -> #relation{ hash = undefined, name = less_than_or_equal, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '=<', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L =< R end }, provenance = undefined, lineage = {base, less_than_or_equal} }. -spec greater_than(atom(), cardinality()) -> #relation{}. greater_than(DomainName, Cardinality) -> #relation{ hash = undefined, name = greater_than, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '>', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L > R end }, provenance = undefined, lineage = {base, greater_than} }. -spec greater_than_or_equal(atom(), cardinality()) -> #relation{}. greater_than_or_equal(DomainName, Cardinality) -> #relation{ hash = undefined, name = greater_than_or_equal, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '>=', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L >= R end }, provenance = undefined, lineage = {base, greater_than_or_equal} }. -spec equal(atom(), cardinality()) -> #relation{}. equal(DomainName, Cardinality) -> #relation{ hash = undefined, name = equal, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '=:=', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L =:= R end }, provenance = undefined, lineage = {base, equal} }. -spec not_equal(atom(), cardinality()) -> #relation{}. not_equal(DomainName, Cardinality) -> #relation{ hash = undefined, name = not_equal, tree = undefined, schema = #{left => DomainName, right => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {comparison, '/=', DomainName}, membership_criteria = #{ intension => fun(#{left := L, right := R}) -> L /= R end }, provenance = undefined, lineage = {base, not_equal} }. -spec plus(atom(), cardinality()) -> #relation{}. plus(DomainName, Cardinality) -> #relation{ hash = undefined, name = plus, tree = undefined, schema = #{a => DomainName, b => DomainName, sum => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {arithmetic, plus, DomainName}, membership_criteria = #{ intension => fun(#{a := A, b := B, sum := S}) -> is_number(A) andalso is_number(B) andalso is_number(S) andalso (A + B =:= S) end }, provenance = undefined, lineage = {base, plus} }. -spec times(atom(), cardinality()) -> #relation{}. times(DomainName, Cardinality) -> #relation{ hash = undefined, name = times, tree = undefined, schema = #{a => DomainName, b => DomainName, product => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {arithmetic, times, DomainName}, membership_criteria = #{ intension => fun(#{a := A, b := B, product := P}) -> is_number(A) andalso is_number(B) andalso is_number(P) andalso (A * B =:= P) end }, provenance = undefined, lineage = {base, times} }. -spec minus(atom(), cardinality()) -> #relation{}. minus(DomainName, Cardinality) -> #relation{ hash = undefined, name = minus, tree = undefined, schema = #{a => DomainName, b => DomainName, difference => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {arithmetic, minus, DomainName}, membership_criteria = #{ intension => fun(#{a := A, b := B, difference := D}) -> is_number(A) andalso is_number(B) andalso is_number(D) andalso (A - B =:= D) end }, provenance = undefined, lineage = {base, minus} }. -spec divide(atom(), cardinality()) -> #relation{}. divide(DomainName, Cardinality) -> #relation{ hash = undefined, name = divide, tree = undefined, schema = #{a => DomainName, b => DomainName, quotient => DomainName, remainder => DomainName}, constraints = empty_constraints(), cardinality = Cardinality, generator = {arithmetic, divide, DomainName}, membership_criteria = #{ intension => fun(#{a := A, b := B, quotient := Q, remainder := R}) -> is_integer(A) andalso is_integer(B) andalso is_integer(Q) andalso is_integer(R) andalso B =/= 0 andalso (A div B =:= Q) andalso (A rem B =:= R) end }, provenance = undefined, lineage = {base, divide} }. -spec member(map(), #domain{} | #relation{}) -> boolean(). member(Tuple, #relation{membership_criteria = #{intension := Intension}}) -> Intension(Tuple); member(Tuple, #relation{membership_criteria = #{test := TestFun}}) -> TestFun(Tuple); member(Tuple, #relation{hash = RelationHash, tree = Tree}) when Tree =/= undefined -> BindingHashes = binding_to_hashes(Tuple), TupleHashes = operations:hashes_from_tuple(RelationHash), lists:any( fun(TupleHash) -> tuple_hash_matches_binding(TupleHash, BindingHashes) end, TupleHashes); member(_Tuple, #relation{tree = undefined, generator = Generator}) when Generator =/= undefined -> false; member(_Tuple, #relation{}) -> false; member(Tuple, #domain{membership_criteria = #{test := TestFun}}) -> TestFun(Tuple); member(Tuple, #domain{membership_criteria = Criteria}) when is_map(Criteria) -> validate_tuple_against_criteria(Tuple, Criteria). binding_to_hashes(Binding) when is_map(Binding) -> maps:map( fun(_Attr, Value) -> operations:value_hash(Value) end, Binding). tuple_hash_matches_binding(TupleHash, BindingHashes) -> [#tuple{attribute_map = AttributeMap}] = mnesia:dirty_read(tuple, TupleHash), tuple_matches_binding_hashes(BindingHashes, AttributeMap). tuple_matches_binding_hashes(BindingHashes, TupleAttributeMap) when is_map(BindingHashes), is_map(TupleAttributeMap) -> maps:fold( fun(Key, ValueHash, Acc) -> Acc andalso maps:get(Key, TupleAttributeMap, undefined) =:= ValueHash end, true, BindingHashes). -spec member(binding(), #domain{} | #relation{}, map()) -> {true, map()} | false. member(Binding, Relation, Substitution) -> BoundTuple = apply_substitution(Binding, Substitution), case find_variables(BoundTuple) of [] -> case member(BoundTuple, Relation) of true -> {true, Substitution}; false -> false end; _ -> false end. -spec 'not'(#relation{}, #relation{}) -> #relation{}. 'not'(R, Universe) -> RName = get_name(R), UniverseName = get_name(Universe), ResultName = list_to_atom("not_" ++ atom_to_list(RName) ++ "_in_" ++ atom_to_list(UniverseName)), #relation{ hash = undefined, name = ResultName, tree = undefined, schema = get_schema(R), constraints = empty_constraints(), cardinality = complement_cardinality(R, Universe), generator = {complement, R, Universe}, membership_criteria = #{ intension => fun(Tuple) -> member(Tuple, Universe) andalso not member(Tuple, R) end }, provenance = undefined, lineage = {complement, get_lineage(R), get_lineage(Universe)} }. -spec 'and'(#relation{}, #relation{}) -> #relation{}. 'and'(R1, R2) -> Schema1 = get_schema(R1), Schema2 = get_schema(R2), case schemas_compatible(Schema1, Schema2) of false -> error({incompatible_schemas, Schema1, Schema2}); true -> R1Name = get_name(R1), R2Name = get_name(R2), ResultName = list_to_atom(atom_to_list(R1Name) ++ "_and_" ++ atom_to_list(R2Name)), #relation{ hash = undefined, name = ResultName, tree = undefined, schema = merge_schemas(Schema1, Schema2), constraints = empty_constraints(), cardinality = intersection_cardinality(R1, R2), generator = {intersection, R1, R2}, membership_criteria = #{ intension => fun(Tuple) -> member(Tuple, R1) andalso member(Tuple, R2) end }, provenance = undefined, lineage = {intersection, get_lineage(R1), get_lineage(R2)} } end. -spec 'or'(#relation{}, #relation{}) -> #relation{}. 'or'(R1, R2) -> Schema1 = get_schema(R1), Schema2 = get_schema(R2), case schemas_compatible(Schema1, Schema2) of false -> error({incompatible_schemas, Schema1, Schema2}); true -> R1Name = get_name(R1), R2Name = get_name(R2), ResultName = list_to_atom(atom_to_list(R1Name) ++ "_or_" ++ atom_to_list(R2Name)), #relation{ hash = undefined, name = ResultName, tree = undefined, schema = merge_schemas(Schema1, Schema2), constraints = empty_constraints(), cardinality = union_cardinality(R1, R2), generator = {union, R1, R2}, membership_criteria = #{ intension => fun(Tuple) -> member(Tuple, R1) orelse member(Tuple, R2) end }, provenance = undefined, lineage = {union, get_lineage(R1), get_lineage(R2)} } end. -spec resolve(#database_state{}, atom(), #domain{} | #relation{} | relational_constraint(), map()) -> {ok, boolean()} | {error, term()}. resolve(_Database, domain, #domain{} = Domain, Values) -> {ok, member(Values, Domain)}; resolve(_Database, relation, #relation{} = Relation, Values) -> {ok, member(Values, Relation)}; resolve(Database, constraint, Constraint, Binding) -> evaluate_constraint(Database, Constraint, Binding). -spec validate_tuple(#database_state{}, map(), #relation{}) -> ok | {error, term()}. validate_tuple(Database, Tuple, #relation{schema = Schema, constraints = Constraints}) -> case validate_tuple_against_schema(Database, Tuple, Schema) of ok -> validate_tuple_against_constraints(Database, Tuple, Constraints); Error -> Error end. -spec validate_tuple_against_schema(#database_state{}, map(), map()) -> ok | {error, term()}. validate_tuple_against_schema(Database, Tuple, Schema) -> SchemaAttrs = maps:keys(Schema), TupleAttrs = maps:keys(Tuple), Missing = SchemaAttrs -- TupleAttrs, case Missing of [] -> Extra = TupleAttrs -- SchemaAttrs, case Extra of [] -> validate_all_attributes(Database, Tuple, Schema); _ -> {error, {extra_attributes, Extra}} end; _ -> {error, {missing_attributes, Missing}} end. validate_all_attributes(Database, Tuple, Schema) -> maps:fold( fun(AttrName, DomainName, Acc) -> case Acc of {error, _} = Err -> Err; ok -> Value = maps:get(AttrName, Tuple), case get_domain_from_db(Database, DomainName) of {ok, Domain} -> case member(#{value => Value}, Domain) of true -> ok; false -> {error, {not_in_domain, AttrName, Value, DomainName}} end; {error, not_found} -> {error, {unknown_domain, AttrName, DomainName}} end end end, ok, Schema ). validate_tuple_against_constraints(_Database, _Tuple, undefined) -> ok; validate_tuple_against_constraints(Database, Tuple, Constraints) -> Normalized = normalize_constraints(Constraints), NamedConstraints = Normalized#relation_constraints.constraints, case evaluate_named_constraints(Database, NamedConstraints, Tuple) of {true, _Violations} -> ok; {false, Violations} -> {error, {constraint_violations, Violations}} end. evaluate_named_constraints(_Database, [], _Context) -> {true, []}; evaluate_named_constraints(Database, NamedConstraints, Context) -> ViolationsRev = lists:foldl( fun({Name, Formula}, Acc) -> {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Formula, Context), case Result of true -> Acc; false -> [#{constraint_name => Name, formula => Formula, diagnostics => Diagnostics} | Acc] end end, [], NamedConstraints), case ViolationsRev of [] -> {true, []}; _ -> {false, lists:reverse(ViolationsRev)} end. -spec evaluate_constraint_list(#database_state{}, [term()], map()) -> {boolean(), [map()]}. evaluate_constraint_list(_Database, [], _Context) -> {true, []}; evaluate_constraint_list(Database, Constraints, Context) -> {AllTrue, DiagnosticsRev} = lists:foldl( fun(ConstraintTerm, {AccTrue, AccDiags}) -> {Result, Diags} = evaluate_constraint_with_diagnostics(Database, ConstraintTerm, Context), {AccTrue andalso Result, lists:reverse(Diags) ++ AccDiags} end, {true, []}, Constraints ), {AllTrue, lists:reverse(DiagnosticsRev)}. -spec evaluate_constraint_with_diagnostics(#database_state{}, term(), map()) -> {boolean(), [map()]}. evaluate_constraint_with_diagnostics(Database, {member_of, RelName, Binding}, Context) when is_atom(RelName), is_map(Binding) -> BoundBinding = bind_variables(Binding, Context), case get_domain_from_db(Database, RelName) of {ok, Relation} -> case member(BoundBinding, Relation) of true -> {true, []}; false -> {false, [#{kind => member_of_false, relation => RelName, binding => BoundBinding}]} end; {error, not_found} -> {false, [#{kind => relation_not_found, relation => RelName, binding => BoundBinding}]} end; evaluate_constraint_with_diagnostics(_Database, {member_of, #relation{} = Rel, Binding}, Context) -> BoundBinding = bind_variables(Binding, Context), case member(BoundBinding, Rel) of true -> {true, []}; false -> {false, [#{kind => member_of_false, relation => get_name(Rel), binding => BoundBinding}]} end; evaluate_constraint_with_diagnostics(_Database, {member_of, #domain{} = Rel, Binding}, Context) -> BoundBinding = bind_variables(Binding, Context), case member(BoundBinding, Rel) of true -> {true, []}; false -> {false, [#{kind => member_of_false, relation => get_name(Rel), binding => BoundBinding}]} end; evaluate_constraint_with_diagnostics(Database, {'and', Constraints}, Context) -> evaluate_constraint_list(Database, Constraints, Context); evaluate_constraint_with_diagnostics(Database, {'or', Constraints}, Context) -> {AnyTrue, DiagnosticsRev} = lists:foldl( fun(ConstraintTerm, {AccTrue, AccDiags}) -> {Result, Diags} = evaluate_constraint_with_diagnostics(Database, ConstraintTerm, Context), {AccTrue orelse Result, lists:reverse(Diags) ++ AccDiags} end, {false, []}, Constraints ), {AnyTrue, lists:reverse(DiagnosticsRev)}; evaluate_constraint_with_diagnostics(Database, {'not', Constraint}, Context) -> {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Constraint, Context), {not Result, Diagnostics}; evaluate_constraint_with_diagnostics(Database, {'not', Constraint, _Universe}, Context) -> {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Constraint, Context), {not Result, Diagnostics}; evaluate_constraint_with_diagnostics(Database, {exists, VarName, QuantifierRel, Constraint}, Context) when is_atom(VarName), is_atom(QuantifierRel) -> evaluate_quantifier_with_diagnostics(Database, exists, VarName, QuantifierRel, Constraint, Context); evaluate_constraint_with_diagnostics(Database, {forall, VarName, QuantifierRel, Constraint}, Context) when is_atom(VarName), is_atom(QuantifierRel) -> evaluate_quantifier_with_diagnostics(Database, forall, VarName, QuantifierRel, Constraint, Context); evaluate_constraint_with_diagnostics(_Database, Constraint, _Context) -> {false, [#{kind => unsupported_constraint, constraint => Constraint}]}. evaluate_quantifier_with_diagnostics(Database, Quantifier, VarName, QuantifierRel, Constraint, Context) -> case get_domain_from_db(Database, QuantifierRel) of {error, not_found} -> {false, [#{kind => quantifier_relation_not_found, quantifier => Quantifier, variable => VarName, relation => QuantifierRel}]}; {ok, Relation} -> case Relation#relation.cardinality of {finite, _} -> evaluate_finite_quantifier(Database, Quantifier, VarName, QuantifierRel, Constraint, Context); _ -> {false, [#{kind => unbounded_quantifier, quantifier => Quantifier, variable => VarName, relation => QuantifierRel}]} end end. evaluate_finite_quantifier(Database, Quantifier, VarName, QuantifierRel, Constraint, Context) -> Iterator = operations:get_tuples_iterator(Database, QuantifierRel), case operations:collect_all(Iterator) of {error, Reason, _Partial} -> {false, [#{kind => quantifier_iteration_error, quantifier => Quantifier, variable => VarName, relation => QuantifierRel, reason => Reason}]}; Tuples when is_list(Tuples) -> evaluate_quantifier_tuples(Database, Quantifier, VarName, Constraint, Context, Tuples) end. evaluate_quantifier_tuples(_Database, exists, _VarName, _Constraint, _Context, []) -> {false, []}; evaluate_quantifier_tuples(_Database, forall, _VarName, _Constraint, _Context, []) -> {true, []}; evaluate_quantifier_tuples(Database, Quantifier, VarName, Constraint, Context, Tuples) -> {ResultsRev, DiagnosticsRev} = lists:foldl( fun(TupleValueMap, {AccResults, AccDiags}) -> case extract_quantifier_value(TupleValueMap) of {ok, QuantifiedValue} -> ScopedContext = Context#{VarName => QuantifiedValue}, {Result, Diags} = evaluate_constraint_with_diagnostics(Database, Constraint, ScopedContext), {[Result | AccResults], lists:reverse(Diags) ++ AccDiags}; {error, Reason} -> {[false | AccResults], [#{kind => invalid_quantifier_tuple, variable => VarName, tuple => TupleValueMap, reason => Reason} | AccDiags]} end end, {[], []}, Tuples ), Results = lists:reverse(ResultsRev), Diagnostics = lists:reverse(DiagnosticsRev), case Quantifier of exists -> {lists:any(fun(X) -> X end, Results), Diagnostics}; forall -> {lists:all(fun(X) -> X end, Results), Diagnostics} end. extract_quantifier_value(TupleValueMap) when is_map(TupleValueMap) -> case maps:to_list(TupleValueMap) of [{_Attr, Value}] -> {ok, Value}; [] -> {error, empty_tuple}; _ -> {error, non_unary_quantifier_relation} end. -spec build_membership_criteria(#database_state{}, map()) -> map(). build_membership_criteria(Database, Schema) -> maps:fold( fun(AttrName, DomainName, Acc) -> case get_domain_from_db(Database, DomainName) of {ok, Domain} -> Test = case Domain#relation.membership_criteria of #{test := TestFun} -> TestFun; _ -> fun(_) -> true end end, Acc#{AttrName => #{domain => DomainName, test => Test}}; {error, not_found} -> Acc#{AttrName => #{domain => DomainName, test => fun(_) -> true end}} end end, #{}, Schema ). -spec lt(term(), term()) -> relational_constraint(). lt(Left, Right) -> {member_of, less_than, #{left => Left, right => Right}}. -spec lte(term(), term()) -> relational_constraint(). lte(Left, Right) -> {member_of, less_than_or_equal, #{left => Left, right => Right}}. -spec gt(term(), term()) -> relational_constraint(). gt(Left, Right) -> {member_of, greater_than, #{left => Left, right => Right}}. -spec gte(term(), term()) -> relational_constraint(). gte(Left, Right) -> {member_of, greater_than_or_equal, #{left => Left, right => Right}}. -spec eq(term(), term()) -> relational_constraint(). eq(Left, Right) -> {member_of, equal, #{left => Left, right => Right}}. -spec neq(term(), term()) -> relational_constraint(). neq(Left, Right) -> {member_of, not_equal, #{left => Left, right => Right}}. -spec between(term(), term()) -> relational_constraint(). between(Min, Max) -> {'and', [gte({var, value}, Min), lt({var, value}, Max)]}. -spec empty_constraints() -> #relation_constraints{}. empty_constraints() -> #relation_constraints{constraints = []}. -spec example_supertype_exclusive_constraints() -> #{atom() => #relation_constraints{}}. example_supertype_exclusive_constraints() -> #{ b => #relation_constraints{constraints = [ {b_requires_a, {member_of, a, #{id => {var, id}}}}, {b_excludes_c, {'not', {member_of, c, #{id => {var, id}}}}} ]}, c => #relation_constraints{constraints = [ {c_requires_a, {member_of, a, #{id => {var, id}}}}, {c_excludes_b, {'not', {member_of, b, #{id => {var, id}}}}} ]} }. -spec example_order_item_references_constraints() -> #relation_constraints{}. example_order_item_references_constraints() -> #relation_constraints{constraints = [ {order_item_order_fk, {member_of, order, #{id => {var, order_id}}}}, {order_item_product_fk, {member_of, product, #{id => {var, product_id}}}} ]}. -spec example_manager_constraints() -> #relation_constraints{}. example_manager_constraints() -> #relation_constraints{constraints = [ {managed_employee_exists, {member_of, employee, #{id => {var, employee_id}}}}, {manager_exists, {member_of, employee, #{id => {var, manager_id}}}}, {no_self_management, neq({var, employee_id}, {var, manager_id})} ]}. -spec example_ticket_status_exclusive_constraints() -> #{atom() => #relation_constraints{}}. example_ticket_status_exclusive_constraints() -> #{ open_ticket => #relation_constraints{constraints = [ {open_excludes_closed, {'not', {member_of, closed_ticket, #{ticket_id => {var, ticket_id}}}}} ]}, closed_ticket => #relation_constraints{constraints = [ {closed_excludes_open, {'not', {member_of, open_ticket, #{ticket_id => {var, ticket_id}}}}} ]} }. -spec example_weak_entity_dependency_constraints() -> #relation_constraints{}. example_weak_entity_dependency_constraints() -> #relation_constraints{constraints = [ {dependent_person_fk, {member_of, person, #{id => {var, person_id}}}} ]}. -spec example_department_has_employee_constraints() -> #relation_constraints{}. example_department_has_employee_constraints() -> #relation_constraints{constraints = [ {department_has_employee, {member_of, employee_dept, #{dept_id => {var, id}}}} ]}. -spec example_symmetry_antisymmetry_constraints() -> #{atom() => #relation_constraints{}}. example_symmetry_antisymmetry_constraints() -> #{ married_to => #relation_constraints{constraints = [ {marriage_is_symmetric, {member_of, married_to, #{person_a => {var, person_b}, person_b => {var, person_a}}}} ]}, reports_to => #relation_constraints{constraints = [ {reports_to_no_self, neq({var, employee_id}, {var, manager_id})}, {reports_to_antisymmetric, {'or', [ eq({var, employee_id}, {var, manager_id}), {'not', {member_of, reports_to, #{employee_id => {var, manager_id}, manager_id => {var, employee_id}}}} ]}} ]} }. -spec run_example_supertype_exclusive() -> map(). run_example_supertype_exclusive() -> main:setup(), DB0 = operations:create_database(example_supertype_exclusive), {DB1, _} = operations:create_relation(DB0, a, #{id => integer}), {DB2, _} = operations:create_relation(DB1, b, #{id => integer}), {DB3, _} = operations:create_relation(DB2, c, #{id => integer}), ConstraintsByRel = example_supertype_exclusive_constraints(), {DB4, _} = operations:update_relation_constraints(DB3, b, maps:get(b, ConstraintsByRel)), {DB5, _} = operations:update_relation_constraints(DB4, c, maps:get(c, ConstraintsByRel)), {DB6, _} = operations:create_tuple(DB5, a, #{id => 1}), InB = operations:create_tuple(DB6, b, #{id => 1}), DBAfterB = db_after(InB), InCWithB = operations:create_tuple(DBAfterB, c, #{id => 1}), {DB7, _} = operations:create_tuple(DBAfterB, a, #{id => 3}), InCWithAAndNoB = operations:create_tuple(DB7, c, #{id => 3}), InCWithoutA = operations:create_tuple(DBAfterB, c, #{id => 2}), #{accepted_in_b => InB, accepted_in_c_with_a_only => InCWithAAndNoB, rejected_c_overlap_b => InCWithB, rejected_c_missing_a => InCWithoutA}. -spec run_example_order_item_references() -> map(). run_example_order_item_references() -> main:setup(), DB0 = operations:create_database(example_order_item_references), {DB1, _} = operations:create_relation(DB0, order, #{id => integer}), {DB2, _} = operations:create_relation(DB1, product, #{id => integer}), {DB3, _} = operations:create_relation(DB2, order_item, #{order_id => integer, product_id => integer}), {DB4, _} = operations:update_relation_constraints(DB3, order_item, example_order_item_references_constraints()), {DB5, _} = operations:create_tuple(DB4, order, #{id => 10}), {DB6, _} = operations:create_tuple(DB5, product, #{id => 20}), Valid = operations:create_tuple(DB6, order_item, #{order_id => 10, product_id => 20}), DBAfterValid = db_after(Valid), MissingOrder = operations:create_tuple(DBAfterValid, order_item, #{order_id => 999, product_id => 20}), MissingProduct = operations:create_tuple(DBAfterValid, order_item, #{order_id => 10, product_id => 999}), #{accepted_valid_order_item => Valid, rejected_missing_order => MissingOrder, rejected_missing_product => MissingProduct}. -spec run_example_manager_constraints() -> map(). run_example_manager_constraints() -> main:setup(), DB0 = operations:create_database(example_manager_constraints), {DB1, _} = operations:create_relation(DB0, employee, #{id => integer}), {DB2, _} = operations:create_relation(DB1, manager_of, #{employee_id => integer, manager_id => integer}), {DB3, _} = operations:update_relation_constraints(DB2, manager_of, example_manager_constraints()), {DB4, _} = operations:create_tuple(DB3, employee, #{id => 1}), {DB5, _} = operations:create_tuple(DB4, employee, #{id => 2}), Valid = operations:create_tuple(DB5, manager_of, #{employee_id => 1, manager_id => 2}), DBAfterValid = db_after(Valid), Self = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 1, manager_id => 1}), MissingEmployee = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 3, manager_id => 2}), MissingManager = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 1, manager_id => 3}), #{accepted_valid_manager_pair => Valid, rejected_self_management => Self, rejected_missing_employee => MissingEmployee, rejected_missing_manager => MissingManager}. -spec run_example_ticket_status_exclusive() -> map(). run_example_ticket_status_exclusive() -> main:setup(), DB0 = operations:create_database(example_ticket_status_exclusive), {DB1, _} = operations:create_relation(DB0, open_ticket, #{ticket_id => integer}), {DB2, _} = operations:create_relation(DB1, closed_ticket, #{ticket_id => integer}), ConstraintsByRel = example_ticket_status_exclusive_constraints(), {DB3, _} = operations:update_relation_constraints(DB2, open_ticket, maps:get(open_ticket, ConstraintsByRel)), {DB4, _} = operations:update_relation_constraints(DB3, closed_ticket, maps:get(closed_ticket, ConstraintsByRel)), Open7 = operations:create_tuple(DB4, open_ticket, #{ticket_id => 7}), DBAfterOpen7 = db_after(Open7), Closed7 = operations:create_tuple(DBAfterOpen7, closed_ticket, #{ticket_id => 7}), Closed8 = operations:create_tuple(DBAfterOpen7, closed_ticket, #{ticket_id => 8}), DBAfterClosed8 = db_after(Closed8), Open8 = operations:create_tuple(DBAfterClosed8, open_ticket, #{ticket_id => 8}), #{accepted_open_7 => Open7, rejected_closed_7 => Closed7, accepted_closed_8 => Closed8, rejected_open_8 => Open8}. -spec run_example_weak_entity_dependency() -> map(). run_example_weak_entity_dependency() -> main:setup(), DB0 = operations:create_database(example_weak_entity_dependency), {DB1, _} = operations:create_relation(DB0, person, #{id => integer}), {DB2, _} = operations:create_relation(DB1, dependent, #{person_id => integer, dep_name => string}), {DB3, _} = operations:update_relation_constraints(DB2, dependent, example_weak_entity_dependency_constraints()), {DB4, _} = operations:create_tuple(DB3, person, #{id => 1}), Valid = operations:create_tuple(DB4, dependent, #{person_id => 1, dep_name => "kid"}), DBAfterValid = db_after(Valid), MissingParent = operations:create_tuple(DBAfterValid, dependent, #{person_id => 2, dep_name => "kid2"}), #{accepted_existing_parent => Valid, rejected_missing_parent => MissingParent}. -spec run_example_department_has_employee() -> map(). run_example_department_has_employee() -> main:setup(), DB0 = operations:create_database(example_department_has_employee), {DB1, _} = operations:create_relation(DB0, employee_dept, #{dept_id => integer, employee_id => integer}), {DB2, _} = operations:create_relation(DB1, department, #{id => integer}), {DB3, _} = operations:update_relation_constraints(DB2, department, example_department_has_employee_constraints()), {DB4, _} = operations:create_tuple(DB3, employee_dept, #{dept_id => 1, employee_id => 99}), Dept1 = operations:create_tuple(DB4, department, #{id => 1}), DBAfterDept1 = db_after(Dept1), Dept2 = operations:create_tuple(DBAfterDept1, department, #{id => 2}), #{accepted_department_with_employee => Dept1, rejected_department_without_employee => Dept2}. -spec run_example_symmetry_antisymmetry() -> map(). run_example_symmetry_antisymmetry() -> main:setup(), DB0 = operations:create_database(example_symmetry_antisymmetry), {DB1, _} = operations:create_relation(DB0, married_to, #{person_a => integer, person_b => integer}), {DB2, _} = operations:create_relation(DB1, reports_to, #{employee_id => integer, manager_id => integer}), {DB3, _} = operations:create_tuple(DB2, married_to, #{person_a => 2, person_b => 1}), {DB4, _} = operations:create_tuple(DB3, married_to, #{person_a => 1, person_b => 2}), ConstraintsByRel = example_symmetry_antisymmetry_constraints(), {DB5, _} = operations:update_relation_constraints(DB4, married_to, maps:get(married_to, ConstraintsByRel)), {DB6, _} = operations:update_relation_constraints(DB5, reports_to, maps:get(reports_to, ConstraintsByRel)), MissingReverseMarriage = operations:create_tuple(DB6, married_to, #{person_a => 3, person_b => 4}), AcceptedWithReverse = operations:create_tuple(DB6, married_to, #{person_a => 2, person_b => 1}), Reports12 = operations:create_tuple(DB6, reports_to, #{employee_id => 1, manager_id => 2}), DBAfterReports12 = db_after(Reports12), Reports21 = operations:create_tuple(DBAfterReports12, reports_to, #{employee_id => 2, manager_id => 1}), Reports33 = operations:create_tuple(DBAfterReports12, reports_to, #{employee_id => 3, manager_id => 3}), #{rejected_missing_reverse_marriage => MissingReverseMarriage, accepted_with_existing_reverse_marriage => AcceptedWithReverse, accepted_reports_1_to_2 => Reports12, rejected_reports_2_to_1 => Reports21, rejected_reports_3_to_3 => Reports33}. -spec run_all_examples() -> map(). run_all_examples() -> #{ supertype_exclusive => run_example_supertype_exclusive(), order_item_references => run_example_order_item_references(), manager_constraints => run_example_manager_constraints(), ticket_status_exclusive => run_example_ticket_status_exclusive(), weak_entity_dependency => run_example_weak_entity_dependency(), department_has_employee => run_example_department_has_employee(), symmetry_antisymmetry => run_example_symmetry_antisymmetry() }. db_after({DB, _Relation}) -> DB. -spec merge_constraints(#relation_constraints{} | undefined, #relation_constraints{} | undefined) -> #relation_constraints{}. merge_constraints(C1, C2) -> N1 = normalize_constraints(C1), N2 = normalize_constraints(C2), #relation_constraints{constraints = N1#relation_constraints.constraints ++ N2#relation_constraints.constraints}. -spec filter_constraints(#relation_constraints{} | undefined, [atom()]) -> #relation_constraints{}. filter_constraints(Constraints, Attrs) -> Normalized = normalize_constraints(Constraints), Keep = [ {Name, Formula} || {Name, Formula} <- Normalized#relation_constraints.constraints, lists:all(fun(V) -> lists:member(V, Attrs) end, vars_in_constraint(Formula)) ], #relation_constraints{constraints = Keep}. -spec rename_constraint_attrs(#relation_constraints{} | undefined, map()) -> #relation_constraints{}. rename_constraint_attrs(Constraints, RenameMappings) -> Normalized = normalize_constraints(Constraints), Renamed = [{Name, rename_vars_in_constraint(Formula, RenameMappings)} || {Name, Formula} <- Normalized#relation_constraints.constraints], #relation_constraints{constraints = Renamed}. -spec infer_constraints_from_pred(term(), map()) -> #relation_constraints{}. infer_constraints_from_pred(Predicate, _Schema) when is_function(Predicate) -> empty_constraints(); infer_constraints_from_pred({attr, AttrName, Op, Value}, _Schema) -> Name = list_to_atom(atom_to_list(AttrName) ++ "_" ++ atom_to_list(Op)), #relation_constraints{constraints = [{Name, op_to_constraint(Op, AttrName, Value)}]}; infer_constraints_from_pred({'and', Predicates}, Schema) -> lists:foldl( fun(Pred, Acc) -> merge_constraints(Acc, infer_constraints_from_pred(Pred, Schema)) end, empty_constraints(), Predicates ); infer_constraints_from_pred({'or', Predicates}, Schema) -> OrParts = [ constraint_from_predicate(P, Schema) || P <- Predicates, constraint_from_predicate(P, Schema) =/= undefined ], case OrParts of [] -> empty_constraints(); _ -> #relation_constraints{constraints = [{inferred_or, {'or', OrParts}}]} end; infer_constraints_from_pred(_, _Schema) -> empty_constraints(). constraint_from_predicate({attr, AttrName, Op, Value}, _Schema) -> op_to_constraint(Op, AttrName, Value); constraint_from_predicate({'and', Predicates}, Schema) -> {'and', [C || C <- [constraint_from_predicate(P, Schema) || P <- Predicates], C =/= undefined]}; constraint_from_predicate({'or', Predicates}, Schema) -> {'or', [C || C <- [constraint_from_predicate(P, Schema) || P <- Predicates], C =/= undefined]}; constraint_from_predicate(_, _Schema) -> undefined. op_to_constraint(lt, Attr, Value) -> lt({var, Attr}, Value); op_to_constraint(lte, Attr, Value) -> lte({var, Attr}, Value); op_to_constraint(gt, Attr, Value) -> gt({var, Attr}, Value); op_to_constraint(gte, Attr, Value) -> gte({var, Attr}, Value); op_to_constraint(eq, Attr, Value) -> eq({var, Attr}, Value); op_to_constraint(neq, Attr, Value) -> neq({var, Attr}, Value); op_to_constraint(between, Attr, {Min, Max}) -> {'and', [gte({var, Attr}, Min), lt({var, Attr}, Max)]}; op_to_constraint(_, _Attr, _Value) -> {'and', []}. normalize_constraints(undefined) -> empty_constraints(); normalize_constraints(#relation_constraints{} = Constraints) -> NormalizedEntries = normalize_named_constraints(Constraints#relation_constraints.constraints), assert_unique_constraint_names(NormalizedEntries), Constraints#relation_constraints{constraints = NormalizedEntries}; normalize_constraints(_) -> empty_constraints(). normalize_named_constraints(Entries) -> [normalize_named_constraint(Entry) || Entry <- Entries]. normalize_named_constraint({Name, Formula}) when is_atom(Name) -> {Name, Formula}; normalize_named_constraint(InvalidEntry) -> error({invalid_constraint_entry, InvalidEntry}). assert_unique_constraint_names(NamedConstraints) -> Names = [Name || {Name, _Formula} <- NamedConstraints], case has_duplicates(Names) of true -> error({duplicate_constraint_names, Names}); false -> ok end. has_duplicates([]) -> false; has_duplicates([Name | Rest]) -> lists:member(Name, Rest) orelse has_duplicates(Rest). vars_in_constraint({member_of, _Rel, Binding}) when is_map(Binding) -> maps:fold( fun(_K, V, Acc) -> Acc ++ vars_in_term(V) end, [], Binding); vars_in_constraint({'and', Cs}) -> lists:usort(lists:flatmap(fun vars_in_constraint/1, Cs)); vars_in_constraint({'or', Cs}) -> lists:usort(lists:flatmap(fun vars_in_constraint/1, Cs)); vars_in_constraint({'not', C}) -> vars_in_constraint(C); vars_in_constraint({'not', C, _}) -> vars_in_constraint(C); vars_in_constraint({exists, Var, _Rel, C}) -> lists:delete(Var, vars_in_constraint(C)); vars_in_constraint({forall, Var, _Rel, C}) -> lists:delete(Var, vars_in_constraint(C)); vars_in_constraint(_) -> []. vars_in_term({var, Name}) when is_atom(Name) -> [Name]; vars_in_term({const, _}) -> []; vars_in_term(T) when is_tuple(T) -> lists:flatmap(fun vars_in_term/1, tuple_to_list(T)); vars_in_term(T) when is_list(T) -> lists:flatmap(fun vars_in_term/1, T); vars_in_term(T) when is_map(T) -> maps:fold(fun(_K, V, Acc) -> Acc ++ vars_in_term(V) end, [], T); vars_in_term(_) -> []. rename_vars_in_constraint({member_of, RelName, Binding}, Mappings) -> RenamedBinding = maps:map( fun(_K, {var, VarName}) -> {var, maps:get(VarName, Mappings, VarName)}; (_K, V) -> V end, Binding ), {member_of, RelName, RenamedBinding}; rename_vars_in_constraint({'and', Constraints}, Mappings) -> {'and', [rename_vars_in_constraint(C, Mappings) || C <- Constraints]}; rename_vars_in_constraint({'or', Constraints}, Mappings) -> {'or', [rename_vars_in_constraint(C, Mappings) || C <- Constraints]}; rename_vars_in_constraint({'not', Constraint}, Mappings) -> {'not', rename_vars_in_constraint(Constraint, Mappings)}; rename_vars_in_constraint({'not', Constraint, Universe}, Mappings) -> {'not', rename_vars_in_constraint(Constraint, Mappings), Universe}; rename_vars_in_constraint({exists, Var, Rel, Constraint}, Mappings) -> {exists, maps:get(Var, Mappings, Var), Rel, rename_vars_in_constraint(Constraint, Mappings)}; rename_vars_in_constraint({forall, Var, Rel, Constraint}, Mappings) -> {forall, maps:get(Var, Mappings, Var), Rel, rename_vars_in_constraint(Constraint, Mappings)}; rename_vars_in_constraint(Other, _Mappings) -> Other. validate_tuple_against_criteria(Tuple, Criteria) when is_map(Criteria) -> maps:fold( fun(Attr, Constraint, Acc) -> Acc andalso validate_attribute(maps:get(Attr, Tuple, undefined), Constraint) end, true, Criteria ). validate_attribute(undefined, _) -> false; validate_attribute(Value, #{test := Test}) -> Test(#{value => Value}); validate_attribute(Value, Constraint) when is_function(Constraint, 1) -> Constraint(Value); validate_attribute(_, _) -> true. apply_substitution(Binding, Substitution) -> maps:map( fun(_K, {var, VarName}) -> maps:get(VarName, Substitution, {var, VarName}); (_K, V) -> V end, Binding ). find_variables(Binding) -> maps:fold( fun(_K, {var, VarName}, Acc) -> [VarName | Acc]; (_K, _, Acc) -> Acc end, [], Binding ). get_schema(#domain{schema = S}) -> S; get_schema(#relation{schema = S}) -> S. get_name(#domain{name = N}) -> N; get_name(#relation{name = N}) -> N. get_lineage(#relation{lineage = L}) -> L; get_lineage(#domain{name = N}) -> {base, N}. schemas_compatible(S1, S2) -> maps:keys(S1) =:= maps:keys(S2). merge_schemas(S1, _S2) -> S1. get_cardinality(#domain{cardinality = C}) -> C; get_cardinality(#relation{cardinality = C}) -> C. complement_cardinality(R, Universe) -> RC = get_cardinality(R), UC = get_cardinality(Universe), case {RC, UC} of {{finite, N}, {finite, M}} -> {finite, max(0, M - N)}; {{finite, _}, Infinite} -> Infinite; _ -> UC end. intersection_cardinality(R1, R2) -> C1 = get_cardinality(R1), C2 = get_cardinality(R2), case {C1, C2} of {{finite, N}, _} -> {finite, N}; {_, {finite, M}} -> {finite, M}; _ -> min_cardinality(C1, C2) end. union_cardinality(R1, R2) -> C1 = get_cardinality(R1), C2 = get_cardinality(R2), case {C1, C2} of {{finite, N}, {finite, M}} -> {finite, N + M}; _ -> max_cardinality(C1, C2) end. min_cardinality({finite, N}, {finite, M}) -> {finite, min(N, M)}; min_cardinality({finite, N}, _) -> {finite, N}; min_cardinality(_, {finite, M}) -> {finite, M}; min_cardinality(aleph_zero, _) -> aleph_zero; min_cardinality(_, aleph_zero) -> aleph_zero; min_cardinality(continuum, continuum) -> continuum. max_cardinality({finite, N}, {finite, M}) -> {finite, max(N, M)}; max_cardinality(continuum, _) -> continuum; max_cardinality(_, continuum) -> continuum; max_cardinality(aleph_zero, _) -> aleph_zero; max_cardinality(_, aleph_zero) -> aleph_zero. evaluate_constraint(Database, {member_of, RelName, Binding}, Context) when is_atom(RelName) -> BoundBinding = bind_variables(Binding, Context), case get_domain_from_db(Database, RelName) of {ok, Relation} -> {ok, member(BoundBinding, Relation)}; {error, not_found} -> {error, {relation_not_found, RelName}} end; evaluate_constraint(_Database, {member_of, #relation{} = Rel, Binding}, Context) -> BoundBinding = bind_variables(Binding, Context), {ok, member(BoundBinding, Rel)}; evaluate_constraint(_Database, {member_of, #domain{} = Rel, Binding}, Context) -> BoundBinding = bind_variables(Binding, Context), {ok, member(BoundBinding, Rel)}; evaluate_constraint(Database, {'not', Constraint, _Universe}, Context) -> case evaluate_constraint(Database, Constraint, Context) of {ok, Bool} -> {ok, not Bool}; Error -> Error end; evaluate_constraint(Database, {'and', Constraints}, Context) -> Results = [evaluate_constraint(Database, C, Context) || C <- Constraints], case lists:all(fun({ok, true}) -> true; (_) -> false end, Results) of true -> {ok, true}; false -> {ok, false} end; evaluate_constraint(Database, {'or', Constraints}, Context) -> Results = [evaluate_constraint(Database, C, Context) || C <- Constraints], case lists:any(fun({ok, true}) -> true; (_) -> false end, Results) of true -> {ok, true}; false -> {ok, false} end; evaluate_constraint(Database, {'not', Constraint}, Context) -> {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {'not', Constraint}, Context), {ok, Result}; evaluate_constraint(Database, {exists, VarName, QuantifierRel, Constraint}, Context) -> {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {exists, VarName, QuantifierRel, Constraint}, Context), {ok, Result}; evaluate_constraint(Database, {forall, VarName, QuantifierRel, Constraint}, Context) -> {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {forall, VarName, QuantifierRel, Constraint}, Context), {ok, Result}; evaluate_constraint(_Database, _, _) -> {error, unsupported_constraint}. bind_variables(Binding, Context) -> maps:map( fun(_K, {var, VarName}) -> maps:get(VarName, Context, maps:get(value, Context, undefined)); (_K, {const, Value}) -> Value; (_K, V) -> V end, Binding ). -spec get_domain_from_db(#database_state{}, atom()) -> {ok, #relation{}} | {error, not_found}. get_domain_from_db(Database, DomainName) -> case maps:find(DomainName, Database#database_state.relations) of {ok, DomainHash} -> [Domain] = mnesia:dirty_read(relation, DomainHash), {ok, Domain}; error -> {error, not_found} end.