|
24 | 24 |
|
25 | 25 | import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.SelectorResolver.SELECTOR_SEPARATOR; |
26 | 26 | import static org.elasticsearch.transport.RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR; |
27 | | -import static org.elasticsearch.transport.RemoteClusterAware.isRemoteIndexName; |
28 | 27 | import static org.elasticsearch.transport.RemoteClusterAware.splitIndexName; |
29 | 28 | import static org.elasticsearch.xpack.esql.core.util.StringUtils.EXCLUSION; |
30 | 29 | import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD; |
@@ -91,7 +90,7 @@ public String visitIndexPattern(List<EsqlBaseParser.IndexPatternContext> ctx) { |
91 | 90 | String selectorString = visitSelectorString(c.selectorString()); |
92 | 91 |
|
93 | 92 | hasSeenStar.set(hasSeenStar.get() || indexPattern.contains(WILDCARD)); |
94 | | - validateClusterAndIndexPatterns(indexPattern, c, clusterString, selectorString, hasSeenStar.get()); |
| 93 | + validate(clusterString, indexPattern, selectorString, c, hasSeenStar.get()); |
95 | 94 | patterns.add(reassembleIndexName(clusterString, indexPattern, selectorString)); |
96 | 95 | }); |
97 | 96 | return Strings.collectionToDelimitedString(patterns, ","); |
@@ -126,98 +125,141 @@ protected static void validateClusterString(String clusterString, EsqlBaseParser |
126 | 125 | } |
127 | 126 | } |
128 | 127 |
|
129 | | - private static void validateClusterAndIndexPatterns( |
130 | | - String indexPattern, |
131 | | - EsqlBaseParser.IndexPatternContext ctx, |
| 128 | + /** |
| 129 | + * Takes the parsed constituent strings and validates them. |
| 130 | + * @param clusterString Name of the remote cluster. Can be null. |
| 131 | + * @param indexPattern Name of the index. |
| 132 | + * @param selectorString Selector string, i.e. "::data" or "::failures". Can be null. |
| 133 | + * @param ctx Index Pattern Context for generating error messages with offsets. |
| 134 | + * @param hasSeenStar If we've seen an asterisk so far. |
| 135 | + */ |
| 136 | + private static void validate( |
132 | 137 | String clusterString, |
| 138 | + String indexPattern, |
133 | 139 | String selectorString, |
| 140 | + EsqlBaseParser.IndexPatternContext ctx, |
134 | 141 | boolean hasSeenStar |
135 | 142 | ) { |
136 | | - // multiple index names can be in the same double quote, e.g. indexPattern = "idx1, *, -idx2" |
137 | | - String[] patterns = indexPattern.split(","); |
138 | | - boolean isFirstPattern = true; |
| 143 | + /* |
| 144 | + * At this point, only 3 formats are possible: |
| 145 | + * "index_pattern(s)", |
| 146 | + * remote:index_pattern, and, |
| 147 | + * index_pattern::selector_string. |
| 148 | + * |
| 149 | + * The grammar prohibits remote:"index_pattern(s)" or "index_pattern(s)"::selector_string as they're |
| 150 | + * partially quoted. So if either of cluster string or selector string are present, there's no need |
| 151 | + * to split the pattern by comma since comma requires partial quoting. |
| 152 | + */ |
139 | 153 |
|
140 | | - for (String pattern : patterns) { |
141 | | - pattern = pattern.strip(); |
| 154 | + String[] patterns; |
| 155 | + if (clusterString == null && selectorString == null) { |
| 156 | + // Pattern could be quoted or is singular like "index_name". |
| 157 | + patterns = indexPattern.split(","); |
| 158 | + } else { |
| 159 | + // Either of cluster string or selector string is present. Pattern is unquoted. |
| 160 | + patterns = new String[] { indexPattern }; |
| 161 | + } |
142 | 162 |
|
| 163 | + if (patterns.length == 1) { |
| 164 | + validateSingleIndexPattern(clusterString, patterns[0], selectorString, ctx, hasSeenStar); |
| 165 | + } else { |
143 | 166 | /* |
144 | | - * Just because there was no clusterString before this index pattern does not mean that the indices |
145 | | - * are local indices. Patterns can be clubbed with remote names within quotes such as: |
146 | | - * "remote_one:remote_index,local_index". In this case, clusterString will be null. |
| 167 | + * Presence of multiple patterns requires a comma and comma requires quoting. If quoting is present, |
| 168 | + * cluster string and selector string cannot be present; they need to be attached within the quoting. |
| 169 | + * So we attempt to extract them later. |
147 | 170 | */ |
148 | | - if (isRemoteIndexName(pattern)) { |
149 | | - /* |
150 | | - * Handle scenarios like remote_one:"index1,remote_two:index2". The clusterString here is |
151 | | - * remote_one and is associated with index1 and not index2. |
152 | | - */ |
153 | | - if (clusterString != null && isFirstPattern) { |
154 | | - throw new ParsingException( |
155 | | - source(ctx), |
156 | | - "Index pattern [{}] contains a cluster alias despite specifying one [{}]", |
157 | | - pattern, |
158 | | - clusterString |
159 | | - ); |
160 | | - } |
161 | | - |
162 | | - // {cluster_alias, indexName} |
163 | | - String[] clusterAliasAndIndex; |
164 | | - try { |
165 | | - clusterAliasAndIndex = splitIndexName(pattern); |
166 | | - } catch (IllegalArgumentException e) { |
167 | | - throw new ParsingException(e, source(ctx), e.getMessage()); |
168 | | - } |
169 | | - |
170 | | - clusterString = clusterAliasAndIndex[0]; |
171 | | - pattern = clusterAliasAndIndex[1]; |
172 | | - } else if (clusterString != null) { |
173 | | - // This is not a remote index pattern and the cluster string preceding this quoted pattern |
174 | | - // cannot be associated with it. |
175 | | - if (isFirstPattern == false) { |
176 | | - clusterString = null; |
177 | | - } |
178 | | - } |
179 | | - |
180 | | - if (clusterString != null) { |
181 | | - if (selectorString != null) { |
182 | | - throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexPattern, selectorString), ctx); |
183 | | - } |
184 | | - validateClusterString(clusterString, ctx); |
185 | | - } |
186 | | - |
187 | | - validateIndexForCluster(clusterString, pattern, ctx, hasSeenStar); |
188 | | - if (selectorString != null) { |
189 | | - try { |
190 | | - // Ensures that the selector provided is one of the valid kinds |
191 | | - IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexPattern, selectorString); |
192 | | - } catch (InvalidIndexNameException e) { |
193 | | - throw new ParsingException(e, source(ctx), e.getMessage()); |
194 | | - } |
| 171 | + for (String pattern : patterns) { |
| 172 | + validateSingleIndexPattern(null, pattern, null, ctx, hasSeenStar); |
195 | 173 | } |
196 | | - |
197 | | - isFirstPattern = false; |
198 | 174 | } |
199 | 175 | } |
200 | 176 |
|
201 | | - private static void validateIndexForCluster( |
| 177 | + /** |
| 178 | + * Validates the constituent strings. Will extract the cluster string and/or selector string from the index |
| 179 | + * name if clubbed together. |
| 180 | + * |
| 181 | + * @param clusterString Name of the remote cluster. Can be null. |
| 182 | + * @param indexName Name of the index. |
| 183 | + * @param selectorString Selector string, i.e. "::data" or "::failures". Can be null. |
| 184 | + * @param ctx Index Pattern Context for generating error messages with offsets. |
| 185 | + * @param hasSeenStar If we've seen an asterisk so far. |
| 186 | + */ |
| 187 | + private static void validateSingleIndexPattern( |
202 | 188 | String clusterString, |
203 | | - String index, |
| 189 | + String indexName, |
| 190 | + String selectorString, |
204 | 191 | EsqlBaseParser.IndexPatternContext ctx, |
205 | 192 | boolean hasSeenStar |
206 | 193 | ) { |
207 | | - // Strip spaces off first because validation checks are not written to handle them |
208 | | - index = index.strip(); |
| 194 | + indexName = indexName.strip(); |
| 195 | + |
| 196 | + /* |
| 197 | + * Precedence: |
| 198 | + * 1. Cannot mix cluster and selector strings. |
| 199 | + * 2. Cluster string must be valid. |
| 200 | + * 3. Index name must be valid. |
| 201 | + * 4. Selector string must be valid. |
| 202 | + * |
| 203 | + * Since cluster string and/or selector string can be clubbed with the index name, we must try to |
| 204 | + * manually extract them before we attempt to do #2, #3, and #4. |
| 205 | + */ |
| 206 | + |
| 207 | + // It is possible to specify a pattern like "remote_cluster:index_name". Try to extract such details from the index string. |
| 208 | + if (clusterString == null && selectorString == null) { |
| 209 | + try { |
| 210 | + var split = splitIndexName(indexName); |
| 211 | + clusterString = split[0]; |
| 212 | + indexName = split[1]; |
| 213 | + } catch (IllegalArgumentException e) { |
| 214 | + throw new ParsingException(e, source(ctx), e.getMessage()); |
| 215 | + } |
| 216 | + } |
209 | 217 |
|
| 218 | + // At the moment, selector strings for remote indices is not allowed. |
| 219 | + if (clusterString != null && selectorString != null) { |
| 220 | + throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexName, selectorString), ctx); |
| 221 | + } |
| 222 | + |
| 223 | + // Validation in the right precedence. |
| 224 | + if (clusterString != null) { |
| 225 | + clusterString = clusterString.strip(); |
| 226 | + validateClusterString(clusterString, ctx); |
| 227 | + } |
| 228 | + |
| 229 | + /* |
| 230 | + * It is possible for selector string to be attached to the index: "index_name::selector_string". |
| 231 | + * Try to extract the selector string. |
| 232 | + */ |
210 | 233 | try { |
211 | | - Tuple<String, String> splitPattern = IndexNameExpressionResolver.splitSelectorExpression(index); |
212 | | - if (splitPattern.v2() != null && clusterString != null) { |
213 | | - throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, splitPattern.v1(), splitPattern.v2()), ctx); |
| 234 | + Tuple<String, String> splitPattern = IndexNameExpressionResolver.splitSelectorExpression(indexName); |
| 235 | + if (splitPattern.v2() != null) { |
| 236 | + // Cluster string too was clubbed with the index name like selector string. |
| 237 | + if (clusterString != null) { |
| 238 | + throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, splitPattern.v1(), splitPattern.v2()), ctx); |
| 239 | + } else { |
| 240 | + // We've seen a selectorString. Use it. |
| 241 | + selectorString = splitPattern.v2(); |
| 242 | + } |
214 | 243 | } |
215 | 244 |
|
216 | | - index = splitPattern.v1(); |
| 245 | + indexName = splitPattern.v1(); |
217 | 246 | } catch (InvalidIndexNameException e) { |
218 | | - // throws exception if the selector expression is invalid. Selector resolution does not complain about exclusions |
219 | 247 | throw new ParsingException(e, source(ctx), e.getMessage()); |
220 | 248 | } |
| 249 | + |
| 250 | + resolveAndValidateIndex(indexName, ctx, hasSeenStar); |
| 251 | + if (selectorString != null) { |
| 252 | + selectorString = selectorString.strip(); |
| 253 | + try { |
| 254 | + // Ensures that the selector provided is one of the valid kinds. |
| 255 | + IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexName, selectorString); |
| 256 | + } catch (InvalidIndexNameException e) { |
| 257 | + throw new ParsingException(e, source(ctx), e.getMessage()); |
| 258 | + } |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + private static void resolveAndValidateIndex(String index, EsqlBaseParser.IndexPatternContext ctx, boolean hasSeenStar) { |
221 | 263 | hasSeenStar = hasSeenStar || index.contains(WILDCARD); |
222 | 264 | index = index.replace(WILDCARD, "").strip(); |
223 | 265 | if (index.isBlank()) { |
|
0 commit comments