Class ValueComparator
- All Implemented Interfaces:
Comparator<Value>
Value instances for use in sort and
aggregation operators.
Two singleton instances are provided:
NULLS_LAST—NULLsorts after all non-null values (standard SQLASCbehaviour).NULLS_FIRST—NULLsorts before all non-null values (standard SQLDESCbehaviour).
This class is the single authority on when two values are equal or
ordered. Equality (equal(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)), ordering (compareNonNull(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)) and the
hash-bucket token (joinToken(com.darkcollective.relix.value.Value)) all read one coercion rule, so a pair the
selection operator matches is a pair the join operator matches. They did once
disagree: each of the two evaluators grew the coercion its own callers needed —
strings against booleans here, strings against temporals there — and a predicate
written as a selection and the same predicate written as a join then gave
different answers with no diagnostic.
Non-null values of the same runtime type are ordered naturally:
NumberValue by BigDecimal.compareTo(java.math.BigDecimal),
StringValue by code point (CodePoints.compare(java.lang.String, java.lang.String)), BooleanValue with
false before true, and the temporal values
(DateValue, TimeValue, TimestampValue,
DurationValue) by their java.time natural order. Comparing
values of different types throws EvaluationException.
A string is taken to mean what it spells. Text naming a boolean
("true", "TRUE") or spelling a temporal
("2024-01-15", "2024-01-15T11:00:00+01:00") is that value here,
whatever it is being compared against — which is what lets a CSV or inline-table
cell, inferred as STRING, meet a typed literal. Two spellings of one value
are therefore equal, and are ordered by the value rather than as text: an offset
timestamp sorts by the instant it denotes, not by its leading digits.
The consequence to know about is that such text is no longer a STRING
for the purpose of ordering, so a column holding "2024-01-15" beside
"pending" holds a DATE beside a STRING and has no order.
Cast it to settle what the column holds.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final ValueComparatorNULLvalues sort before all non-null values.static final ValueComparatorNULLvalues sort after all non-null values. -
Method Summary
Modifier and TypeMethodDescriptionstatic ValueReturns the value a string denotes — the temporal it spells or the boolean it names — orvunchanged when it is not a string or denotes neither.intstatic intcompareNonNull(Value a, Value b) Compares two non-null values, canonicalised percanonical(com.darkcollective.relix.value.Value).static booleanReturns whether two values are equal, canonicalised percanonical(com.darkcollective.relix.value.Value).static StringReturns the hash-bucket token forv— the form in which any two valuesequal(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)accepts are guaranteed to produce the same string.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface java.util.Comparator
equals, reversed, thenComparing, thenComparing, thenComparing, thenComparingDouble, thenComparingInt, thenComparingLong
-
Field Details
-
NULLS_LAST
NULLvalues sort after all non-null values. -
NULLS_FIRST
NULLvalues sort before all non-null values.
-
-
Method Details
-
compare
- Specified by:
comparein interfaceComparator<Value>
-
canonical
Returns the value a string denotes — the temporal it spells or the boolean it names — orvunchanged when it is not a string or denotes neither.Inline-table, CSV and JSON cells all infer as
STRING, so a column holding timestamps or flags reaches the executor asStringValuewhile the literal it is compared against is properly typed. Both coercions exist for that one reason —"2024-01-15T10:00:00"against aTIMESTAMPand"true"against aBOOLEAN— and both belong here rather than at a call site, because a coercion only one caller knows about is a way for two callers to disagree.It reads one value and no partner, and that is the whole design. A rule that coerced a string only when the other operand was typed made equality depend on the pair: a
TIMESTAMPequalled both"2024-01-15T10:00:00Z"and"2024-01-15T11:00:00+01:00"while those two, being both strings, were compared as text and were unequal. That is precisely the substitutability axiomComparatorrequires, and breaking it does not raise an error — it makes a sort's result depend on the order the rows arrived in, and can place a later timestamp before an earlier one. No pairwise rule can avoid this: equivalence classes have to be a property of a value alone.The price is paid by a
STRINGcolumn that mixes text denoting a value with text denoting nothing."2024-01-15"is aDATEhere, so ordering it against"pending"is ordering aDATEagainst aSTRING, which has no answer and says so. Cast the column to settle what it holds.It is public because the operators that deduplicate need it as much as the ones that match. δ, γ and the set operations build a key and compare it with
Value.equals, which is record equality and knows nothing of this rule — so σ called a string-spelled boolean equal to aBOOLEANwhile δ kept the two apart, and the duplicate it returned printed identically to the row beside it. Keying on the canonical form is what closes that, and it works with plain list equality becauseValue.equalsis already right within a type (NumberValueoverriding it so that5and5.0are one number); this rule is what settles it across types.A key is not an output. The canonical form decides which rows group together and must never replace the value the operator emits, or grouping a
STRINGcolumn of dates would start returningDATEs. -
equal
Returns whether two values are equal, canonicalised percanonical(com.darkcollective.relix.value.Value).Unlike
compareNonNull(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)this never throws: values of genuinely different types are simply not equal. That is what an equality test wants — a join looking for matching rows should skip a mismatched pair, not abort the query — and it is why a match test must call this rather than compare against zero, which conflates "different" with "unorderable".NULL is equal to nothing, itself included.
-
joinToken
Returns the hash-bucket token forv— the form in which any two valuesequal(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)accepts are guaranteed to produce the same string.A hash join buckets rows by this token and then re-checks each candidate pair, so the token is a pre-filter and carries a one-way obligation: it may put more pairs in a bucket than actually match (the re-check discards them, costing only time), but it must never separate a pair that would match — those rows are then silently dropped from the result.
That is why this is not simply
Value.asDisplayString(). The display form already collapsesNUMBERscale (5and5.0both render"5"), but it keeps a string apart from the valuecanonical(com.darkcollective.relix.value.Value)turns it into —"2024-01-15T10:00:00"against anInstantrendering"2024-01-15T10:00:00Z", or"TRUE"againsttrue. Reading the same canonical form closes that gap, and reading it from the same method is what keeps the token andequal(com.darkcollective.relix.value.Value, com.darkcollective.relix.value.Value)from drifting apart — they were two statements of one rule until they were not. -
compareNonNull
Compares two non-null values, canonicalised percanonical(com.darkcollective.relix.value.Value).- Throws:
EvaluationException- if the values have no shared order
-