From f5d0c2746df36ff0ffa67a0987e7395e15d3f767 Mon Sep 17 00:00:00 2001 From: Javier Lores Date: Wed, 24 Jun 2026 12:13:06 -0400 Subject: [PATCH 1/2] feat: [GH-75] Modification-timestamp marker (foo[t1~]) for added-at-or-after binding Add an addition-relative key-bracket binding on flat leaf keys: foo[t1~] binds the key to the values whose ADD occurred in the half-open window [t1, now) -- start-inclusive at t1, end-exclusive at the present moment. This is distinct from the existing forms: foo[t] binds the value present AT the instant regardless of add time, and foo[t1...t2] (GH-74) binds values present DURING an interval regardless of add time. foo[t1~] keys strictly off the add event, so a value added before t1 is excluded even if still present. Motivating use: stale-lock detection (locked[~] = null). Grammar/AST/parse only; server-side evaluation is a separate concourse change. The canonical toString() renders key[~], a re-parseable cross-repo contract. - Accept a standalone '~' (SEARCH_MATCH) inside KeyBracketParameter so a detached marker after a quoted/natural-language timestamp is captured; '~' outside brackets is unaffected. - Detect the marker in the centralized Parsing.applyKeyBracket (added by GH-74): trailing '~' -> ModificationKeySymbol; reject the leading added-before form key[~t] (not yet supported); reject a range combined with a marker (key[t1...t2~]). - Add ModificationKeySymbol (sibling to TemporalKeySymbol): rejects navigation and already-parameterized keys, isParameterized() == true so writes and audit/chronicle/diff reject it for free. - Tests: node unit tests, matrix (numeric, quoted/NL, keyword equivalence, before-marker / range-with-marker / double-bracket rejection, round-trip), command accept (select/get/find) and reject (add/audit). - Update CCL_REFERENCE.md section 8.4. Markers on navigation stops and scope prefixes remain out of scope (leaf-only first cut, matching GH-74). Part of the connector data-sync locking initiative; concourse evaluation is cinchapi/concourse#790. --- CCL_REFERENCE.md | 35 + grammar/grammar.jjt | 10 +- src/main/java/com/cinchapi/ccl/Parsing.java | 57 +- .../com/cinchapi/ccl/generated/Grammar.java | 603 +++++++++--------- .../ccl/grammar/ModificationKeySymbol.java | 128 ++++ .../ccl/BracketTimestampCommandTest.java | 57 ++ .../ccl/BracketTimestampMatrixTest.java | 137 ++++ .../grammar/ModificationKeySymbolTest.java | 134 ++++ 8 files changed, 851 insertions(+), 310 deletions(-) create mode 100644 src/main/java/com/cinchapi/ccl/grammar/ModificationKeySymbol.java create mode 100644 src/test/java/com/cinchapi/ccl/grammar/ModificationKeySymbolTest.java diff --git a/CCL_REFERENCE.md b/CCL_REFERENCE.md index 62034cd..21f591f 100644 --- a/CCL_REFERENCE.md +++ b/CCL_REFERENCE.md @@ -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[~]` 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 diff --git a/grammar/grammar.jjt b/grammar/grammar.jjt index a3a08ec..f13b632 100644 --- a/grammar/grammar.jjt +++ b/grammar/grammar.jjt @@ -763,8 +763,12 @@ 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. String KeyBracketParameter() : { Token word; @@ -772,7 +776,7 @@ String KeyBracketParameter() : } { ()? - (LOOKAHEAD(2) (word= | word= | word= | word= | word= | word= | word=) { parameter += (parameter.equals("")) ? word.image : " " + word.image; })+ + (LOOKAHEAD(2) (word= | word= | word= | word= | word= | word= | word= | word=) { parameter += (parameter.equals("")) ? word.image : " " + word.image; })+ { return parameter; } } diff --git a/src/main/java/com/cinchapi/ccl/Parsing.java b/src/main/java/com/cinchapi/ccl/Parsing.java index 87e74c2..88ed20e 100644 --- a/src/main/java/com/cinchapi/ccl/Parsing.java +++ b/src/main/java/com/cinchapi/ccl/Parsing.java @@ -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; @@ -60,30 +61,64 @@ 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. * - *

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. + *

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. + * + *

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) { + 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'", + content, RANGE_SEPARATOR, MODIFICATION_MARKER); + return new ModificationKeySymbol(base, + new TimestampSymbol(NaturalLanguage.parseMicros(timestamp))); + } int separator = indexOfRangeSeparator(content, 0); if(separator < 0) { TimestampSymbol timestamp = new TimestampSymbol( diff --git a/src/main/java/com/cinchapi/ccl/generated/Grammar.java b/src/main/java/com/cinchapi/ccl/generated/Grammar.java index fa2d98b..7e4679a 100644 --- a/src/main/java/com/cinchapi/ccl/generated/Grammar.java +++ b/src/main/java/com/cinchapi/ccl/generated/Grammar.java @@ -1621,8 +1621,12 @@ final public void RelationalExpressionNoTimestamp() throws ParseException {/*@bg // 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. final public String KeyBracketParameter() throws ParseException {Token word; String parameter = ""; switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { @@ -1665,6 +1669,10 @@ final public void RelationalExpressionNoTimestamp() throws ParseException {/*@bg word = jj_consume_token(ASTERISK_SUFFIXED_STRING); break; } + case SEARCH_MATCH:{ + word = jj_consume_token(SEARCH_MATCH); + break; + } default: jj_la1[40] = jj_gen; jj_consume_token(-1); @@ -5615,24 +5623,6 @@ private boolean jj_2_71(int xla) finally { jj_save(70, xla); } } - private boolean jj_3_29() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_40()) return true; - } - return false; - } - - private boolean jj_3_54() - { - if (jj_3R_55()) return true; - if (jj_scan_token(QUOTED_STRING)) return true; - return false; - } - private boolean jj_3R_22() { if (jj_3R_67()) return true; @@ -5671,34 +5661,34 @@ private boolean jj_3_32() return false; } - private boolean jj_3R_23() + private boolean jj_3R_39() { - if (jj_3R_69()) return true; + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; return false; } - private boolean jj_3R_29() + private boolean jj_3R_23() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(82)) { - jj_scanpos = xsp; - if (jj_scan_token(83)) return true; - } + if (jj_3R_69()) return true; return false; } - private boolean jj_3R_39() + private boolean jj_3_31() { + if (jj_scan_token(OPEN_BRACKET)) return true; if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; return false; } - private boolean jj_3_31() + private boolean jj_3R_29() { - if (jj_scan_token(OPEN_BRACKET)) return true; - if (jj_scan_token(NUMERIC)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(82)) { + jj_scanpos = xsp; + if (jj_scan_token(83)) return true; + } return false; } @@ -5717,12 +5707,6 @@ private boolean jj_3_71() return false; } - private boolean jj_3R_108() - { - if (jj_scan_token(BINARY_OPERATOR)) return true; - return false; - } - private boolean jj_3_70() { Token xsp; @@ -5742,6 +5726,12 @@ private boolean jj_3_70() return false; } + private boolean jj_3R_108() + { + if (jj_scan_token(BINARY_OPERATOR)) return true; + return false; + } + private boolean jj_3R_65() { if (jj_scan_token(NUMERIC)) return true; @@ -5749,27 +5739,34 @@ private boolean jj_3R_65() return false; } - private boolean jj_3R_106() + private boolean jj_3_28() { Token xsp; xsp = jj_scanpos; - if (jj_scan_token(60)) { - jj_scanpos = xsp; - if (jj_scan_token(62)) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_scan_token(63)) return true; - } + if (jj_3R_39()) return true; } return false; } - private boolean jj_3_28() + private boolean jj_3R_38() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + + private boolean jj_3R_106() { Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_scan_token(60)) { jj_scanpos = xsp; - if (jj_3R_39()) return true; + if (jj_scan_token(62)) { + jj_scanpos = xsp; + if (jj_scan_token(63)) return true; + } } return false; } @@ -5787,13 +5784,6 @@ private boolean jj_3_2() return false; } - private boolean jj_3R_38() - { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - private boolean jj_3R_104() { Token xsp; @@ -5811,12 +5801,6 @@ private boolean jj_3R_104() return false; } - private boolean jj_3R_102() - { - if (jj_scan_token(LINKS_TO)) return true; - return false; - } - private boolean jj_3R_54() { if (jj_scan_token(NUMERIC)) return true; @@ -5849,24 +5833,16 @@ private boolean jj_3_53() return false; } - private boolean jj_3_1() + private boolean jj_3R_37() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(56)) jj_scanpos = xsp; - if (jj_3R_22()) return true; - xsp = jj_scanpos; - if (jj_3R_23()) jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_24()) jj_scanpos = xsp; - if (jj_scan_token(102)) return true; + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; return false; } - private boolean jj_3R_37() + private boolean jj_3R_102() { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; + if (jj_scan_token(LINKS_TO)) return true; return false; } @@ -5881,9 +5857,17 @@ private boolean jj_3_27() return false; } - private boolean jj_3R_116() + private boolean jj_3_1() { - if (jj_scan_token(QUOTED_STRING)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(56)) jj_scanpos = xsp; + if (jj_3R_22()) return true; + xsp = jj_scanpos; + if (jj_3R_23()) jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_24()) jj_scanpos = xsp; + if (jj_scan_token(102)) return true; return false; } @@ -5894,6 +5878,12 @@ private boolean jj_3_52() return false; } + private boolean jj_3R_116() + { + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + private boolean jj_3_47() { Token xsp; @@ -6067,24 +6057,24 @@ private boolean jj_3_16() return false; } - private boolean jj_3R_100() + private boolean jj_3_66() { Token xsp; - if (jj_3_16()) return true; - while (true) { - xsp = jj_scanpos; - if (jj_3_16()) { jj_scanpos = xsp; break; } + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_63()) return true; } return false; } - private boolean jj_3_66() + private boolean jj_3R_100() { Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_63()) return true; + if (jj_3_16()) return true; + while (true) { + xsp = jj_scanpos; + if (jj_3_16()) { jj_scanpos = xsp; break; } } return false; } @@ -6142,16 +6132,16 @@ private boolean jj_3_65() return false; } - private boolean jj_3R_112() + private boolean jj_3_48() { - if (jj_scan_token(QUOTED_STRING)) return true; + if (jj_3R_42()) return true; + if (jj_scan_token(WHERE)) return true; return false; } - private boolean jj_3_48() + private boolean jj_3R_112() { - if (jj_3R_42()) return true; - if (jj_scan_token(WHERE)) return true; + if (jj_scan_token(QUOTED_STRING)) return true; return false; } @@ -6300,15 +6290,15 @@ private boolean jj_3_39() return false; } - private boolean jj_3R_103() + private boolean jj_3R_36() { - if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(OPEN_BRACKET)) return true; return false; } - private boolean jj_3R_36() + private boolean jj_3R_103() { - if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_scan_token(NUMERIC)) return true; return false; } @@ -6329,12 +6319,6 @@ private boolean jj_3R_35() return false; } - private boolean jj_3R_114() - { - if (jj_scan_token(QUOTED_STRING)) return true; - return false; - } - private boolean jj_3R_46() { if (jj_scan_token(NUMERIC)) return true; @@ -6381,6 +6365,12 @@ private boolean jj_3R_34() return false; } + private boolean jj_3R_114() + { + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + private boolean jj_3R_33() { if (jj_scan_token(CLOSE_PARENTHESES)) return true; @@ -6461,17 +6451,6 @@ private boolean jj_3_12() return false; } - private boolean jj_3R_113() - { - Token xsp; - if (jj_3_12()) return true; - while (true) { - xsp = jj_scanpos; - if (jj_3_12()) { jj_scanpos = xsp; break; } - } - return false; - } - private boolean jj_3_24() { Token xsp; @@ -6497,26 +6476,17 @@ private boolean jj_3_24() return false; } - private boolean jj_3R_107() + private boolean jj_3R_113() { Token xsp; - xsp = jj_scanpos; - if (jj_3_13()) { - jj_scanpos = xsp; - if (jj_3R_113()) { - jj_scanpos = xsp; - if (jj_3R_114()) return true; - } + if (jj_3_12()) return true; + while (true) { + xsp = jj_scanpos; + if (jj_3_12()) { jj_scanpos = xsp; break; } } return false; } - private boolean jj_3_13() - { - if (jj_3R_32()) return true; - return false; - } - private boolean jj_3_37() { Token xsp; @@ -6535,6 +6505,26 @@ private boolean jj_3R_32() return false; } + private boolean jj_3R_107() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3_13()) { + jj_scanpos = xsp; + if (jj_3R_113()) { + jj_scanpos = xsp; + if (jj_3R_114()) return true; + } + } + return false; + } + + private boolean jj_3_13() + { + if (jj_3R_32()) return true; + return false; + } + private boolean jj_3R_48() { if (jj_scan_token(NUMERIC)) return true; @@ -6597,14 +6587,6 @@ private boolean jj_3_42() return false; } - private boolean jj_3_11() - { - if (jj_scan_token(OPEN_BRACKET)) return true; - if (jj_3R_31()) return true; - if (jj_scan_token(CLOSE_BRACKET)) return true; - return false; - } - private boolean jj_3_63() { if (jj_3R_43()) return true; @@ -6636,20 +6618,35 @@ private boolean jj_3_38() return false; } - private boolean jj_3R_76() + private boolean jj_3_11() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(93)) { - jj_scanpos = xsp; - if (jj_scan_token(96)) return true; - } + if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_3R_31()) return true; + if (jj_scan_token(CLOSE_BRACKET)) return true; return false; } - private boolean jj_3R_75() + private boolean jj_3_41() { - Token xsp; + if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_scan_token(NUMERIC)) return true; + return false; + } + + private boolean jj_3R_76() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(93)) { + jj_scanpos = xsp; + if (jj_scan_token(96)) return true; + } + return false; + } + + private boolean jj_3R_75() + { + Token xsp; xsp = jj_scanpos; if (jj_scan_token(57)) { jj_scanpos = xsp; @@ -6667,10 +6664,9 @@ private boolean jj_3R_75() return false; } - private boolean jj_3_41() + private boolean jj_3_23() { - if (jj_scan_token(OPEN_BRACKET)) return true; - if (jj_scan_token(NUMERIC)) return true; + if (jj_3R_30()) return true; return false; } @@ -6680,9 +6676,21 @@ private boolean jj_3_10() return false; } - private boolean jj_3_23() + private boolean jj_3_62() { - if (jj_3R_30()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_60()) return true; + } + return false; + } + + private boolean jj_3R_59() + { + if (jj_3R_43()) return true; + if (jj_scan_token(COMMA)) return true; return false; } @@ -6702,24 +6710,6 @@ private boolean jj_3R_43() return false; } - private boolean jj_3_62() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_60()) return true; - } - return false; - } - - private boolean jj_3R_59() - { - if (jj_3R_43()) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - private boolean jj_3R_72() { Token xsp; @@ -6751,24 +6741,24 @@ private boolean jj_3R_71() return false; } - private boolean jj_3R_28() + private boolean jj_3_61() { Token xsp; xsp = jj_scanpos; - if (jj_3R_71()) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_3R_72()) return true; + if (jj_3R_59()) return true; } return false; } - private boolean jj_3_61() + private boolean jj_3R_28() { Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_3R_71()) { jj_scanpos = xsp; - if (jj_3R_59()) return true; + if (jj_3R_72()) return true; } return false; } @@ -6786,13 +6776,6 @@ private boolean jj_3_6() return false; } - private boolean jj_3_9() - { - if (jj_3R_28()) return true; - if (jj_3R_29()) return true; - return false; - } - private boolean jj_3_60() { Token xsp; @@ -6804,6 +6787,13 @@ private boolean jj_3_60() return false; } + private boolean jj_3_9() + { + if (jj_3R_28()) return true; + if (jj_3R_29()) return true; + return false; + } + private boolean jj_3R_45() { if (jj_scan_token(NUMERIC)) return true; @@ -6823,26 +6813,26 @@ private boolean jj_3_4() return false; } - private boolean jj_3_3() + private boolean jj_3_36() { - if (jj_3R_27()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_45()) return true; + } return false; } - private boolean jj_3_5() + private boolean jj_3_3() { if (jj_3R_27()) return true; return false; } - private boolean jj_3_36() + private boolean jj_3_5() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_45()) return true; - } + if (jj_3R_27()) return true; return false; } @@ -6931,14 +6921,6 @@ private boolean jj_3R_81() return false; } - private boolean jj_3R_98() - { - if (jj_3R_108()) return true; - if (jj_3R_109()) return true; - if (jj_3R_109()) return true; - return false; - } - private boolean jj_3R_80() { if (jj_3R_89()) return true; @@ -6956,24 +6938,32 @@ private boolean jj_3_57() return false; } - private boolean jj_3R_97() + private boolean jj_3R_70() { - if (jj_3R_106()) return true; - if (jj_3R_107()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3_5()) jj_scanpos = xsp; + if (jj_3R_80()) { + jj_scanpos = xsp; + if (jj_3R_81()) return true; + } return false; } - private boolean jj_3R_70() + private boolean jj_3R_98() { + if (jj_3R_108()) return true; + if (jj_3R_109()) return true; + if (jj_3R_109()) return true; + return false; + } + + private boolean jj_3R_97() + { + if (jj_3R_106()) return true; + if (jj_3R_107()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_80()) { - jj_scanpos = xsp; - if (jj_3R_81()) return true; - } + if (jj_3_5()) jj_scanpos = xsp; return false; } @@ -6997,20 +6987,25 @@ private boolean jj_3R_95() return false; } - private boolean jj_3R_93() + private boolean jj_3_21() { - if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_95()) { + if (jj_scan_token(85)) { jj_scanpos = xsp; - if (jj_3R_96()) { + if (jj_scan_token(90)) { jj_scanpos = xsp; - if (jj_3R_97()) { + if (jj_scan_token(91)) { jj_scanpos = xsp; - if (jj_3R_98()) { + if (jj_scan_token(88)) { jj_scanpos = xsp; - if (jj_3R_99()) return true; + if (jj_scan_token(92)) { + jj_scanpos = xsp; + if (jj_scan_token(97)) { + jj_scanpos = xsp; + if (jj_scan_token(96)) return true; + } + } } } } @@ -7018,33 +7013,20 @@ private boolean jj_3R_93() return false; } - private boolean jj_3_8() - { - if (jj_3R_28()) return true; - if (jj_3R_29()) return true; - if (jj_3R_94()) return true; - return false; - } - - private boolean jj_3_21() + private boolean jj_3R_93() { + if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(85)) { - jj_scanpos = xsp; - if (jj_scan_token(90)) { - jj_scanpos = xsp; - if (jj_scan_token(91)) { + if (jj_3R_95()) { jj_scanpos = xsp; - if (jj_scan_token(88)) { + if (jj_3R_96()) { jj_scanpos = xsp; - if (jj_scan_token(92)) { + if (jj_3R_97()) { jj_scanpos = xsp; - if (jj_scan_token(97)) { + if (jj_3R_98()) { jj_scanpos = xsp; - if (jj_scan_token(96)) return true; - } - } + if (jj_3R_99()) return true; } } } @@ -7083,6 +7065,14 @@ private boolean jj_3R_55() return false; } + private boolean jj_3_8() + { + if (jj_3R_28()) return true; + if (jj_3R_29()) return true; + if (jj_3R_94()) return true; + return false; + } + private boolean jj_3R_92() { Token xsp; @@ -7173,18 +7163,6 @@ private boolean jj_3R_41() return false; } - private boolean jj_3R_79() - { - if (jj_scan_token(CONJUNCTION)) return true; - return false; - } - - private boolean jj_3R_68() - { - if (jj_scan_token(DISJUNCTION)) return true; - return false; - } - private boolean jj_3R_42() { Token xsp; @@ -7196,10 +7174,9 @@ private boolean jj_3R_42() return false; } - private boolean jj_3R_91() + private boolean jj_3R_79() { - if (jj_scan_token(NAVIGATION_SCOPE_OPEN)) return true; - if (jj_3R_22()) return true; + if (jj_scan_token(CONJUNCTION)) return true; return false; } @@ -7210,12 +7187,25 @@ private boolean jj_3R_66() return false; } + private boolean jj_3R_68() + { + if (jj_scan_token(DISJUNCTION)) return true; + return false; + } + private boolean jj_3_56() { if (jj_scan_token(NUMERIC)) return true; return false; } + private boolean jj_3R_91() + { + if (jj_scan_token(NAVIGATION_SCOPE_OPEN)) return true; + if (jj_3R_22()) return true; + return false; + } + private boolean jj_3_55() { Token xsp; @@ -7233,18 +7223,6 @@ private boolean jj_3R_85() return false; } - private boolean jj_3R_26() - { - if (jj_3R_70()) return true; - return false; - } - - private boolean jj_3R_88() - { - if (jj_3R_92()) return true; - return false; - } - private boolean jj_3R_40() { if (jj_scan_token(NUMERIC)) return true; @@ -7259,13 +7237,6 @@ private boolean jj_3_34() return false; } - private boolean jj_3R_87() - { - if (jj_scan_token(OPEN_PARENTHESES)) return true; - if (jj_3R_22()) return true; - return false; - } - private boolean jj_3_20() { Token xsp; @@ -7282,13 +7253,22 @@ private boolean jj_3_20() jj_scanpos = xsp; if (jj_scan_token(97)) { jj_scanpos = xsp; - if (jj_scan_token(96)) return true; + if (jj_scan_token(96)) { + jj_scanpos = xsp; + if (jj_scan_token(82)) return true; } } } } } } + } + return false; + } + + private boolean jj_3R_26() + { + if (jj_3R_70()) return true; return false; } @@ -7298,6 +7278,43 @@ private boolean jj_3R_84() return false; } + private boolean jj_3R_88() + { + if (jj_3R_92()) return true; + return false; + } + + private boolean jj_3R_31() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(55)) jj_scanpos = xsp; + if (jj_3_20()) return true; + while (true) { + xsp = jj_scanpos; + if (jj_3_20()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3_30() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_41()) return true; + } + return false; + } + + private boolean jj_3R_87() + { + if (jj_scan_token(OPEN_PARENTHESES)) return true; + if (jj_3R_22()) return true; + return false; + } + private boolean jj_3R_78() { Token xsp; @@ -7344,37 +7361,28 @@ private boolean jj_3_19() return false; } - private boolean jj_3R_31() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(55)) jj_scanpos = xsp; - if (jj_3_20()) return true; - while (true) { - xsp = jj_scanpos; - if (jj_3_20()) { jj_scanpos = xsp; break; } - } - return false; - } - - private boolean jj_3_30() + private boolean jj_3R_77() { Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_3R_84()) { jj_scanpos = xsp; - if (jj_3R_41()) return true; + if (jj_3R_85()) return true; } return false; } - private boolean jj_3R_77() + private boolean jj_3_33() { + if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_84()) { + if (jj_scan_token(40)) { jj_scanpos = xsp; - if (jj_3R_85()) return true; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(39)) return true; + } } return false; } @@ -7396,21 +7404,24 @@ private boolean jj_3R_67() return false; } - private boolean jj_3_33() + private boolean jj_3_29() { - if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(40)) { - jj_scanpos = xsp; - if (jj_scan_token(37)) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } + if (jj_3R_40()) return true; } return false; } + private boolean jj_3_54() + { + if (jj_3R_55()) return true; + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + private boolean jj_3R_24() { if (jj_3R_70()) return true; @@ -7446,7 +7457,7 @@ private static void jj_la1_init_1() { jj_la1_1 = new int[] {0x1000000,0x0,0x0,0x1f,0x0,0x1000000,0x0,0x0,0x1f,0x0,0x8000000,0x4000000,0x2080000,0xf0000000,0x2000000,0x8000000,0x4000000,0x2080000,0xf0000000,0x2000000,0x2000000,0x0,0x2000000,0x2000000,0x0,0x2000000,0x0,0x0,0x0,0x0,0x80000,0x0,0x0,0x0,0x0,0x0,0xd0000000,0x0,0x0,0x800000,0x0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xc2000000,0x800000,0xc0000000,0x0,0x0,0x0,0x0,0x100000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x200000,0x0,0x100000,0x0,0x1f,0x0,0x2a0,0x0,0x2a0,0xa0,0x0,0x1a0,0x0,0x1a0,0x0,0x1a0,0x0,0x0,0xa0,0xa0,0x2e0,0x0,0x2e0,0x0,0x0,0x0,0x800400,0x1a0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x800400,0x0,0x0,0x1000000,0x1a0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x800400,0x0,0x0,0x1000000,0x800400,0x0,0x0,0x800400,0x800400,0x1000000,0x0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x1a0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x1a0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x800400,0x0,0x0,0x1a0,0x800400,0x0,0x0,0x800400,0x0,0x0,0x0,0x800400,0x800400,0x800400,0x10001a0,0x0,0x800400,0x0,0x0,0x800400,0x800400,0x800400,0xa0,0x800400,0x2000000,0x800400,0xa0,0x200,0x100,0xa0,0x200,0x200,0x200,0x2000000,0x0,0x4000,0x800400,0x2000000,0x800400,0x1a0,0x800400,0x800400,0x0,0x10001a0,0xa0,0xa0,0x200,0x100,0x800400,0x800400,0x0,0x1,0xa0,0x0,0xa0,0x0,0x2000000,0x1c,0x0,0x0,0x1a0,0x800400,0x1a0,0x800400,0x800400,0x800400,0x1000000,0x0,0x0,0x200000,0x0,0x0,0x200000,0x0,0x0,0x2200000,}; } private static void jj_la1_init_2() { - jj_la1_2 = new int[] {0x0,0x8000,0x7000,0x0,0x3d00f000,0x0,0x8000,0x7000,0x0,0x3d00f000,0x0,0x0,0x7d000000,0xc07c0,0x3d000000,0x0,0x0,0x7d000000,0xc07c0,0x3d000000,0x1d000000,0x20000000,0x3d000000,0x1d000000,0x20000000,0x3d000000,0x3d000000,0x3d200000,0x3d000000,0x3d000000,0x3d200000,0x1d000000,0x1d200000,0x3d000000,0x3d200000,0x780,0x0,0xc0000,0x1d200000,0x0,0x1d200000,0x0,0x1d200000,0x1d200000,0x0,0x1d200000,0x4000,0x3000,0x7000,0x3000,0x2000000,0x30000,0x3d000000,0x0,0x0,0x30000,0x10000000,0x3d000000,0x3c000000,0x2000000,0x3c000000,0x5000000,0x2000000,0x5000000,0x5000000,0x2000000,0x5000000,0x5000000,0x3c000000,0x2000000,0x3c000000,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x1000000,0x0,0x1000000,0x0,0x1000000,0x1000000,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x0,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x0,0x0,0x1000000,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x0,0x0,0x0,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x0,0x0,0x0,0x3d000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3d000000,0x1000000,0x0,0x0,0x3d000000,0x0,0x0,0x0,0x0,0x1000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x1000000,0x3d000000,0x0,0x1000000,0x1000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2000000,0x2000000,0x3d200000,0x2000000,0x2000000,0x1000000,0x2000000,0x2000000,0x3d000000,}; + jj_la1_2 = new int[] {0x0,0x8000,0x7000,0x0,0x3d00f000,0x0,0x8000,0x7000,0x0,0x3d00f000,0x0,0x0,0x7d000000,0xc07c0,0x3d000000,0x0,0x0,0x7d000000,0xc07c0,0x3d000000,0x1d000000,0x20000000,0x3d000000,0x1d000000,0x20000000,0x3d000000,0x3d000000,0x3d200000,0x3d000000,0x3d000000,0x3d200000,0x1d000000,0x1d200000,0x3d000000,0x3d200000,0x780,0x0,0xc0000,0x1d200000,0x0,0x1d240000,0x0,0x1d200000,0x1d200000,0x0,0x1d200000,0x4000,0x3000,0x7000,0x3000,0x2000000,0x30000,0x3d000000,0x0,0x0,0x30000,0x10000000,0x3d000000,0x3c000000,0x2000000,0x3c000000,0x5000000,0x2000000,0x5000000,0x5000000,0x2000000,0x5000000,0x5000000,0x3c000000,0x2000000,0x3c000000,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x1000000,0x0,0x1000000,0x0,0x1000000,0x1000000,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x0,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x0,0x0,0x1000000,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x8000,0x7000,0x0,0x8000,0x7000,0x1000000,0x0,0x0,0x0,0x0,0x1000000,0x0,0x8000,0x7000,0x0,0x0,0x0,0x0,0x0,0x3d000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3d000000,0x1000000,0x0,0x0,0x3d000000,0x0,0x0,0x0,0x0,0x1000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1000000,0x0,0x0,0x1000000,0x0,0x1000000,0x3d000000,0x0,0x1000000,0x1000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2000000,0x2000000,0x3d200000,0x2000000,0x2000000,0x1000000,0x2000000,0x2000000,0x3d000000,}; } private static void jj_la1_init_3() { jj_la1_3 = new int[] {0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x1,0x0,0x1,0x0,0x0,0x1,0x0,0x1,0x0,0x1,0x1,0x0,0x1,0x1,0x3,0x3,0x183,0x183,0x183,0x3,0x3,0x3,0x3,0x0,0x0,0x0,0x3,0x0,0x3,0x0,0x3,0x3,0x0,0x3,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x1,0x1,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x0,0x0,0x0,0x0,0x0,0x1,}; diff --git a/src/main/java/com/cinchapi/ccl/grammar/ModificationKeySymbol.java b/src/main/java/com/cinchapi/ccl/grammar/ModificationKeySymbol.java new file mode 100644 index 0000000..178749c --- /dev/null +++ b/src/main/java/com/cinchapi/ccl/grammar/ModificationKeySymbol.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2013-2026 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.ccl.grammar; + +import java.util.Objects; + +import com.google.common.base.Preconditions; + +/** + * A {@link ModificationKeySymbol} pairs a {@link KeyTokenSymbol} with a + * timestamp that marks an addition-relative binding: the key binds to the + * values whose add occurred in the half-open interval + * {@code [timestamp, now)}, start-inclusive at the timestamp and + * end-exclusive at the present moment ({@code key[~]}). + * + *

This differs from a {@link TemporalKeySymbol}, which binds the single + * value present at the instant regardless of when it was added, + * and from a {@link TemporalRangeKeySymbol}, which binds the values + * present during an interval regardless of their add time. A + * value added before the timestamp is not in range here even if it is + * still present. + * + *

This symbol records the endpoint only; it does not decide what a + * read returns. That evaluation lives server-side and is out of scope for + * the grammar layer. + * + *

As with {@link TemporalKeySymbol}, a {@link NavigationKeySymbol} is + * never wrapped and an already-parameterized key is rejected, enforcing + * the "at most one bracket-timestamp parameter per key" invariant the + * rest of the AST relies on. + * + * @author Jeff Nelson + */ +public final class ModificationKeySymbol + extends KeyTokenSymbol> { + + /** + * The inclusive start of the addition window; the end of the window is + * the (exclusive) present moment. + */ + private final TimestampSymbol timestamp; + + /** + * Construct a new {@link ModificationKeySymbol}. + * + * @param key the wrapped {@link KeyTokenSymbol}; must not be a + * {@link NavigationKeySymbol} (navigation timestamps live + * on the path's stops) and must not already be + * parameterized (a key carries at most one + * bracket-timestamp parameter) + * @param timestamp the inclusive start of the addition window + * @throws IllegalArgumentException if {@code key} is a + * {@link NavigationKeySymbol} or already parameterized + */ + public ModificationKeySymbol(KeyTokenSymbol key, + TimestampSymbol timestamp) { + super(Preconditions.checkNotNull(key)); + Preconditions.checkArgument(!(key instanceof NavigationKeySymbol), + "ModificationKeySymbol cannot wrap a NavigationKeySymbol; " + + "modification bindings on navigation keys are not " + + "yet supported"); + Preconditions.checkArgument(!key.isParameterized(), + "ModificationKeySymbol cannot wrap an already-parameterized " + + "key; a key carries at most one bracket-timestamp " + + "parameter"); + this.timestamp = Preconditions.checkNotNull(timestamp); + } + + /** + * Return the inclusive start of the addition window. + * + * @return the timestamp {@link TimestampSymbol} + */ + public TimestampSymbol timestamp() { + return timestamp; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + else if(!(obj instanceof ModificationKeySymbol)) { + return false; + } + ModificationKeySymbol other = (ModificationKeySymbol) obj; + return key.equals(other.key) && timestamp.equals(other.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(key, timestamp); + } + + @Override + public String toString() { + return key.toString() + "[" + timestamp.timestamp() + "~]"; + } + + @Override + public String baseKey() { + return key.baseKey(); + } + + @Override + public boolean isParameterized() { + return true; + } + + @Override + public KeyTokenSymbol stripParameters() { + return key.stripParameters(); + } + +} diff --git a/src/test/java/com/cinchapi/ccl/BracketTimestampCommandTest.java b/src/test/java/com/cinchapi/ccl/BracketTimestampCommandTest.java index bb706fc..9709a18 100644 --- a/src/test/java/com/cinchapi/ccl/BracketTimestampCommandTest.java +++ b/src/test/java/com/cinchapi/ccl/BracketTimestampCommandTest.java @@ -28,6 +28,7 @@ import com.cinchapi.ccl.grammar.NavigationKeyStop; import com.cinchapi.ccl.grammar.OrderComponentSymbol; import com.cinchapi.ccl.grammar.OrderSymbol; +import com.cinchapi.ccl.grammar.ModificationKeySymbol; import com.cinchapi.ccl.grammar.TemporalKeySymbol; import com.cinchapi.ccl.grammar.TemporalRangeKeySymbol; import com.cinchapi.ccl.grammar.command.BrowseSymbol; @@ -218,6 +219,49 @@ public void testAuditRejectsRangeKey() { "audit command"); } + @Test + public void testSelectModificationKeyBracket() { + SelectSymbol cmd = parseCommand( + String.format("select locked[%d~] from 1", T1), + SelectSymbol.class); + assertModificationKey(cmd.keys().iterator().next(), "locked", T1); + } + + @Test + public void testGetModificationKeyBracket() { + GetSymbol cmd = parseCommand( + String.format("get locked[%d~] from 1", T1), + GetSymbol.class); + assertModificationKey(cmd.keys().iterator().next(), "locked", T1); + } + + @Test + public void testFindModificationKeyBracket() { + AbstractSyntaxTree tree = compiler().parse( + String.format("find locked[%d~] = \"pod-A\"", T1)); + Assert.assertTrue(tree instanceof CommandTree); + com.cinchapi.ccl.syntax.ConditionTree cond = + ((CommandTree) tree).conditionTree(); + com.cinchapi.ccl.grammar.ExpressionSymbol expr = + (com.cinchapi.ccl.grammar.ExpressionSymbol) + ((com.cinchapi.ccl.syntax.ExpressionTree) cond).root(); + assertModificationKey(expr.key(), "locked", T1); + } + + @Test + public void testAddRejectsModificationKey() { + assertCommandRejected( + String.format("add locked[%d~] as \"pod-A\" in 1", T1), + "add command"); + } + + @Test + public void testAuditRejectsModificationKey() { + assertCommandRejected( + String.format("audit locked[%d~] in 1", T1), + "audit command"); + } + @Test public void testAddRejectsBracketKey() { assertCommandRejected( @@ -535,6 +579,19 @@ private void assertTemporalRangeKey(KeyTokenSymbol key, Assert.assertEquals(expectedEnd, range.end().timestamp()); } + private void assertModificationKey(KeyTokenSymbol key, + String expectedKey, long expectedTs) { + Assert.assertTrue( + "expected ModificationKeySymbol but got " + + key.getClass().getSimpleName(), + key instanceof ModificationKeySymbol); + ModificationKeySymbol modification = (ModificationKeySymbol) key; + Assert.assertTrue(modification.key() instanceof KeySymbol); + Assert.assertEquals(expectedKey, + ((KeySymbol) modification.key()).key()); + Assert.assertEquals(expectedTs, modification.timestamp().timestamp()); + } + private static final Function VALUE_FN = Convert::stringToJava; private static final Function OP_FN = Convert::stringToOperator; diff --git a/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java b/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java index 0fa83b3..257cc49 100644 --- a/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java +++ b/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java @@ -26,6 +26,7 @@ import com.cinchapi.ccl.grammar.ExpressionSymbol; import com.cinchapi.ccl.grammar.KeySymbol; import com.cinchapi.ccl.grammar.KeyTokenSymbol; +import com.cinchapi.ccl.grammar.ModificationKeySymbol; import com.cinchapi.ccl.grammar.NavigationKeyStop; import com.cinchapi.ccl.grammar.NavigationKeySymbol; import com.cinchapi.ccl.grammar.Symbol; @@ -347,6 +348,128 @@ public void testRoundTripRangeResolvedEndpoints() { assertRoundTrip("foo[last week...] = \"X\""); } + /** + * Goal: Verify that an addition-marker on a flat leaf + * ({@code foo[t1~] = X}) parses to a {@link ModificationKeySymbol} + * carrying the marked timestamp. The half-open {@code [t1, now)} + * add-window intent is documented here even though the parser only + * records the endpoint: a value added at or after {@code t1} is in + * range, a value added before {@code t1} (even if still present) is + * not. + */ + @Test + public void testM1_AfterMarkerNumeric() { + String ccl = String.format("foo[%d~] = \"X\"", T1); + ModificationKeySymbol modification = assertLeafKeyModification( + parseExpression(ccl), "foo"); + Assert.assertEquals(T1, modification.timestamp().timestamp()); + } + + /** + * Goal: Verify that an addition-marker whose + * timestamp is a quoted natural-language phrase + * ({@code foo["last week"~] = X}) parses to a + * {@link ModificationKeySymbol}. This exercises the detached + * {@code ~} token path (the marker is lexed as a {@code SEARCH_MATCH} + * separate from the quoted timestamp), which the numeric case, where + * {@code ~} attaches to the digits, does not. Compared at day + * precision because the phrase is anchored to the current instant. + */ + @Test + public void testM2_AfterMarkerQuotedNaturalLanguage() { + TimestampSymbol expected = new TimestampSymbol( + NaturalLanguage.parseMicros("last week"), TimeUnit.DAYS); + ModificationKeySymbol modification = assertLeafKeyModification( + parseExpression("foo[\"last week\"~] = \"X\""), "foo"); + Assert.assertEquals(expected, modification.timestamp()); + } + + /** + * Goal: Verify that the optional {@code at} keyword + * is accepted in front of the marked timestamp + * ({@code foo[at t1~] = X}) and yields the same + * {@link ModificationKeySymbol} as the keyword-less form. + */ + @Test + public void testM3_AfterMarkerKeywordEquivalence() { + ModificationKeySymbol withKeyword = assertLeafKeyModification( + parseExpression(String.format("foo[at %d~] = \"X\"", T1)), + "foo"); + Assert.assertEquals(T1, withKeyword.timestamp().timestamp()); + } + + /** + * Goal: Verify that the leading before-marker form + * ({@code foo[~t1] = X}) is rejected at parse time, since the + * added-before semantic is not yet supported. + */ + @Test + public void testM4_BeforeMarkerRejected() { + String ccl = String.format("foo[~%d] = \"X\"", T1); + try { + compiler().parse(ccl); + Assert.fail("expected SyntaxException for before-marker in: " + + ccl); + } + catch (SyntaxException e) { + Assert.assertTrue( + "expected message to mention 'not yet supported' but " + + "was: " + e.getMessage(), + e.getMessage().contains("not yet supported")); + } + } + + /** + * Goal: Verify that combining a range with an + * addition-marker ({@code foo[t1...t2~] = X}) is rejected, since a + * marker binds a single instant rather than a range. + */ + @Test + public void testM5_RangeWithMarkerRejected() { + String ccl = String.format("foo[%d...%d~] = \"X\"", T1, T2); + try { + compiler().parse(ccl); + Assert.fail("expected SyntaxException for range-with-marker in: " + + ccl); + } + catch (SyntaxException e) { + Assert.assertTrue( + "expected message to mention 'single instant, not a " + + "range' but was: " + e.getMessage(), + e.getMessage().contains("single instant, not a range")); + } + } + + /** + * Goal: Verify that a marker bracket followed by a + * second bracket on one leaf ({@code foo[t1~][t2] = X}) is rejected, + * since {@code Key()} accepts at most one trailing bracket. + */ + @Test + public void testM6_MarkerBeforeSingleBracketRejected() { + String ccl = String.format("foo[%d~][%d] = \"X\"", T1, T2); + try { + compiler().parse(ccl); + Assert.fail("expected SyntaxException for double bracket with " + + "marker in: " + ccl); + } + catch (SyntaxException e) { + // Pass — Key()'s optional bracket cannot fire twice. + } + } + + /** + * Goal: Verify lossless round-trip of the addition + * marker. The canonical {@code toString()} renders the timestamp as + * resolved microseconds with a trailing {@code ~}, so re-parsing the + * re-emitted form must yield an equal AST. + */ + @Test + public void testRoundTripModification() { + assertRoundTrip(String.format("foo[%d~] = \"X\"", T1)); + assertRoundTrip("foo[\"last week\"~] = \"X\""); + } + /** * Goal: Verify that an unbracketed navigation key * ({@code a.foo = X}) parses to a {@link NavigationKeySymbol} @@ -1079,6 +1202,20 @@ private void assertLeafKeyTemporalRange(ExpressionSymbol expr, Assert.assertEquals(expectedEnd, range.end().timestamp()); } + private ModificationKeySymbol assertLeafKeyModification( + ExpressionSymbol expr, String expectedKey) { + KeyTokenSymbol key = expr.key(); + Assert.assertTrue( + "expected ModificationKeySymbol but was " + + key.getClass().getSimpleName(), + key instanceof ModificationKeySymbol); + ModificationKeySymbol modification = (ModificationKeySymbol) key; + Assert.assertTrue(modification.key() instanceof KeySymbol); + Assert.assertEquals(expectedKey, + ((KeySymbol) modification.key()).key().toString()); + return modification; + } + private void assertLeafKeyUnstamped(AbstractSyntaxTree leaf) { ExpressionTree exprTree = (ExpressionTree) leaf; ExpressionSymbol expr = (ExpressionSymbol) exprTree.root(); diff --git a/src/test/java/com/cinchapi/ccl/grammar/ModificationKeySymbolTest.java b/src/test/java/com/cinchapi/ccl/grammar/ModificationKeySymbolTest.java new file mode 100644 index 0000000..b0d2415 --- /dev/null +++ b/src/test/java/com/cinchapi/ccl/grammar/ModificationKeySymbolTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2013-2026 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.ccl.grammar; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for {@link ModificationKeySymbol}. + * + * @author Jeff Nelson + */ +public class ModificationKeySymbolTest { + + @Test(expected = NullPointerException.class) + public void testConstructRejectsNullKey() { + new ModificationKeySymbol(null, new TimestampSymbol(1L)); + } + + @Test(expected = NullPointerException.class) + public void testConstructRejectsNullTimestamp() { + new ModificationKeySymbol(new KeySymbol("foo"), null); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructRejectsNavigationKeySymbolWrapping() { + new ModificationKeySymbol(new NavigationKeySymbol("a.b"), + new TimestampSymbol(1L)); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructRejectsParameterizedKeyWrapping() { + TemporalKeySymbol inner = new TemporalKeySymbol(new KeySymbol("foo"), + new TimestampSymbol(1L)); + new ModificationKeySymbol(inner, new TimestampSymbol(2L)); + } + + @Test + public void testTimestampAccessor() { + TimestampSymbol ts = new TimestampSymbol(1L); + ModificationKeySymbol symbol = new ModificationKeySymbol( + new KeySymbol("foo"), ts); + Assert.assertSame(ts, symbol.timestamp()); + } + + @Test + public void testToString() { + ModificationKeySymbol symbol = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(123L)); + Assert.assertEquals("foo[123~]", symbol.toString()); + } + + @Test + public void testEqualsWhenKeyAndTimestampMatch() { + ModificationKeySymbol a = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + ModificationKeySymbol b = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + Assert.assertEquals(a, b); + Assert.assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + public void testNotEqualsWhenTimestampDiffers() { + ModificationKeySymbol a = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + ModificationKeySymbol b = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(2L)); + Assert.assertNotEquals(a, b); + } + + @Test + public void testNotEqualsWhenKeyDiffers() { + ModificationKeySymbol a = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + ModificationKeySymbol b = new ModificationKeySymbol( + new KeySymbol("bar"), new TimestampSymbol(1L)); + Assert.assertNotEquals(a, b); + } + + @Test + public void testNotEqualsToTemporalKeySymbol() { + ModificationKeySymbol modification = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + Assert.assertNotEquals(modification, + new TemporalKeySymbol(new KeySymbol("foo"), + new TimestampSymbol(1L))); + } + + @Test + public void testNotEqualsToTemporalRangeKeySymbol() { + ModificationKeySymbol modification = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + Assert.assertNotEquals(modification, + new TemporalRangeKeySymbol(new KeySymbol("foo"), + new TimestampSymbol(1L), null)); + } + + @Test + public void testBaseKeyDelegatesToWrappedKey() { + ModificationKeySymbol symbol = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + Assert.assertEquals("foo", symbol.baseKey()); + } + + @Test + public void testIsParameterized() { + ModificationKeySymbol symbol = new ModificationKeySymbol( + new KeySymbol("foo"), new TimestampSymbol(1L)); + Assert.assertTrue(symbol.isParameterized()); + } + + @Test + public void testStripParametersReturnsWrappedKey() { + KeySymbol inner = new KeySymbol("foo"); + ModificationKeySymbol symbol = new ModificationKeySymbol(inner, + new TimestampSymbol(1L)); + Assert.assertEquals(inner, symbol.stripParameters()); + } + +} From 6b860fb03c7e2d1373604f23bac411d7814de41e Mon Sep 17 00:00:00 2001 From: Javier Lores Date: Wed, 24 Jun 2026 14:44:58 -0400 Subject: [PATCH 2/2] fix: [GH-75] reject search keywords in key brackets; tidy applyKeyBracket Address code-review findings on the modification-marker work: - Grammar: the SEARCH_MATCH token also lexes the 'search_match'/'contains' keyword spellings, which were silently admitted into KeyBracketParameter content and only failed later at timestamp parsing. Guard the action to admit only the '~' marker and reject the keyword forms with a clear message at parse time. Parser regenerated. - Parsing.applyKeyBracket: use the already-trimmed content uniformly across the marker, single-instant, and range branches (was mixing content and content.trim()). Behavior unchanged. - Tests: testM7 confirms a '~' inside a quoted timestamp is not read as a marker; testM8 confirms a search keyword in a bracket is rejected. --- grammar/grammar.jjt | 13 +- src/main/java/com/cinchapi/ccl/Parsing.java | 14 +- .../com/cinchapi/ccl/generated/Grammar.java | 810 +++++++++--------- .../ccl/BracketTimestampMatrixTest.java | 50 ++ 4 files changed, 476 insertions(+), 411 deletions(-) diff --git a/grammar/grammar.jjt b/grammar/grammar.jjt index f13b632..a7352db 100644 --- a/grammar/grammar.jjt +++ b/grammar/grammar.jjt @@ -768,7 +768,9 @@ TimestampSymbol Timestamp() : // 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. +// 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; @@ -776,7 +778,14 @@ String KeyBracketParameter() : } { ()? - (LOOKAHEAD(2) (word= | word= | word= | word= | word= | word= | word= | word=) { parameter += (parameter.equals("")) ? word.image : " " + word.image; })+ + (LOOKAHEAD(2) (word= | word= | word= | word= | word= | word= | word= | word=) + { + 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; } } diff --git a/src/main/java/com/cinchapi/ccl/Parsing.java b/src/main/java/com/cinchapi/ccl/Parsing.java index 88ed20e..6583dc1 100644 --- a/src/main/java/com/cinchapi/ccl/Parsing.java +++ b/src/main/java/com/cinchapi/ccl/Parsing.java @@ -115,14 +115,14 @@ public static KeyTokenSymbol applyKeyBracket(KeyTokenSymbol base, indexOfRangeSeparator(timestamp, 0) < 0, "a modification marker binds a single instant, not a " + "range; '%s' combines '%s' with '%s'", - content, RANGE_SEPARATOR, MODIFICATION_MARKER); + trimmed, RANGE_SEPARATOR, MODIFICATION_MARKER); return new ModificationKeySymbol(base, new TimestampSymbol(NaturalLanguage.parseMicros(timestamp))); } - int separator = indexOfRangeSeparator(content, 0); + 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); @@ -133,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( diff --git a/src/main/java/com/cinchapi/ccl/generated/Grammar.java b/src/main/java/com/cinchapi/ccl/generated/Grammar.java index 7e4679a..41dad50 100644 --- a/src/main/java/com/cinchapi/ccl/generated/Grammar.java +++ b/src/main/java/com/cinchapi/ccl/generated/Grammar.java @@ -1626,7 +1626,9 @@ final public void RelationalExpressionNoTimestamp() throws ParseException {/*@bg // 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. +// 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 '~'. final public String KeyBracketParameter() throws ParseException {Token word; String parameter = ""; switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { @@ -1678,7 +1680,11 @@ final public void RelationalExpressionNoTimestamp() throws ParseException {/*@bg jj_consume_token(-1); throw new ParseException(); } -parameter += (parameter.equals("")) ? word.image : " " + word.image; +if(word.kind == GrammarConstants.SEARCH_MATCH && !word.image.equals("~")) { + {if (true) 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; if (jj_2_20(2)) { ; } else { @@ -5623,6 +5629,20 @@ private boolean jj_2_71(int xla) finally { jj_save(70, xla); } } + private boolean jj_3R_39() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + + private boolean jj_3_31() + { + if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_scan_token(NUMERIC)) return true; + return false; + } + private boolean jj_3R_22() { if (jj_3R_67()) return true; @@ -5646,52 +5666,6 @@ private boolean jj_3R_27() return false; } - private boolean jj_3_32() - { - if (jj_3R_42()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(40)) { - jj_scanpos = xsp; - if (jj_scan_token(37)) { - jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } - } - return false; - } - - private boolean jj_3R_39() - { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - - private boolean jj_3R_23() - { - if (jj_3R_69()) return true; - return false; - } - - private boolean jj_3_31() - { - if (jj_scan_token(OPEN_BRACKET)) return true; - if (jj_scan_token(NUMERIC)) return true; - return false; - } - - private boolean jj_3R_29() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(82)) { - jj_scanpos = xsp; - if (jj_scan_token(83)) return true; - } - return false; - } - private boolean jj_3_71() { Token xsp; @@ -5726,9 +5700,20 @@ private boolean jj_3_70() return false; } - private boolean jj_3R_108() + private boolean jj_3R_23() { - if (jj_scan_token(BINARY_OPERATOR)) return true; + if (jj_3R_69()) return true; + return false; + } + + private boolean jj_3R_29() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(82)) { + jj_scanpos = xsp; + if (jj_scan_token(83)) return true; + } return false; } @@ -5757,6 +5742,12 @@ private boolean jj_3R_38() return false; } + private boolean jj_3R_108() + { + if (jj_scan_token(BINARY_OPERATOR)) return true; + return false; + } + private boolean jj_3R_106() { Token xsp; @@ -5784,23 +5775,6 @@ private boolean jj_3_2() return false; } - private boolean jj_3R_104() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(71)) { - jj_scanpos = xsp; - if (jj_scan_token(72)) { - jj_scanpos = xsp; - if (jj_scan_token(73)) { - jj_scanpos = xsp; - if (jj_scan_token(74)) return true; - } - } - } - return false; - } - private boolean jj_3R_54() { if (jj_scan_token(NUMERIC)) return true; @@ -5840,12 +5814,6 @@ private boolean jj_3R_37() return false; } - private boolean jj_3R_102() - { - if (jj_scan_token(LINKS_TO)) return true; - return false; - } - private boolean jj_3_27() { Token xsp; @@ -5857,17 +5825,20 @@ private boolean jj_3_27() return false; } - private boolean jj_3_1() + private boolean jj_3R_104() { Token xsp; xsp = jj_scanpos; - if (jj_scan_token(56)) jj_scanpos = xsp; - if (jj_3R_22()) return true; - xsp = jj_scanpos; - if (jj_3R_23()) jj_scanpos = xsp; - xsp = jj_scanpos; - if (jj_3R_24()) jj_scanpos = xsp; - if (jj_scan_token(102)) return true; + if (jj_scan_token(71)) { + jj_scanpos = xsp; + if (jj_scan_token(72)) { + jj_scanpos = xsp; + if (jj_scan_token(73)) { + jj_scanpos = xsp; + if (jj_scan_token(74)) return true; + } + } + } return false; } @@ -5878,9 +5849,23 @@ private boolean jj_3_52() return false; } - private boolean jj_3R_116() + private boolean jj_3R_102() { - if (jj_scan_token(QUOTED_STRING)) return true; + if (jj_scan_token(LINKS_TO)) return true; + return false; + } + + private boolean jj_3_1() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(56)) jj_scanpos = xsp; + if (jj_3R_22()) return true; + xsp = jj_scanpos; + if (jj_3R_23()) jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_3R_24()) jj_scanpos = xsp; + if (jj_scan_token(102)) return true; return false; } @@ -5924,6 +5909,30 @@ private boolean jj_3_26() return false; } + private boolean jj_3R_116() + { + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + + private boolean jj_3R_52() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + + private boolean jj_3_46() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_53()) return true; + } + return false; + } + private boolean jj_3R_115() { Token xsp; @@ -5950,6 +5959,13 @@ private boolean jj_3R_115() return false; } + private boolean jj_3R_63() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + private boolean jj_3R_109() { Token xsp; @@ -5970,25 +5986,33 @@ private boolean jj_3_18() return false; } - private boolean jj_3R_52() + private boolean jj_3_51() { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; + if (jj_3R_43()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(40)) { + jj_scanpos = xsp; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(39)) return true; + } + } return false; } - private boolean jj_3_46() + private boolean jj_3_45() { Token xsp; xsp = jj_scanpos; if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_3R_53()) return true; + if (jj_3R_52()) return true; } return false; } - private boolean jj_3R_63() + private boolean jj_3R_62() { if (jj_scan_token(NUMERIC)) return true; if (jj_scan_token(COMMA)) return true; @@ -6001,36 +6025,29 @@ private boolean jj_3R_101() return false; } - private boolean jj_3_51() + private boolean jj_3_66() { - if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(40)) { - jj_scanpos = xsp; - if (jj_scan_token(37)) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } + if (jj_3R_63()) return true; } return false; } - private boolean jj_3_45() + private boolean jj_3_50() { + if (jj_3R_42()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_scan_token(40)) { jj_scanpos = xsp; - if (jj_3R_52()) return true; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(39)) return true; + } } - return false; - } - - private boolean jj_3R_62() - { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; return false; } @@ -6057,14 +6074,10 @@ private boolean jj_3_16() return false; } - private boolean jj_3_66() + private boolean jj_3_49() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_63()) return true; - } + if (jj_3R_43()) return true; + if (jj_scan_token(WHERE)) return true; return false; } @@ -6079,21 +6092,24 @@ private boolean jj_3R_100() return false; } - private boolean jj_3_50() + private boolean jj_3_65() { - if (jj_3R_42()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(40)) { - jj_scanpos = xsp; - if (jj_scan_token(37)) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } + if (jj_3R_62()) return true; } return false; } + private boolean jj_3_48() + { + if (jj_3R_42()) return true; + if (jj_scan_token(WHERE)) return true; + return false; + } + private boolean jj_3R_94() { Token xsp; @@ -6114,49 +6130,24 @@ private boolean jj_3_17() return false; } - private boolean jj_3_49() - { - if (jj_3R_43()) return true; - if (jj_scan_token(WHERE)) return true; - return false; - } - - private boolean jj_3_65() + private boolean jj_3_67() { + if (jj_3R_42()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_scan_token(37)) { jj_scanpos = xsp; - if (jj_3R_62()) return true; + if (jj_scan_token(39)) return true; } return false; } - private boolean jj_3_48() - { - if (jj_3R_42()) return true; - if (jj_scan_token(WHERE)) return true; - return false; - } - private boolean jj_3R_112() { if (jj_scan_token(QUOTED_STRING)) return true; return false; } - private boolean jj_3_67() - { - if (jj_3R_42()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(37)) { - jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } - return false; - } - private boolean jj_3_14() { Token xsp; @@ -6189,6 +6180,20 @@ private boolean jj_3_14() return false; } + private boolean jj_3R_49() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + + private boolean jj_3_44() + { + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + private boolean jj_3_15() { Token xsp; @@ -6232,17 +6237,21 @@ private boolean jj_3R_111() return false; } - private boolean jj_3R_49() + private boolean jj_3R_61() { if (jj_scan_token(NUMERIC)) return true; if (jj_scan_token(COMMA)) return true; return false; } - private boolean jj_3_44() + private boolean jj_3_39() { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_49()) return true; + } return false; } @@ -6272,36 +6281,12 @@ private boolean jj_3R_105() return false; } - private boolean jj_3R_61() - { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - - private boolean jj_3_39() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_49()) return true; - } - return false; - } - private boolean jj_3R_36() { if (jj_scan_token(OPEN_BRACKET)) return true; return false; } - private boolean jj_3R_103() - { - if (jj_scan_token(NUMERIC)) return true; - return false; - } - private boolean jj_3R_51() { if (jj_scan_token(NUMERIC)) return true; @@ -6365,9 +6350,9 @@ private boolean jj_3R_34() return false; } - private boolean jj_3R_114() + private boolean jj_3R_103() { - if (jj_scan_token(QUOTED_STRING)) return true; + if (jj_scan_token(NUMERIC)) return true; return false; } @@ -6425,7 +6410,13 @@ private boolean jj_3_40() return false; } - private boolean jj_3_12() + private boolean jj_3R_114() + { + if (jj_scan_token(QUOTED_STRING)) return true; + return false; + } + + private boolean jj_3_24() { Token xsp; xsp = jj_scanpos; @@ -6433,12 +6424,8 @@ private boolean jj_3_12() jj_scanpos = xsp; if (jj_scan_token(91)) { jj_scanpos = xsp; - if (jj_scan_token(88)) { - jj_scanpos = xsp; if (jj_scan_token(92)) { jj_scanpos = xsp; - if (jj_scan_token(97)) { - jj_scanpos = xsp; if (jj_scan_token(93)) { jj_scanpos = xsp; if (jj_scan_token(96)) return true; @@ -6446,12 +6433,33 @@ private boolean jj_3_12() } } } + xsp = jj_scanpos; + if (jj_3R_33()) { + jj_scanpos = xsp; + if (jj_3R_34()) return true; } + return false; + } + + private boolean jj_3_37() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_46()) return true; } return false; } - private boolean jj_3_24() + private boolean jj_3R_32() + { + if (jj_scan_token(ALPHANUMERIC)) return true; + if (jj_scan_token(OPEN_PARENTHESES)) return true; + return false; + } + + private boolean jj_3_12() { Token xsp; xsp = jj_scanpos; @@ -6459,8 +6467,12 @@ private boolean jj_3_24() jj_scanpos = xsp; if (jj_scan_token(91)) { jj_scanpos = xsp; + if (jj_scan_token(88)) { + jj_scanpos = xsp; if (jj_scan_token(92)) { jj_scanpos = xsp; + if (jj_scan_token(97)) { + jj_scanpos = xsp; if (jj_scan_token(93)) { jj_scanpos = xsp; if (jj_scan_token(96)) return true; @@ -6468,11 +6480,14 @@ private boolean jj_3_24() } } } - xsp = jj_scanpos; - if (jj_3R_33()) { - jj_scanpos = xsp; - if (jj_3R_34()) return true; } + } + return false; + } + + private boolean jj_3R_48() + { + if (jj_scan_token(NUMERIC)) return true; return false; } @@ -6487,21 +6502,9 @@ private boolean jj_3R_113() return false; } - private boolean jj_3_37() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_46()) return true; - } - return false; - } - - private boolean jj_3R_32() + private boolean jj_3R_47() { - if (jj_scan_token(ALPHANUMERIC)) return true; - if (jj_scan_token(OPEN_PARENTHESES)) return true; + if (jj_3R_77()) return true; return false; } @@ -6525,18 +6528,6 @@ private boolean jj_3_13() return false; } - private boolean jj_3R_48() - { - if (jj_scan_token(NUMERIC)) return true; - return false; - } - - private boolean jj_3R_47() - { - if (jj_3R_77()) return true; - return false; - } - private boolean jj_3R_30() { Token xsp; @@ -6618,6 +6609,19 @@ private boolean jj_3_38() return false; } + private boolean jj_3_41() + { + if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_scan_token(NUMERIC)) return true; + return false; + } + + private boolean jj_3_23() + { + if (jj_3R_30()) return true; + return false; + } + private boolean jj_3_11() { if (jj_scan_token(OPEN_BRACKET)) return true; @@ -6626,10 +6630,14 @@ private boolean jj_3_11() return false; } - private boolean jj_3_41() + private boolean jj_3_62() { - if (jj_scan_token(OPEN_BRACKET)) return true; - if (jj_scan_token(NUMERIC)) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_60()) return true; + } return false; } @@ -6644,6 +6652,13 @@ private boolean jj_3R_76() return false; } + private boolean jj_3R_59() + { + if (jj_3R_43()) return true; + if (jj_scan_token(COMMA)) return true; + return false; + } + private boolean jj_3R_75() { Token xsp; @@ -6664,36 +6679,12 @@ private boolean jj_3R_75() return false; } - private boolean jj_3_23() - { - if (jj_3R_30()) return true; - return false; - } - private boolean jj_3_10() { if (jj_3R_30()) return true; return false; } - private boolean jj_3_62() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_60()) return true; - } - return false; - } - - private boolean jj_3R_59() - { - if (jj_3R_43()) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - private boolean jj_3R_43() { Token xsp; @@ -6710,6 +6701,17 @@ private boolean jj_3R_43() return false; } + private boolean jj_3_61() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_59()) return true; + } + return false; + } + private boolean jj_3R_72() { Token xsp; @@ -6741,14 +6743,10 @@ private boolean jj_3R_71() return false; } - private boolean jj_3_61() + private boolean jj_3R_58() { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(53)) { - jj_scanpos = xsp; - if (jj_3R_59()) return true; - } + if (jj_scan_token(NUMERIC)) return true; + if (jj_scan_token(COMMA)) return true; return false; } @@ -6763,19 +6761,6 @@ private boolean jj_3R_28() return false; } - private boolean jj_3R_58() - { - if (jj_scan_token(NUMERIC)) return true; - if (jj_scan_token(COMMA)) return true; - return false; - } - - private boolean jj_3_6() - { - if (jj_3R_27()) return true; - return false; - } - private boolean jj_3_60() { Token xsp; @@ -6787,13 +6772,6 @@ private boolean jj_3_60() return false; } - private boolean jj_3_9() - { - if (jj_3R_28()) return true; - if (jj_3R_29()) return true; - return false; - } - private boolean jj_3R_45() { if (jj_scan_token(NUMERIC)) return true; @@ -6801,15 +6779,22 @@ private boolean jj_3R_45() return false; } - private boolean jj_3_7() + private boolean jj_3_6() { if (jj_3R_27()) return true; return false; } - private boolean jj_3_4() + private boolean jj_3_9() { - if (jj_3R_27()) return true; + if (jj_3R_28()) return true; + if (jj_3R_29()) return true; + return false; + } + + private boolean jj_3_7() + { + if (jj_3R_27()) return true; return false; } @@ -6824,13 +6809,7 @@ private boolean jj_3_36() return false; } - private boolean jj_3_3() - { - if (jj_3R_27()) return true; - return false; - } - - private boolean jj_3_5() + private boolean jj_3_4() { if (jj_3R_27()) return true; return false; @@ -6849,6 +6828,18 @@ private boolean jj_3R_44() return false; } + private boolean jj_3_3() + { + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3_5() + { + if (jj_3R_27()) return true; + return false; + } + private boolean jj_3_59() { if (jj_scan_token(NUMERIC)) return true; @@ -6905,16 +6896,6 @@ private boolean jj_3R_89() return false; } - private boolean jj_3R_99() - { - if (jj_3R_29()) return true; - if (jj_3R_94()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3_7()) jj_scanpos = xsp; - return false; - } - private boolean jj_3R_81() { if (jj_3R_90()) return true; @@ -6949,41 +6930,21 @@ private boolean jj_3R_70() return false; } - private boolean jj_3R_98() - { - if (jj_3R_108()) return true; - if (jj_3R_109()) return true; - if (jj_3R_109()) return true; - return false; - } - - private boolean jj_3R_97() - { - if (jj_3R_106()) return true; - if (jj_3R_107()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3_5()) jj_scanpos = xsp; - return false; - } - - private boolean jj_3R_96() + private boolean jj_3R_99() { - if (jj_3R_104()) return true; - if (jj_3R_105()) return true; + if (jj_3R_29()) return true; + if (jj_3R_94()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3_4()) jj_scanpos = xsp; + if (jj_3_7()) jj_scanpos = xsp; return false; } - private boolean jj_3R_95() + private boolean jj_3R_98() { - if (jj_3R_102()) return true; - if (jj_3R_103()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_3_3()) jj_scanpos = xsp; + if (jj_3R_108()) return true; + if (jj_3R_109()) return true; + if (jj_3R_109()) return true; return false; } @@ -7013,24 +6974,13 @@ private boolean jj_3_21() return false; } - private boolean jj_3R_93() + private boolean jj_3R_97() { - if (jj_3R_43()) return true; + if (jj_3R_106()) return true; + if (jj_3R_107()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3R_95()) { - jj_scanpos = xsp; - if (jj_3R_96()) { - jj_scanpos = xsp; - if (jj_3R_97()) { - jj_scanpos = xsp; - if (jj_3R_98()) { - jj_scanpos = xsp; - if (jj_3R_99()) return true; - } - } - } - } + if (jj_3_5()) jj_scanpos = xsp; return false; } @@ -7065,21 +7015,43 @@ private boolean jj_3R_55() return false; } - private boolean jj_3_8() + private boolean jj_3R_96() { - if (jj_3R_28()) return true; - if (jj_3R_29()) return true; - if (jj_3R_94()) return true; + if (jj_3R_104()) return true; + if (jj_3R_105()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3_4()) jj_scanpos = xsp; return false; } - private boolean jj_3R_92() + private boolean jj_3R_95() { + if (jj_3R_102()) return true; + if (jj_3R_103()) return true; Token xsp; xsp = jj_scanpos; - if (jj_3_8()) { + if (jj_3_3()) jj_scanpos = xsp; + return false; + } + + private boolean jj_3R_93() + { + if (jj_3R_43()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_3R_95()) { jj_scanpos = xsp; - if (jj_3R_93()) return true; + if (jj_3R_96()) { + jj_scanpos = xsp; + if (jj_3R_97()) { + jj_scanpos = xsp; + if (jj_3R_98()) { + jj_scanpos = xsp; + if (jj_3R_99()) return true; + } + } + } } return false; } @@ -7117,6 +7089,14 @@ private boolean jj_3R_74() return false; } + private boolean jj_3_8() + { + if (jj_3R_28()) return true; + if (jj_3R_29()) return true; + if (jj_3R_94()) return true; + return false; + } + private boolean jj_3_22() { Token xsp; @@ -7143,6 +7123,17 @@ private boolean jj_3_22() return false; } + private boolean jj_3R_92() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3_8()) { + jj_scanpos = xsp; + if (jj_3R_93()) return true; + } + return false; + } + private boolean jj_3R_73() { if (jj_scan_token(OPEN_BRACKET)) return true; @@ -7174,12 +7165,6 @@ private boolean jj_3R_42() return false; } - private boolean jj_3R_79() - { - if (jj_scan_token(CONJUNCTION)) return true; - return false; - } - private boolean jj_3R_66() { if (jj_scan_token(NUMERIC)) return true; @@ -7187,25 +7172,12 @@ private boolean jj_3R_66() return false; } - private boolean jj_3R_68() - { - if (jj_scan_token(DISJUNCTION)) return true; - return false; - } - private boolean jj_3_56() { if (jj_scan_token(NUMERIC)) return true; return false; } - private boolean jj_3R_91() - { - if (jj_scan_token(NAVIGATION_SCOPE_OPEN)) return true; - if (jj_3R_22()) return true; - return false; - } - private boolean jj_3_55() { Token xsp; @@ -7237,6 +7209,53 @@ private boolean jj_3_34() return false; } + private boolean jj_3R_79() + { + if (jj_scan_token(CONJUNCTION)) return true; + return false; + } + + private boolean jj_3R_68() + { + if (jj_scan_token(DISJUNCTION)) return true; + return false; + } + + private boolean jj_3R_84() + { + if (jj_scan_token(OPEN_BRACKET)) return true; + return false; + } + + private boolean jj_3R_91() + { + if (jj_scan_token(NAVIGATION_SCOPE_OPEN)) return true; + if (jj_3R_22()) return true; + return false; + } + + private boolean jj_3_30() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(53)) { + jj_scanpos = xsp; + if (jj_3R_41()) return true; + } + return false; + } + + private boolean jj_3R_77() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_84()) { + jj_scanpos = xsp; + if (jj_3R_85()) return true; + } + return false; + } + private boolean jj_3_20() { Token xsp; @@ -7266,15 +7285,22 @@ private boolean jj_3_20() return false; } - private boolean jj_3R_26() + private boolean jj_3R_31() { - if (jj_3R_70()) return true; + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(55)) jj_scanpos = xsp; + if (jj_3_20()) return true; + while (true) { + xsp = jj_scanpos; + if (jj_3_20()) { jj_scanpos = xsp; break; } + } return false; } - private boolean jj_3R_84() + private boolean jj_3R_26() { - if (jj_scan_token(OPEN_BRACKET)) return true; + if (jj_3R_70()) return true; return false; } @@ -7284,26 +7310,17 @@ private boolean jj_3R_88() return false; } - private boolean jj_3R_31() - { - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(55)) jj_scanpos = xsp; - if (jj_3_20()) return true; - while (true) { - xsp = jj_scanpos; - if (jj_3_20()) { jj_scanpos = xsp; break; } - } - return false; - } - - private boolean jj_3_30() + private boolean jj_3_33() { + if (jj_3R_43()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_scan_token(40)) { jj_scanpos = xsp; - if (jj_3R_41()) return true; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(39)) return true; + } } return false; } @@ -7361,29 +7378,21 @@ private boolean jj_3_19() return false; } - private boolean jj_3R_77() + private boolean jj_3_29() { Token xsp; xsp = jj_scanpos; - if (jj_3R_84()) { + if (jj_scan_token(53)) { jj_scanpos = xsp; - if (jj_3R_85()) return true; + if (jj_3R_40()) return true; } return false; } - private boolean jj_3_33() + private boolean jj_3_54() { - if (jj_3R_43()) return true; - Token xsp; - xsp = jj_scanpos; - if (jj_scan_token(40)) { - jj_scanpos = xsp; - if (jj_scan_token(37)) { - jj_scanpos = xsp; - if (jj_scan_token(39)) return true; - } - } + if (jj_3R_55()) return true; + if (jj_scan_token(QUOTED_STRING)) return true; return false; } @@ -7404,21 +7413,18 @@ private boolean jj_3R_67() return false; } - private boolean jj_3_29() + private boolean jj_3_32() { + if (jj_3R_42()) return true; Token xsp; xsp = jj_scanpos; - if (jj_scan_token(53)) { + if (jj_scan_token(40)) { jj_scanpos = xsp; - if (jj_3R_40()) return true; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(39)) return true; + } } - return false; - } - - private boolean jj_3_54() - { - if (jj_3R_55()) return true; - if (jj_scan_token(QUOTED_STRING)) return true; return false; } diff --git a/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java b/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java index 257cc49..4ec015e 100644 --- a/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java +++ b/src/test/java/com/cinchapi/ccl/BracketTimestampMatrixTest.java @@ -470,6 +470,56 @@ public void testRoundTripModification() { assertRoundTrip("foo[\"last week\"~] = \"X\""); } + /** + * Goal: Verify that a {@code ~} appearing + * inside a quoted timestamp ({@code foo["last week ~"] = X}) + * is not mistaken for a trailing modification marker. The marker is + * recognized only when {@code ~} is the final, unquoted character of + * the bracket content; because the closing quote is the last + * character here, the content resolves as an ordinary single-instant + * timestamp and yields a {@link TemporalKeySymbol}, not a + * {@link ModificationKeySymbol}. + */ + @Test + public void testM7_QuotedTildeIsNotMarker() { + KeyTokenSymbol key = parseExpression("foo[\"last week ~\"] = \"X\"") + .key(); + Assert.assertFalse( + "a '~' inside quotes must not be read as a modification " + + "marker, but parsed to a ModificationKeySymbol", + key instanceof ModificationKeySymbol); + Assert.assertTrue( + "expected TemporalKeySymbol but was " + + key.getClass().getSimpleName(), + key instanceof TemporalKeySymbol); + Assert.assertEquals("foo", + ((KeySymbol) ((TemporalKeySymbol) key).key()).key().toString()); + } + + /** + * Goal: Verify that the {@code search_match} / + * {@code contains} keyword spellings of the {@code SEARCH_MATCH} + * token are rejected inside a key bracket. The token is admitted only + * to capture the standalone {@code ~} marker; a keyword spelling is + * meaningless here and must fail at parse time with a clear message + * rather than fall through to a confusing timestamp-parse error. + */ + @Test + public void testM8_SearchKeywordInBracketRejected() { + String ccl = String.format("foo[%d contains] = \"X\"", T1); + try { + compiler().parse(ccl); + Assert.fail("expected SyntaxException for search keyword in " + + "bracket in: " + ccl); + } + catch (SyntaxException e) { + Assert.assertTrue( + "expected message to mention the '~' marker but was: " + + e.getMessage(), + e.getMessage().contains("'~' modification marker")); + } + } + /** * Goal: Verify that an unbracketed navigation key * ({@code a.foo = X}) parses to a {@link NavigationKeySymbol}