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
28 changes: 28 additions & 0 deletions CCL_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,34 @@ name["last week"] = "Jeff"
score[1700000000] >= 90
```

#### Temporal range (leaf keys)

A leaf key can bind to a half-open time interval `[start, end)` rather
than a single instant, using the `...` range separator. The interval is
start-inclusive and end-exclusive, matching the `BETWEEN` convention.
Either endpoint may be omitted for an open-ended range:

```
lock[1718380800000000...1718384400000000] = "claimed" -- closed
status[...1700000000] = "active" -- open start
lock[1700000000...] = "claimed" -- open end
```

A range must pin at least one endpoint (`foo[...]` is rejected), carries
exactly one `...` separator (`foo[t1...t2...t3]` is rejected), and a
closed range may not start after it ends (`foo[t2...t1]` with `t2 > t1`
is rejected); `t1 == t2` is an empty interval and is accepted. A key
still carries at most one bracket annotation, so a range cannot follow
another bracket (`foo[t1][t2...t3]` is rejected).

The canonical serialization renders both endpoints as microseconds and
omits any keyword, for example `lock[1700000000...]`.

Range bindings are currently supported only on flat leaf keys. Range
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.

#### Per-stop navigation

Navigation keys carry one bracket per stop. Each annotation binds only
Expand Down
29 changes: 13 additions & 16 deletions grammar/grammar.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.cinchapi.ccl.grammar.NavigationKeySymbol;
import com.cinchapi.ccl.grammar.KeyTokenSymbol;
import com.cinchapi.ccl.grammar.OperatorSymbol;
import com.cinchapi.ccl.grammar.OrderComponentSymbol;
import com.cinchapi.ccl.grammar.TemporalKeySymbol;
import com.cinchapi.ccl.grammar.TimestampSymbol;
import com.cinchapi.ccl.grammar.ValueSymbol;
import com.cinchapi.ccl.grammar.PageSymbol;
Expand Down Expand Up @@ -545,7 +544,7 @@ KeyTokenSymbol Key() :
FunctionKeySymbol function;
Token key;
KeyTokenSymbol result;
TimestampSymbol ts = null;
String parameter = null;
}
{
(
Expand All @@ -560,16 +559,12 @@ KeyTokenSymbol Key() :
(key=<PERIOD_SEPARATED_STRING> | key=<ASTERISK_SUFFIXED_STRING>)
{ result = new NavigationKeySymbol(key.image); }
)
( LOOKAHEAD(2) <OPEN_BRACKET> ts=BracketedTimestamp() <CLOSE_BRACKET> )?
( LOOKAHEAD(2) <OPEN_BRACKET> parameter=KeyBracketParameter() <CLOSE_BRACKET> )?
{
if(ts == null) {
if(parameter == null) {
return result;
}
if(result instanceof NavigationKeySymbol) {
return NavigationKeySymbol.withTimestampOnLastStop(
(NavigationKeySymbol) result, ts);
}
return new TemporalKeySymbol(result, ts);
return Parsing.applyKeyBracket(result, parameter);
}
}

Expand Down Expand Up @@ -765,18 +760,20 @@ TimestampSymbol Timestamp() :
{ return new TimestampSymbol(NaturalLanguage.parseMicros(timestamp)); }
}

// Bracket-form timestamp: the brackets themselves are the timestamp pivot,
// so the leading at/on/during keyword is optional. The keyword form is
// accepted for backward compatibility.
TimestampSymbol BracketedTimestamp() :
// 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.
String KeyBracketParameter() :
{
Token word;
String timestamp = "";
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>) { timestamp += (timestamp.equals("")) ? word.image : " " + word.image; })+
{ return new TimestampSymbol(NaturalLanguage.parseMicros(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; })+
{ return parameter; }
}

TimestampSymbol TimestampReadCommand() :
Expand Down
100 changes: 100 additions & 0 deletions src/main/java/com/cinchapi/ccl/Parsing.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
import com.cinchapi.ccl.grammar.ConjunctionSymbol;
import com.cinchapi.ccl.grammar.ExpressionSymbol;
import com.cinchapi.ccl.grammar.KeyTokenSymbol;
import com.cinchapi.ccl.grammar.NavigationKeySymbol;
import com.cinchapi.ccl.grammar.OperatorSymbol;
import com.cinchapi.ccl.grammar.ParenthesisSymbol;
import com.cinchapi.ccl.grammar.PostfixNotationSymbol;
import com.cinchapi.ccl.grammar.ScopeEndSymbol;
import com.cinchapi.ccl.grammar.ScopeSymbol;
import com.cinchapi.ccl.grammar.TemporalKeySymbol;
import com.cinchapi.ccl.grammar.TemporalRangeKeySymbol;
import com.cinchapi.ccl.grammar.TimestampSymbol;
import com.cinchapi.ccl.grammar.Symbol;
import com.cinchapi.ccl.grammar.ValueTokenSymbol;
import com.cinchapi.ccl.util.NaturalLanguage;
import com.cinchapi.common.base.AnyStrings;
import com.cinchapi.common.base.Array;
import com.google.common.base.Preconditions;
Expand All @@ -50,6 +54,102 @@
*/
public final class Parsing {

/**
* The literal token that separates the two endpoints of a temporal
* range bracket binding (for example {@code foo[t1...t2]}).
*/
private static final String RANGE_SEPARATOR = "...";

/**
* 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.
*
* @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
*/
public static KeyTokenSymbol<?> applyKeyBracket(KeyTokenSymbol<?> base,
String content) {
int separator = indexOfRangeSeparator(content, 0);
if(separator < 0) {
TimestampSymbol timestamp = new TimestampSymbol(
NaturalLanguage.parseMicros(content.trim()));
if(base instanceof NavigationKeySymbol) {
return NavigationKeySymbol.withTimestampOnLastStop(
(NavigationKeySymbol) base, timestamp);
}
else {
return new TemporalKeySymbol(base, timestamp);
}
}
else {
Preconditions.checkArgument(
indexOfRangeSeparator(content,
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
.substring(separator + RANGE_SEPARATOR.length()).trim();
TimestampSymbol start = startText.isEmpty() ? null
: new TimestampSymbol(
NaturalLanguage.parseMicros(startText));
TimestampSymbol end = endText.isEmpty() ? null
: new TimestampSymbol(
NaturalLanguage.parseMicros(endText));
return new TemporalRangeKeySymbol(base, start, end);
}
}

/**
* Return the index of the next {@link #RANGE_SEPARATOR} in
* {@code content} at or after {@code from} that lies <em>outside</em>
* any quoted endpoint, or {@code -1} if there is none.
*
* <p>A quoted timestamp phrase may itself contain {@code ...} (for
* example {@code foo["a...b"]}); such an occurrence is part of the
* endpoint, not a range separator, so it is skipped. This keeps the
* single-vs-range decision and the endpoint split from being fooled
* by punctuation inside a quoted phrase.
*
* @param content the raw bracket content
* @param from the index to begin scanning from
* @return the index of the next unquoted separator, or {@code -1}
*/
private static int indexOfRangeSeparator(String content, int from) {
char quote = 0;
for (int i = from; i < content.length(); ++i) {
char c = content.charAt(i);
if(quote != 0) {
if(c == quote) {
quote = 0;
}
}
else if(c == '"' || c == '\'' || c == '`') {
quote = c;
}
else if(content.startsWith(RANGE_SEPARATOR, i)) {
return i;
}
}
return -1;
}

/**
* Go through a list of symbols and group the expressions together in a
* {@link ExpressionSymbol} object.
Expand Down
Loading