aboutsummaryrefslogblamecommitdiffstats
path: root/docs/n-way-binding.org
blob: c49d2a1d710744f5180f9d4c1116cf25492240a2 (plain) (tree)
1
2
3
4
5
6
                                         



                  
                                                                                            



































































                                                                                                        
                     

                                                                              
                                                                 































































                                                                                           
                                                                                          




























































                                                                                              
                                                                                     
                                                     
#+TITLE: N-Way Binding in ICL Constraints
#+DATE: 2026-03-11

* Overview

The ICL constraint language supports quantified membership checks via ~Exists~ and ~Forall~.
This document illustrates the hotel/building/room/suite example to motivate n-ary quantification
and show how 3-wise (and deeper) relational composition is expressed.

* DML — Schema

#+begin_src lisp
(CreateRelation
  (name "Building")
  (schema
    ((building_id "integer")
     (building_name "string")
     (floors "integer"))))

(CreateRelation
  (name "Room")
  (schema
    ((room_id "integer")
     (building_id "integer")
     (floor "integer")
     (room_number "string"))))

(CreateRelation
  (name "Suite")
  (schema
    ((suite_id "integer")
     (room_id "integer")
     (suite_name "string")
     (capacity "integer"))))
#+end_src

* DML — Data

Tower B has only 2 floors, which is what causes the ~suite_in_tall_building~ constraint
to fire when suites referencing its rooms are inserted.

#+begin_src lisp
(InsertTuple
  (relation "Building")
  (attributes
    ((building_id (Int 1))
     (building_name (Str "Tower A"))
     (floors (Int 10)))))

(InsertTuple
  (relation "Building")
  (attributes
    ((building_id (Int 2))
     (building_name (Str "Tower B"))
     (floors (Int 2)))))   ;; ≤ 3 — suites in rooms here will be rejected

(InsertTuples
  (relation "Room")
  (tuples
    (((room_id (Int 101)) (building_id (Int 1)) (floor (Int 1)) (room_number (Str "1A")))
     ((room_id (Int 102)) (building_id (Int 1)) (floor (Int 2)) (room_number (Str "2A")))
     ((room_id (Int 201)) (building_id (Int 2)) (floor (Int 1)) (room_number (Str "1B"))))))

;; Suites 1001 and 1002 reference room 101 (Tower A, 10 floors) — accepted.
;; Suite 1002 references room 102 (Tower A, 10 floors) — accepted.
;; Suite 1003 references room 201 (Tower B, 2 floors) — rejected by suite_in_tall_building.
(InsertTuples
  (relation "Suite")
  (tuples
    (((suite_id (Int 1001)) (room_id (Int 101)) (suite_name (Str "Presidential")) (capacity (Int 4)))
     ((suite_id (Int 1002)) (room_id (Int 101)) (suite_name (Str "Standard"))     (capacity (Int 2)))
     ((suite_id (Int 1003)) (room_id (Int 102)) (suite_name (Str "Deluxe"))       (capacity (Int 3))))))
#+end_src

* ICL — Constraints

Binary FK constraints compose to enforce referential integrity along the chain
~Suite -> Room -> Building~. Each link is declared independently.

#+begin_src lisp
;; FK: every Room must belong to an existing Building
(RegisterConstraint
  (constraint_name "fk_room_building")
  (relation_name "Room")
  (body (MemberOf
    (target "Building")
    (binding ((building_id (Var "building_id")))))))

;; FK: every Suite must belong to an existing Room
(RegisterConstraint
  (constraint_name "fk_suite_room")
  (relation_name "Suite")
  (body (MemberOf
    (target "Room")
    (binding ((room_id (Var "room_id")))))))
#+end_src

* DRL — Queries

The 3-wise join is expressed by composing binary joins. No special n-ary syntax is needed
in DRL — the relational model handles this naturally through join composition.

#+begin_src lisp
;; All suites with their room and building — full 3-way join
(Join
  (room_id)
  (Base "Suite")
  (Join
    (building_id)
    (Base "Room")
    (Base "Building")))

;; Suites in Building 1 only
(Select
  (Const ((building_id (Int 1))))
  (Join
    (room_id)
    (Base "Suite")
    (Join
      (building_id)
      (Base "Room")
      (Base "Building"))))

;; Just suite names and the building they ultimately belong to
(Project
  (suite_name building_name)
  (Join
    (room_id)
    (Base "Suite")
    (Join
      (building_id)
      (Base "Room")
      (Base "Building"))))
#+end_src

* Motivation for N-Ary ~Exists~

The binary FK constraints above can be decomposed into independent ~MemberOf~ checks.
However, some constraints require joining through intermediate variables before a
membership check can be made. These cannot be decomposed.

Example: /a suite is only valid if its room belongs to a building with more than 3 floors/.
The chain ~suite.room_id -> room.building_id -> building.floors~ requires two intermediate
variables — it is inherently a 3-wise constraint.

Each ~Exists~ record field must be wrapped by name when written in s-expression form, as
required by ~[@@deriving sexp]~. The ~body~ field of the inner ~Exists~ must be written
explicitly as ~(body ...)~, not passed positionally.

#+begin_src lisp
(RegisterConstraint
  (constraint_name "suite_in_tall_building")
  (relation_name "Suite")
  (body
    (Exists (variable "r") (quantifier "Room")
      (body
        (Exists (variable "b") (quantifier "Building")
          (body
            (And
              ((MemberOf (target "Room")
                 (binding ((room_id (Var "room_id")))))
               (MemberOf (target "Building")
                 (binding ((building_id (Var "r.building_id")))))
               (MemberOf (target "greater_than_natural")
                 (binding ((left (Var "b.floors")) (right (Const (Int 3)))))))))))))
#+end_src

With the current single-binding ~Exists~, this requires nesting. With the proposed n-ary form,
it becomes flat and the quantifier chain is explicit:

#+begin_src lisp
;; Proposed n-ary Exists (not yet implemented)
(RegisterConstraint
  (constraint_name "suite_in_tall_building")
  (relation_name "Suite")
  (body
    (Exists
      (bindings ((r "Room") (b "Building")))
      (body
        (And
          ((MemberOf (target "Room")
             (binding ((room_id (Var "room_id")))))
           (MemberOf (target "Building")
             (binding ((building_id (Var "r.building_id")))))
           (MemberOf (target "greater_than_natural")
             (binding ((left (Var "b.floors")) (right (Const (Int 3))))))))))))
#+end_src

* Namespacing

Attributes from quantifier rows are namespaced by the variable name in ~extend_tuple~,
so that same-named attributes from different scopes do not overwrite each other.
~Var "attr"~ without a namespace refers to the tuple being checked (the relation the
constraint is registered on). ~Var "x.attr"~ refers to attribute ~attr~ from the row
bound to variable ~x~.

| Expression              | Resolves to                                   |
|-------------------------+-----------------------------------------------|
| ~(Var "room_id")~       | ~suite.room_id~ (the incoming tuple)          |
| ~(Var "r.building_id")~ | ~building_id~ from the ~r~ (Room) quantifier  |
| ~(Var "b.floors")~      | ~floors~ from the ~b~ (Building) quantifier   |

Without namespacing, ~extend_tuple~ would overwrite the incoming tuple's attributes
with same-named attributes from the quantifier row, silently making ~Var~ resolve to
the wrong scope. See ~lib/constraint.ml~ and ~docs/icl-quantification.org~ for a full
account of this failure mode and the provenance TODO.