Decomposes a query to an expanded DNF form.
Takes a where clause part of a query and decomposes it into a list of expressions in a Disjunctive Normal Form (DNF). Each expression is a conjunction of comparisons.
To avoid duplication, it returns a list of lists, where the outer list is a list of disjuncts (conjunctions), and the inner list is a list of comparisons. Each comparison (i.e. the sub-expression of the original where clause) is represented by an Erlang reference, which is then mentioned in the map of references to the AST of the referenced subexpression.
NOT handling
To properly convert to DNF, NOT expressions are pushed down to leaf expressions using De Morgan's laws:
NOT (a OR b)becomes(NOT a) AND (NOT b)NOT (a AND b)becomes(NOT a) OR (NOT b)
Because of this, leaf expressions in the result can be either:
ref- a positive reference to a subexpression{:not, ref}- a negated reference to a subexpressionnil- this position is not part of this disjunct
The subexpressions map always contains the base (non-negated) form of each expression.
Expanded format
The "expanded" part means that each inner list MUST be the same length, equal to the total count of expressions
across all disjuncts. Each position in the inner list corresponds to a specific expression slot from the original
query structure, and contains either a reference (possibly negated) to that subexpression or nil if that
expression is not part of the given disjunct.
References allow deduplication: if the same subexpression appears in multiple disjuncts, they will share the same reference (but occupy different positions, since positions correspond to the original query structure).
Examples
For the query (already in a normalized form):
WHERE (a = 1 AND b = 2) OR (c = 3 AND d = 4) OR (a = 1 AND c = 3)Has 3 disjuncts with 2 + 2 + 2 = 6 total expression slots. It will be decomposed into:
[[r1, r2, nil, nil, nil, nil],
[nil, nil, r3, r4, nil, nil],
[nil, nil, nil, nil, r1, r3]]Where:
- Positions 0-1 correspond to disjunct 1's expressions (
a = 1,b = 2) - Positions 2-3 correspond to disjunct 2's expressions (
c = 3,d = 4) - Positions 4-5 correspond to disjunct 3's expressions (
a = 1,c = 3) r1appears at positions 0 and 4 (same subexpressiona = 1)r3appears at positions 2 and 5 (same subexpressionc = 3)
The reference map will contain: r1 => "a = 1", r2 => "b = 2", r3 => "c = 3", r4 => "d = 4".
For a query with NOT that needs De Morgan transformation:
WHERE NOT (a = 1 OR b = 2)Becomes (NOT a = 1) AND (NOT b = 2) - a single disjunct with two negated terms:
[[{:not, r1}, {:not, r2}]]And for:
WHERE NOT (a = 1 AND b = 2)Becomes (NOT a = 1) OR (NOT b = 2) - two disjuncts:
[[{:not, r1}, nil],
[nil, {:not, r2}]]
Summary
Types
@type conjunction() :: [literal()]
@type decomposition() :: %{ disjuncts: dnf(), disjuncts_positions: [[position()]], subexpressions: %{required(position()) => subexpression()}, position_count: non_neg_integer() }
@type dnf() :: [conjunction()]
@type literal() :: {position(), :positive | :negated}
@type position() :: non_neg_integer()
@type subexpression() :: %{ ast: Electric.Replication.Eval.Parser.tree_part(), is_subquery: boolean(), negated: boolean() }
Functions
@spec decompose(query :: Electric.Replication.Eval.Parser.tree_part()) :: {:ok, decomposition()} | {:error, term()}