Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CCL_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,41 @@ bindings on navigation stops, scope prefixes, and transitive keys are
not yet supported. Evaluating what a range read returns is a
server-side concern and is outside the grammar.

#### Modification marker (leaf keys)

A leaf key can bind to the values whose **add** occurred at or after a
timestamp, using a trailing `~` marker. The form `key[t~]` binds the key
over the half-open window `[t, now)`: start-inclusive at `t`,
end-exclusive at the present moment.

```
locked[1718380800000000~] = null -- lock values ADDED at or after t
find locked[1700000000~] = "pod-A" -- ...added at or after t, equal to pod-A
```

This is distinct from both the single-instant and the range forms. The
single-instant `foo[t]` binds the value *present at* the instant `t`
regardless of when it was added; `foo[t~]` keys strictly off the
add-event, so a value added before `t` (even if it is still present) is
not in range. The range `foo[t1...t2]` binds anything *present during*
the interval regardless of add time. The motivating use is stale-lock
detection: a record whose only lock value was added before the cutoff
binds to nothing under `locked[<cutoff>~]` and is therefore claimable.

A marker binds a single instant, so it cannot be combined with a range
(`foo[t1...t2~]` is rejected), and a key still carries at most one
bracket annotation (`foo[t1~][t2]` is rejected). The leading form
`foo[~t]` (added-before) is **not** supported and is rejected at parse
time; it is semantically equivalent to an existence-before range and is
deferred to that sibling work.

The canonical serialization renders the timestamp as microseconds with a
trailing `~` and omits any keyword, for example `locked[1700000000~]`.

Modification markers are currently supported only on flat leaf keys, not
on navigation stops or scope prefixes. Evaluating what a marked read
returns is a server-side concern and is outside the grammar.

#### Per-stop navigation

Navigation keys carry one bracket per stop. Each annotation binds only
Expand Down
19 changes: 16 additions & 3 deletions grammar/grammar.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -763,16 +763,29 @@ TimestampSymbol Timestamp() :
// Bracket-form key parameter: the raw content between a leaf key's
// brackets. The leading at/on/during keyword is optional (the brackets
// themselves are the pivot) and is consumed without being returned.
// Interpreting the content -- single instant vs. temporal range -- is
// deferred to Parsing.applyKeyBracket so the meaning lives in one place.
// Interpreting the content -- single instant, temporal range, or
// modification marker -- is deferred to Parsing.applyKeyBracket so the
// meaning lives in one place. SEARCH_MATCH is accepted here only so a
// standalone '~' modification marker (e.g. after a quoted timestamp) is
// captured into the content; its meaning as a search operator outside
// brackets is unaffected. The SEARCH_MATCH token also lexes the
// 'search_match'/'contains' keyword spellings, which are meaningless
// inside a bracket, so the action rejects those and admits only '~'.
String KeyBracketParameter() :
{
Token word;
String parameter = "";
}
{
(<TIMESTAMP>)?
(LOOKAHEAD(2) (word=<QUOTED_STRING> | word=<SIGNED_INTEGER> | word=<SIGNED_DECIMAL> | word=<NUMERIC> | word=<ALPHANUMERIC> | word=<NON_ALPHANUMERIC_AND_ALPHANUMERIC> | word=<ASTERISK_SUFFIXED_STRING>) { parameter += (parameter.equals("")) ? word.image : " " + word.image; })+
(LOOKAHEAD(2) (word=<QUOTED_STRING> | word=<SIGNED_INTEGER> | word=<SIGNED_DECIMAL> | word=<NUMERIC> | word=<ALPHANUMERIC> | word=<NON_ALPHANUMERIC_AND_ALPHANUMERIC> | word=<ASTERISK_SUFFIXED_STRING> | word=<SEARCH_MATCH>)
{
if(word.kind == GrammarConstants.SEARCH_MATCH && !word.image.equals("~")) {
throw new ParseException("only the '~' modification marker is allowed "
+ "inside a key bracket, not the '" + word.image + "' keyword");
}
parameter += (parameter.equals("")) ? word.image : " " + word.image;
})+
{ return parameter; }
}

Expand Down
69 changes: 52 additions & 17 deletions src/main/java/com/cinchapi/ccl/Parsing.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.cinchapi.ccl.grammar.ConjunctionSymbol;
import com.cinchapi.ccl.grammar.ExpressionSymbol;
import com.cinchapi.ccl.grammar.KeyTokenSymbol;
import com.cinchapi.ccl.grammar.ModificationKeySymbol;
import com.cinchapi.ccl.grammar.NavigationKeySymbol;
import com.cinchapi.ccl.grammar.OperatorSymbol;
import com.cinchapi.ccl.grammar.ParenthesisSymbol;
Expand Down Expand Up @@ -60,34 +61,68 @@ public final class Parsing {
*/
private static final String RANGE_SEPARATOR = "...";

/**
* The literal marker that, when trailing a bracket timestamp, denotes
* an addition-relative binding (for example {@code foo[t1~]}): the
* values whose add occurred at or after the timestamp.
*/
private static final String MODIFICATION_MARKER = "~";

/**
* Interpret the raw {@code content} of a leaf key's bracket parameter
* and pair it with {@code base} as the appropriate parameterized key.
*
* <p>Content containing the range separator {@code ...} produces a
* {@link TemporalRangeKeySymbol} over the half-open interval its
* endpoints describe; either endpoint may be omitted for an
* open-ended range. All other content is a single instant and
* produces a {@link TemporalKeySymbol}, or, when {@code base} is a
* {@link NavigationKeySymbol}, folds the timestamp onto the path's
* last stop.
* <p>Content ending in the modification marker {@code ~} produces a
* {@link ModificationKeySymbol} bound to the values added at or after
* the timestamp ({@code foo[t1~]}). Content containing the range
* separator {@code ...} produces a {@link TemporalRangeKeySymbol} over
* the half-open interval its endpoints describe; either endpoint may
* be omitted for an open-ended range. All other content is a single
* instant and produces a {@link TemporalKeySymbol}, or, when
* {@code base} is a {@link NavigationKeySymbol}, folds the timestamp
* onto the path's last stop.
*
* <p>The leading modification form {@code key[~t]} (added-before) is
* not yet supported and is rejected; it is semantically equivalent to
* an existence-before range and is deferred to that sibling work.
*
* @param base the key the bracket parameter binds to
* @param content the raw bracket content; the leading {@code at} /
* {@code on} / {@code during} keyword, if any, has already
* been consumed by the grammar
* @return the parameterized {@link KeyTokenSymbol}
* @throws IllegalArgumentException if a range binding is applied to a
* navigation key, if both endpoints are omitted, if more
* than one range separator is present, or if an endpoint
* cannot be parsed as a timestamp
* @throws IllegalArgumentException if a range or modification binding
* is applied to a navigation key, if a range omits both
* endpoints or has more than one separator, if the
* unsupported leading {@code ~t} form is used, or if a
* timestamp cannot be parsed
*/
public static KeyTokenSymbol<?> applyKeyBracket(KeyTokenSymbol<?> base,
String content) {
int separator = indexOfRangeSeparator(content, 0);
String trimmed = content.trim();
if(trimmed.startsWith(MODIFICATION_MARKER)) {
throw new IllegalArgumentException(
"the leading modification form key[~t] (added-before) is "
+ "not yet supported; use a single instant key[t] "
+ "or a range key[t1...t2]");
}
if(trimmed.endsWith(MODIFICATION_MARKER)) {
String timestamp = trimmed
.substring(0,
trimmed.length() - MODIFICATION_MARKER.length())
.trim();
Preconditions.checkArgument(
indexOfRangeSeparator(timestamp, 0) < 0,
"a modification marker binds a single instant, not a "
+ "range; '%s' combines '%s' with '%s'",
trimmed, RANGE_SEPARATOR, MODIFICATION_MARKER);
return new ModificationKeySymbol(base,
new TimestampSymbol(NaturalLanguage.parseMicros(timestamp)));
}
int separator = indexOfRangeSeparator(trimmed, 0);
if(separator < 0) {
TimestampSymbol timestamp = new TimestampSymbol(
NaturalLanguage.parseMicros(content.trim()));
NaturalLanguage.parseMicros(trimmed));
if(base instanceof NavigationKeySymbol) {
return NavigationKeySymbol.withTimestampOnLastStop(
(NavigationKeySymbol) base, timestamp);
Expand All @@ -98,13 +133,13 @@ public static KeyTokenSymbol<?> applyKeyBracket(KeyTokenSymbol<?> base,
}
else {
Preconditions.checkArgument(
indexOfRangeSeparator(content,
indexOfRangeSeparator(trimmed,
separator + RANGE_SEPARATOR.length()) < 0,
"a temporal range has exactly two endpoints separated by "
+ "a single '%s'; '%s' has more than one",
RANGE_SEPARATOR, content);
String startText = content.substring(0, separator).trim();
String endText = content
RANGE_SEPARATOR, trimmed);
String startText = trimmed.substring(0, separator).trim();
String endText = trimmed
.substring(separator + RANGE_SEPARATOR.length()).trim();
TimestampSymbol start = startText.isEmpty() ? null
: new TimestampSymbol(
Expand Down
Loading