From 4e91116095589b9a3bd04399182b87c337ef5d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 25 Sep 2026 20:04:08 +0200 Subject: [PATCH 1/2] fix: parse text-format array literals according to PostgreSQL array grammar Previously, text-format array parameters and COPY FROM rows were parsed using SimpleParser.readArrayLiteral(), which tokenized elements as SQL string literals and processed them with Java string unescaping. This caused multiple data corruption and bind failures: 1. Adjacent quotes inside quoted elements (such as empty string values in JSON: '{"k":""}') were collapsed as SQL quote-escapes, corrupting element data. 2. Unquoted elements containing SQL comment markers ('--' or '/*') or lexical tokens (such as apostrophes or dollar quotes) were stripped or caused parameter binding to fail with SQLState 22023. 3. Elements containing backslashes were subjected to Java unicode unescaping, causing non-Java escape sequences (like Windows file paths 'C:\users' or '\uZZZZ') to throw uncaught IllegalArgumentExceptions. This change introduces ArrayLiteralParser, a dedicated parser implementing PostgreSQL's text-format array grammar (array_in): - Preserves adjacent quotes in quoted elements and SQL tokens in unquoted elements. - Unescapes literal backslash escapes without Java-specific string or unicode processing. - Conforms to PostgreSQL's ASCII whitespace definition (array_isspace), preserving Unicode spaces such as CJK ideographic space (\u3000). - Validates 1-dimensional dimension headers and bounds syntax. - Adds INTERVAL array parameter binding support in ArrayParser.bind(). - Replaces SimpleParser.readArrayLiteral with a 1-line delegate to ArrayLiteralParser, removing the obsolete returnRawHexValue overload and helpers. --- .../pgadapter/parsers/ArrayLiteralParser.java | 334 ++++++++++++++++++ .../pgadapter/parsers/ArrayParser.java | 10 +- .../pgadapter/statements/SimpleParser.java | 53 +-- .../parsers/ArrayLiteralParserTest.java | 297 ++++++++++++++++ .../pgadapter/parsers/ArrayParserTest.java | 74 +++- .../statements/SimpleParserTest.java | 48 ++- 6 files changed, 747 insertions(+), 69 deletions(-) create mode 100644 src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java create mode 100644 src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java new file mode 100644 index 0000000000..db5a76b7c9 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java @@ -0,0 +1,334 @@ +// Copyright 2026 Google LLC +// +// 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.google.cloud.spanner.pgadapter.parsers; + +import com.google.api.core.InternalApi; +import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory; +import com.google.cloud.spanner.pgadapter.error.SQLState; +import java.util.ArrayList; +import java.util.List; + +/** + * Dedicated parser for PostgreSQL text-format array literals (e.g. {@code {1, 2, 3}}, {@code + * {"foo", "bar"}}). + * + *

Implements the PostgreSQL array literal grammar as specified in PostgreSQL's {@code + * arrayfuncs.c}. Specifically: + * + *

+ */ +@InternalApi +public class ArrayLiteralParser { + private final String expression; + private final int length; + private int position; + + private ArrayLiteralParser(String expression) { + this.expression = expression; + this.length = expression.length(); + this.position = 0; + } + + /** + * Parses a PostgreSQL text-format array literal into a list of element strings. Elements that + * represent SQL NULL are represented as {@code null} in the returned list. + */ + public static List readArrayLiteral(String expression) { + if (expression == null) { + throw PGExceptionFactory.newPGException( + "Array value cannot be null", SQLState.InvalidParameterValue); + } + return new ArrayLiteralParser(expression).parse(); + } + + private List parse() { + skipWhitespace(); + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Missing '{' at start of array value: " + expression, SQLState.InvalidParameterValue); + } + skipDimensions(); + if (position >= length || expression.charAt(position) != '{') { + throw PGExceptionFactory.newPGException( + "Missing '{' at start of array value: " + expression, SQLState.InvalidParameterValue); + } + position++; // skip '{' + + skipWhitespace(); + if (position < length && expression.charAt(position) == '}') { + position++; // skip '}' + checkEndOfArray(); + return new ArrayList<>(0); + } + + List result = new ArrayList<>(); + while (true) { + skipWhitespace(); + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Missing '}' at end of array value: " + expression, SQLState.InvalidParameterValue); + } + char character = expression.charAt(position); + if (character == '"') { + result.add(parseQuotedElement()); + } else if (character == '}' || character == ',') { + throw PGExceptionFactory.newPGException( + "Invalid element in array: " + expression, SQLState.InvalidParameterValue); + } else if (character == '{') { + throw PGExceptionFactory.newPGException( + "Multidimensional arrays are not supported: " + expression, + SQLState.InvalidParameterValue); + } else { + result.add(parseUnquotedElement()); + } + + if (expression.charAt(position) == ',') { + position++; // skip ',' and continue + } else { + position++; // skip '}' and finish + break; + } + } + + checkEndOfArray(); + return result; + } + + private void skipWhitespace() { + while (position < length && isWhitespace(expression.charAt(position))) { + position++; + } + } + + private static boolean isWhitespace(char character) { + return character == ' ' + || character == '\t' + || character == '\n' + || character == '\r' + || character == '\u000B' + || character == '\f'; + } + + private void skipDimensions() { + if (expression.charAt(position) != '[') { + return; + } + int closeBracket = expression.indexOf(']', position); + if (closeBracket == -1) { + throw PGExceptionFactory.newPGException( + "Missing ']' in array dimensions: " + expression, SQLState.InvalidParameterValue); + } + validateDimension(expression.substring(position + 1, closeBracket)); + position = closeBracket + 1; + skipWhitespace(); + if (position < length && expression.charAt(position) == '[') { + throw PGExceptionFactory.newPGException( + "Multidimensional arrays are not supported: " + expression, + SQLState.InvalidParameterValue); + } + if (position >= length || expression.charAt(position) != '=') { + throw PGExceptionFactory.newPGException( + "Missing '=' after array dimensions: " + expression, SQLState.InvalidParameterValue); + } + position++; // skip '=' + skipWhitespace(); + } + + private void validateDimension(String dimension) { + int colonIndex = dimension.indexOf(':'); + try { + if (colonIndex == -1) { + long upper = Long.parseLong(dimension.trim()); + if (upper < 1) { + throw PGExceptionFactory.newPGException( + "Upper bound cannot be less than lower bound: " + expression, + SQLState.InvalidParameterValue); + } + } else { + long lower = Long.parseLong(dimension.substring(0, colonIndex).trim()); + long upper = Long.parseLong(dimension.substring(colonIndex + 1).trim()); + if (upper < lower) { + throw PGExceptionFactory.newPGException( + "Upper bound cannot be less than lower bound: " + expression, + SQLState.InvalidParameterValue); + } + } + } catch (NumberFormatException exception) { + throw PGExceptionFactory.newPGException( + "Invalid array dimensions: " + expression, SQLState.InvalidParameterValue); + } + } + + private String parseQuotedElement() { + position++; // skip opening quote '"' + int start = position; + + // Fast path: scan for closing quote or escape character. + while (position < length) { + char character = expression.charAt(position); + if (character == '"') { + String element = expression.substring(start, position); + position++; // skip closing quote + validateAfterQuotedElement(); + return element; + } + if (character == '\\') { + return parseQuotedElementWithEscapes(start); + } + position++; + } + + throw PGExceptionFactory.newPGException( + "Missing end quote character in array value: " + expression, + SQLState.InvalidParameterValue); + } + + private String parseQuotedElementWithEscapes(int start) { + StringBuilder element = new StringBuilder(position - start + 16); + element.append(expression, start, position); + boolean closed = false; + while (position < length) { + char character = expression.charAt(position); + if (character == '\\') { + position++; + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Unexpected end of array value: " + expression, SQLState.InvalidParameterValue); + } + element.append(expression.charAt(position)); + position++; + } else if (character == '"') { + closed = true; + position++; // skip closing quote + break; + } else { + element.append(character); + position++; + } + } + if (!closed) { + throw PGExceptionFactory.newPGException( + "Missing end quote character in array value: " + expression, + SQLState.InvalidParameterValue); + } + validateAfterQuotedElement(); + return element.toString(); + } + + private void validateAfterQuotedElement() { + skipWhitespace(); + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Missing '}' at end of array value: " + expression, SQLState.InvalidParameterValue); + } + char next = expression.charAt(position); + if (next != ',' && next != '}') { + throw PGExceptionFactory.newPGException( + "Incorrectly quoted array element: " + expression, SQLState.InvalidParameterValue); + } + } + + private String parseUnquotedElement() { + int start = position; + int lastNonWhitespace = start; + + // Fast path: scan until delimiter or escape. + while (position < length) { + char character = expression.charAt(position); + if (character == ',' || character == '}') { + int elementLength = lastNonWhitespace - start; + if (elementLength == 4 && expression.regionMatches(true, start, "null", 0, 4)) { + return null; + } + return expression.substring(start, lastNonWhitespace); + } + if (character == '\\') { + return parseUnquotedElementWithEscapes(start); + } + if (character == '"') { + throw PGExceptionFactory.newPGException( + "Incorrectly quoted array element: " + expression, SQLState.InvalidParameterValue); + } + if (character == '{') { + throw PGExceptionFactory.newPGException( + "Unexpected '{' character in array value: " + expression, + SQLState.InvalidParameterValue); + } + if (!isWhitespace(character)) { + lastNonWhitespace = position + 1; + } + position++; + } + + throw PGExceptionFactory.newPGException( + "Missing '}' at end of array value: " + expression, SQLState.InvalidParameterValue); + } + + private String parseUnquotedElementWithEscapes(int start) { + StringBuilder element = new StringBuilder(position - start + 16); + element.append(expression, start, position); + int nonWhitespaceLength = 0; + while (position < length) { + char character = expression.charAt(position); + if (character == '\\') { + position++; + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Unexpected end of array value: " + expression, SQLState.InvalidParameterValue); + } + element.append(expression.charAt(position)); + nonWhitespaceLength = element.length(); + position++; + } else if (character == ',' || character == '}') { + break; + } else if (character == '"') { + throw PGExceptionFactory.newPGException( + "Incorrectly quoted array element: " + expression, SQLState.InvalidParameterValue); + } else if (character == '{') { + throw PGExceptionFactory.newPGException( + "Unexpected '{' character in array value: " + expression, + SQLState.InvalidParameterValue); + } else { + element.append(character); + if (!isWhitespace(character)) { + nonWhitespaceLength = element.length(); + } + position++; + } + } + + if (position >= length) { + throw PGExceptionFactory.newPGException( + "Missing '}' at end of array value: " + expression, SQLState.InvalidParameterValue); + } + element.setLength(nonWhitespaceLength); + return element.toString(); + } + + private void checkEndOfArray() { + skipWhitespace(); + if (position < length) { + throw PGExceptionFactory.newPGException( + "Unexpected characters after array value: " + expression, SQLState.InvalidParameterValue); + } + } +} diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java index 1e30472aa1..7ac7c7d8be 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java @@ -18,6 +18,7 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.Interval; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Type; @@ -27,7 +28,6 @@ import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory; import com.google.cloud.spanner.pgadapter.error.SQLState; import com.google.cloud.spanner.pgadapter.session.SessionState; -import com.google.cloud.spanner.pgadapter.statements.SimpleParser; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import java.io.ByteArrayInputStream; @@ -147,7 +147,7 @@ public static List stringArrayToList( if (value == null) { return null; } - List values = SimpleParser.readArrayLiteral(value, elementOid == Oid.BYTEA); + List values = ArrayLiteralParser.readArrayLiteral(value); ArrayList result = new ArrayList<>(values.size()); for (String element : values) { if (element == null) { @@ -240,7 +240,8 @@ private boolean stringEquivalence(Code arrayElementType) { || arrayElementType == Code.DATE || arrayElementType == Code.STRING || arrayElementType == Code.TIMESTAMP - || arrayElementType == Code.PG_JSONB; + || arrayElementType == Code.PG_JSONB + || arrayElementType == Code.UUID; } /** @@ -446,6 +447,9 @@ public static void bind( case Oid.DATE: parametersBuilder.put(name, Value.dateArray((List) list)); break; + case Oid.INTERVAL: + parametersBuilder.put(name, Value.intervalArray((List) list)); + break; default: throw PGExceptionFactory.newPGException( "Unsupported array element type: " + elementOid, SQLState.InvalidParameterValue); diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java index 5160da2a10..c227713a94 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java @@ -19,6 +19,7 @@ import com.google.cloud.spanner.Value; import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory; import com.google.cloud.spanner.pgadapter.error.SQLState; +import com.google.cloud.spanner.pgadapter.parsers.ArrayLiteralParser; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -154,14 +155,10 @@ static class QuotedString { } String getValue() { - return getValue(false); - } - - String getValue(boolean returnRawHexValue) { if (this.value == null) { this.value = this.escaped - ? unescapeQuotedStringValue(this.rawValue, this.quote, returnRawHexValue) + ? unescapeQuotedStringValue(this.rawValue, this.quote) : quotedStringValue(this.rawValue, this.quote); } return this.value; @@ -182,18 +179,13 @@ static String quotedStringValue(String quotedString, char quoteChar) { } static String unescapeQuotedStringValue(String quotedString, char quoteChar) { - return unescapeQuotedStringValue(quotedString, quoteChar, false); - } - - static String unescapeQuotedStringValue( - String quotedString, char quoteChar, boolean returnRawHexValue) { if (quotedString.length() < 2 || quotedString.charAt(0) != quoteChar || quotedString.charAt(quotedString.length() - 1) != quoteChar) { throw PGExceptionFactory.newPGException( quotedString + " is not a valid string", SQLState.SyntaxError); } - if (!returnRawHexValue && quotedString.startsWith(quoteChar + "\\x")) { + if (quotedString.startsWith(quoteChar + "\\x")) { throw PGExceptionFactory.newPGException( "PGAdapter does not support hexadecimal byte values in string literals", SQLState.SyntaxError); @@ -410,43 +402,8 @@ public static boolean isCommand(ImmutableList commands, String query) { return true; } - public static List readArrayLiteral(String expression, boolean returnRawHexValue) { - List result = new ArrayList<>(); - SimpleParser parser = new SimpleParser(expression); - if (!parser.eatToken("{")) { - throw PGExceptionFactory.newPGException( - "Missing '{' at start of array value: " + expression, SQLState.InvalidParameterValue); - } - do { - if (result.isEmpty() && parser.peekToken("}")) { - break; - } else if (parser.eatKeyword("null")) { - result.add(null); - } else if (parser.peekToken("\"")) { - QuotedString quotedString = parser.readQuotedString('"', true); - if (quotedString == null) { - throw PGExceptionFactory.newPGException( - "Invalid string in array: " + expression, SQLState.InvalidParameterValue); - } - result.add(quotedString.getValue(returnRawHexValue)); - } else { - String unquotedString = parser.parseExpressionUntilKeyword(ImmutableList.of("}")); - if (unquotedString == null) { - throw PGExceptionFactory.newPGException( - "Invalid element in array: " + expression, SQLState.InvalidParameterValue); - } - result.add(unquotedString); - } - } while (parser.eatToken(",")); - if (!parser.eatToken("}")) { - throw PGExceptionFactory.newPGException( - "Missing '}' at end of array value: " + expression, SQLState.InvalidParameterValue); - } - if (parser.hasMoreTokens()) { - throw PGExceptionFactory.newPGException( - "Unexpected characters after array value: " + expression, SQLState.InvalidParameterValue); - } - return result; + public static List readArrayLiteral(String expression) { + return ArrayLiteralParser.readArrayLiteral(expression); } /** diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java new file mode 100644 index 0000000000..a89cb27683 --- /dev/null +++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java @@ -0,0 +1,297 @@ +// Copyright 2026 Google LLC +// +// 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.google.cloud.spanner.pgadapter.parsers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.spanner.pgadapter.error.PGException; +import com.google.cloud.spanner.pgadapter.error.SQLState; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ArrayLiteralParserTest { + + @Test + public void testEmptyArray() { + assertEquals(Collections.emptyList(), ArrayLiteralParser.readArrayLiteral("{}")); + assertEquals(Collections.emptyList(), ArrayLiteralParser.readArrayLiteral("{ }")); + assertEquals(Collections.emptyList(), ArrayLiteralParser.readArrayLiteral(" {} ")); + } + + @Test + public void testSingleElement() { + assertEquals(ImmutableList.of("1"), ArrayLiteralParser.readArrayLiteral("{1}")); + assertEquals(ImmutableList.of("foo"), ArrayLiteralParser.readArrayLiteral("{\"foo\"}")); + assertEquals(ImmutableList.of(""), ArrayLiteralParser.readArrayLiteral("{\"\"}")); + assertEquals(ImmutableList.of(" "), ArrayLiteralParser.readArrayLiteral("{\" \"}")); + } + + @Test + public void testMultipleElements() { + assertEquals(ImmutableList.of("1", "2", "3"), ArrayLiteralParser.readArrayLiteral("{1, 2, 3}")); + assertEquals(ImmutableList.of("1", "2", "3"), ArrayLiteralParser.readArrayLiteral("{1,2,3}")); + assertEquals( + ImmutableList.of("foo", "bar", "baz"), + ArrayLiteralParser.readArrayLiteral("{\"foo\", \"bar\", \"baz\"}")); + assertEquals( + ImmutableList.of("foo", "bar", "baz"), + ArrayLiteralParser.readArrayLiteral("{foo, bar, baz}")); + } + + @Test + public void testWhitespaceHandling() { + assertEquals( + ImmutableList.of("1", "2", "3"), ArrayLiteralParser.readArrayLiteral("{ 1 , 2 , 3 }")); + assertEquals( + ImmutableList.of("foo 1", "bar 2"), + ArrayLiteralParser.readArrayLiteral("{ foo 1 , bar 2 }")); + assertEquals( + ImmutableList.of(" foo ", " bar "), + ArrayLiteralParser.readArrayLiteral("{ \" foo \" , \" bar \" }")); + assertEquals( + ImmutableList.of("1", "2"), ArrayLiteralParser.readArrayLiteral("{\t1\r,\u000B2\f}")); + } + + @Test + public void testAdjacentQuotesPreserved() { + // Adjacent quotes inside quoted elements must NOT be collapsed (e.g. empty strings in JSON). + assertEquals( + ImmutableList.of("{\"k\":\"\"}"), + ArrayLiteralParser.readArrayLiteral("{\"{\\\"k\\\":\\\"\\\"}\"}")); + assertEquals( + ImmutableList.of("\"foo\"", "\"\""), + ArrayLiteralParser.readArrayLiteral("{\"\\\"foo\\\"\", \"\\\"\\\"\"}")); + } + + @Test + public void testCommentsPreservedInUnquoted() { + // Unquoted elements must not be tokenized as SQL comments. + assertEquals(ImmutableList.of("--foo"), ArrayLiteralParser.readArrayLiteral("{--foo}")); + assertEquals( + ImmutableList.of("a", "--", "b"), ArrayLiteralParser.readArrayLiteral("{a, --, b}")); + assertEquals( + ImmutableList.of("/*comment*/"), ArrayLiteralParser.readArrayLiteral("{/*comment*/}")); + assertEquals( + ImmutableList.of("a", "/*comment*/", "b"), + ArrayLiteralParser.readArrayLiteral("{a, /*comment*/, b}")); + } + + @Test + public void testEscapesAndWindowsPaths() { + // PostgreSQL array syntax escapes \c to c; backslash does not trigger Java unicode unescaping. + assertEquals( + ImmutableList.of("C:\\user\\test"), + ArrayLiteralParser.readArrayLiteral("{\"C:\\\\user\\\\test\"}")); + assertEquals(ImmutableList.of("C:\\user"), ArrayLiteralParser.readArrayLiteral("{C:\\\\user}")); + assertEquals( + ImmutableList.of("a\\n", "b\\u1234"), + ArrayLiteralParser.readArrayLiteral("{\"a\\\\n\", \"b\\\\u1234\"}")); + } + + @Test + public void testEscapedDelimiters() { + assertEquals(ImmutableList.of(",", "}"), ArrayLiteralParser.readArrayLiteral("{\\,, \\}}")); + assertEquals(ImmutableList.of("{", "}"), ArrayLiteralParser.readArrayLiteral("{\\{, \\}}")); + assertEquals( + ImmutableList.of(",", "}"), ArrayLiteralParser.readArrayLiteral("{\"\\,\", \"\\}\"}")); + } + + @Test + public void testNullHandling() { + assertEquals( + Arrays.asList("foo", "null", null, null, null, "null"), + ArrayLiteralParser.readArrayLiteral("{\"foo\", \\null, null, NULL, Null, \"null\"}")); + assertEquals(Collections.singletonList(null), ArrayLiteralParser.readArrayLiteral("{null}")); + assertEquals(ImmutableList.of("null"), ArrayLiteralParser.readArrayLiteral("{\\null}")); + assertEquals(ImmutableList.of("null"), ArrayLiteralParser.readArrayLiteral("{\"null\"}")); + assertEquals(ImmutableList.of("nullify"), ArrayLiteralParser.readArrayLiteral("{nullify}")); + assertEquals(ImmutableList.of("notnull"), ArrayLiteralParser.readArrayLiteral("{notnull}")); + } + + @Test + public void testDelimitersAndBracesInQuotedElements() { + assertEquals( + ImmutableList.of("foo,bar", "baz"), + ArrayLiteralParser.readArrayLiteral("{\"foo,bar\", \"baz\"}")); + assertEquals( + ImmutableList.of("{foo}", "}bar{"), + ArrayLiteralParser.readArrayLiteral("{\"\\{foo\\}\", \"\\}bar\\{\"}")); + assertEquals(ImmutableList.of("{", "}"), ArrayLiteralParser.readArrayLiteral("{\"{\", \"}\"}")); + } + + @Test + public void testEscapedQuotesInUnquotedElements() { + // In PostgreSQL, \" in unquoted element unescapes to literal " + assertEquals(ImmutableList.of("foo\"bar"), ArrayLiteralParser.readArrayLiteral("{foo\\\"bar}")); + } + + @Test + public void testEscapedSpaces() { + assertEquals(ImmutableList.of(" "), ArrayLiteralParser.readArrayLiteral("{\\ }")); + assertEquals(ImmutableList.of(" "), ArrayLiteralParser.readArrayLiteral("{\\ \\ }")); + assertEquals(ImmutableList.of(" a "), ArrayLiteralParser.readArrayLiteral("{\\ a\\ }")); + } + + @Test + public void testUnquotedByteaHex() { + assertEquals( + ImmutableList.of("\\x1234", "\\x5678"), + ArrayLiteralParser.readArrayLiteral("{\\\\x1234, \\\\x5678}")); + } + + @Test + public void testDimensionDecorations() { + assertEquals(ImmutableList.of("1", "2"), ArrayLiteralParser.readArrayLiteral("[1:2]={1, 2}")); + assertEquals( + ImmutableList.of("1", "2", "3"), ArrayLiteralParser.readArrayLiteral("[3]={1, 2, 3}")); + assertEquals( + ImmutableList.of("1", "2", "3", "4", "5"), + ArrayLiteralParser.readArrayLiteral("[-2:2]={1, 2, 3, 4, 5}")); + assertEquals( + ImmutableList.of("1", "2"), ArrayLiteralParser.readArrayLiteral(" [1:2] = {1, 2} ")); + } + + @Test + public void testAdditionalFeatures() { + assertEquals( + Arrays.asList("1 year", null), ArrayLiteralParser.readArrayLiteral("{1 year, NULL}")); + assertEquals( + ImmutableList.of("2026-09-25 18:30:00+02"), + ArrayLiteralParser.readArrayLiteral("{2026-09-25 18:30:00+02}")); + assertEquals( + ImmutableList.of("{\"k\": [1, 2]}"), + ArrayLiteralParser.readArrayLiteral("{\"{\\\"k\\\": [1, 2]}\"}")); + } + + @Test + public void testUnicodeWhitespacePreserved() { + // Unicode spaces (e.g. \u3000 CJK space) are not ASCII whitespace and must be preserved as + // unquoted element content according to PostgreSQL arrayfuncs.c. + assertEquals(ImmutableList.of("foo\u3000"), ArrayLiteralParser.readArrayLiteral("{foo\u3000}")); + assertEquals(ImmutableList.of("\u3000"), ArrayLiteralParser.readArrayLiteral("{\u3000}")); + assertEquals(ImmutableList.of("\u3000foo"), ArrayLiteralParser.readArrayLiteral("{\u3000foo}")); + } + + @Test + public void testUnquotedFourCharacterWords() { + assertEquals( + ImmutableList.of("word", "true", "test", "abcd"), + ArrayLiteralParser.readArrayLiteral("{word, true, test, abcd}")); + } + + @Test + public void testUnquotedSqlTokens() { + // Array unquoted elements are not SQL tokens; apostrophes, parentheses, and dollar signs must + // be + // preserved. + assertEquals( + ImmutableList.of("O'Reilly", "it's"), + ArrayLiteralParser.readArrayLiteral("{O'Reilly, it's}")); + assertEquals( + ImmutableList.of("foo(bar", "baz)qux"), + ArrayLiteralParser.readArrayLiteral("{foo(bar, baz)qux}")); + assertEquals( + ImmutableList.of("$tag$foo", "$$bar"), + ArrayLiteralParser.readArrayLiteral("{$tag$foo, $$bar}")); + assertEquals( + ImmutableList.of("null-1", "null:2"), + ArrayLiteralParser.readArrayLiteral("{null-1, null:2}")); + } + + @Test + public void testQuotedInvalidJavaUnicodeEscapes() { + // In PostgreSQL array grammar, \c unescapes to c; non-Java escape sequences like \\uZZZZ must + // not crash with IllegalArgumentException. + assertEquals( + ImmutableList.of("\\uZZZZ"), ArrayLiteralParser.readArrayLiteral("{\"\\\\uZZZZ\"}")); + assertEquals( + ImmutableList.of("C:\\users\\admin"), + ArrayLiteralParser.readArrayLiteral("{\"C:\\\\users\\\\admin\"}")); + assertEquals(ImmutableList.of("\\u12"), ArrayLiteralParser.readArrayLiteral("{\"\\\\u12\"}")); + } + + @Test + public void testUnquotedWithInternalSpacesAndEscapes() { + assertEquals( + ImmutableList.of("foo bar baz"), ArrayLiteralParser.readArrayLiteral("{foo bar\\ baz }")); + } + + @Test + public void testErrorConditions() { + assertInvalidParameter(null); + assertInvalidParameter(""); + assertInvalidParameter(" "); + assertInvalidParameter("1, 2"); + assertInvalidParameter("{1, 2"); + assertInvalidParameter("1, 2}"); + assertInvalidParameter("{"); + assertInvalidParameter("{ "); + assertInvalidParameter("{1, 2} extra token"); + + // Missing elements or unclosed delimiters + assertInvalidParameter("{1,}"); + assertInvalidParameter("{,1}"); + assertInvalidParameter("{1,,2}"); + assertInvalidParameter("{,}"); + assertInvalidParameter("{1,"); + + // Misquoted elements + assertInvalidParameter("{\"foo\"bar}"); + assertInvalidParameter("{foo\"bar}"); + assertInvalidParameter("{\"foo}"); + assertInvalidParameter("{\"foo\\"); + assertInvalidParameter("{\"foo\\nbar"); + assertInvalidParameter("{\"foo\""); + assertInvalidParameter("{\"foo\\\"bar\"baz}"); + assertInvalidParameter("{\"foo\\\"bar"); + + // Unquoted elements with invalid characters or unclosed states + assertInvalidParameter("{foo{bar}"); + assertInvalidParameter("{foo\\bar\\"); + assertInvalidParameter("{foo\\bar\"baz}"); + assertInvalidParameter("{foo\\bar{baz}"); + assertInvalidParameter("{foo\\bar"); + + // Multidimensional arrays not supported + assertInvalidParameter("{{1, 2}, {3, 4}}"); + + // Malformed dimension headers + assertInvalidParameter("[1:2{1, 2}"); + assertInvalidParameter("[1:2] {1, 2}"); + assertInvalidParameter("[1:2]="); + assertInvalidParameter("[1:2]"); + assertInvalidParameter("[1:2] 123"); + assertInvalidParameter("[1:2][1:2]={{1, 2}, {3, 4}}"); + assertInvalidParameter("[1:2] [1:2]={1, 2}"); + assertInvalidParameter("[]={1}"); + assertInvalidParameter("[abc]={1}"); + assertInvalidParameter("[2:1]={1}"); + assertInvalidParameter("[1:2:3]={1}"); + assertInvalidParameter("[-1]={1}"); + assertInvalidParameter("[0]={1}"); + } + + private static void assertInvalidParameter(String expression) { + PGException exception = + assertThrows(PGException.class, () -> ArrayLiteralParser.readArrayLiteral(expression)); + assertEquals(SQLState.InvalidParameterValue, exception.getSQLState()); + } +} diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java index 3b1607a223..bfaeb59a6c 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java @@ -23,6 +23,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Interval; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Struct; @@ -34,9 +35,12 @@ import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode; import com.google.cloud.spanner.pgadapter.session.SessionState; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.nio.charset.StandardCharsets; import java.time.ZoneId; import java.util.Arrays; +import java.util.List; +import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -489,10 +493,72 @@ public void testNullTextArray() { ArrayParser.stringArrayToList(null, Oid.UNSPECIFIED, mock(SessionState.class), false)); assertNull( ArrayParser.stringArrayToList(null, Oid.UNSPECIFIED, mock(SessionState.class), true)); - assertNull( - ArrayParser.stringArrayToList(null, Oid.UNSPECIFIED, mock(SessionState.class), false)); - assertNull( - ArrayParser.stringArrayToList(null, Oid.UNSPECIFIED, mock(SessionState.class), true)); + } + + @Test + public void testStringArrayToList_jsonWithAdjacentQuotes() { + List result = + ArrayParser.stringArrayToList( + "{\"{\\\"k\\\":\\\"\\\"}\"}", Oid.JSONB, mock(SessionState.class), false); + assertEquals(ImmutableList.of("{\"k\":\"\"}"), result); + } + + @Test + public void testStringArrayToList_unquotedWithDashes() { + List result = + ArrayParser.stringArrayToList( + "{--foo, a, --, b}", Oid.TEXT, mock(SessionState.class), false); + assertEquals(ImmutableList.of("--foo", "a", "--", "b"), result); + } + + @Test + public void testBindIntervalArray() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + Interval interval = Interval.parseFromString("P1Y2M3DT4H5M6S"); + ArrayParser.bind(builder, "p1", ImmutableList.of(interval), Oid.INTERVAL); + ImmutableMap parameters = builder.build(); + assertEquals(Value.intervalArray(ImmutableList.of(interval)), parameters.get("p1")); + } + + @Test + public void testParserBindIntervalArray() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + Parser.bind( + builder, + "p1", + "{1 year 2 months, 01:00:00}".getBytes(StandardCharsets.UTF_8), + Oid.INTERVAL_ARRAY, + FormatCode.TEXT, + mock(SessionState.class)); + ImmutableMap parameters = builder.build(); + assertEquals( + Value.intervalArray( + ImmutableList.of( + IntervalParser.toInterval("1 year 2 months"), + IntervalParser.toInterval("01:00:00"))), + parameters.get("p1")); + } + + @Test + public void testStringParseUuid() { + UUID uuid = UUID.fromString("1e4d8cd3-099d-4954-a5c1-85272713c3e9"); + ArrayParser parser = + new ArrayParser( + createArrayResultSet(Type.uuid(), Value.uuidArray(ImmutableList.of(uuid))), + 0, + mock(SessionState.class)); + assertEquals("{\"1e4d8cd3-099d-4954-a5c1-85272713c3e9\"}", parser.stringParse()); + } + + @Test + public void testStringParseInterval() { + Interval interval = IntervalParser.toInterval("1 year 2 months"); + ArrayParser parser = + new ArrayParser( + createArrayResultSet(Type.interval(), Value.intervalArray(ImmutableList.of(interval))), + 0, + mock(SessionState.class)); + assertEquals("{14 mons 0 days 00:00:0.000000}", parser.stringParse()); } static ResultSet createArrayResultSet(Type arrayElementType, Value value) { diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SimpleParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SimpleParserTest.java index 42cfd6e3e2..21e119ed32 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SimpleParserTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SimpleParserTest.java @@ -499,28 +499,48 @@ public void testParserTableOrIndexName() { @Test public void testReadArrayLiteral() { - assertEquals(ImmutableList.of(), SimpleParser.readArrayLiteral("{}", true)); - assertEquals(ImmutableList.of("foo"), SimpleParser.readArrayLiteral("{\"foo\"}", true)); + assertEquals(ImmutableList.of(), SimpleParser.readArrayLiteral("{}")); + assertEquals(ImmutableList.of("foo"), SimpleParser.readArrayLiteral("{\"foo\"}")); assertEquals( - ImmutableList.of("foo", "bar"), SimpleParser.readArrayLiteral("{\"foo\", \"bar\"}", true)); + ImmutableList.of("foo", "bar"), SimpleParser.readArrayLiteral("{\"foo\", \"bar\"}")); assertEquals( Arrays.asList("foo", "bar", null), - SimpleParser.readArrayLiteral("{\"foo\", \"bar\", null}", true)); - assertEquals(ImmutableList.of("1", "2"), SimpleParser.readArrayLiteral("{1, 2}", true)); - assertEquals(ImmutableList.of("1", "2"), SimpleParser.readArrayLiteral("{\"1\", \"2\"}", true)); + SimpleParser.readArrayLiteral("{\"foo\", \"bar\", null}")); + assertEquals(ImmutableList.of("1", "2"), SimpleParser.readArrayLiteral("{1, 2}")); + assertEquals(ImmutableList.of("1", "2"), SimpleParser.readArrayLiteral("{\"1\", \"2\"}")); assertEquals( ImmutableList.of("{\"foo\": \"bar\"}"), - SimpleParser.readArrayLiteral("{\"{\\\"foo\\\": \\\"bar\\\"}\"}", true)); + SimpleParser.readArrayLiteral("{\"{\\\"foo\\\": \\\"bar\\\"}\"}")); - assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("1, 2", true)); - assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{1, 2", true)); - assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("1, 2}", true)); - assertThrows( - PGException.class, () -> SimpleParser.readArrayLiteral("{1, 2} extra token", true)); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("1, 2")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{1, 2")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("1, 2}")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{1, 2} extra token")); - assertEquals(ImmutableList.of("foo", "bar"), SimpleParser.readArrayLiteral("{foo, bar}", true)); + assertEquals(ImmutableList.of("foo", "bar"), SimpleParser.readArrayLiteral("{foo, bar}")); assertEquals( - ImmutableList.of("foo 1", "bar 2"), SimpleParser.readArrayLiteral("{foo 1, bar 2}", true)); + ImmutableList.of("foo 1", "bar 2"), SimpleParser.readArrayLiteral("{foo 1, bar 2}")); + assertEquals( + ImmutableList.of("{\"k\":\"\"}"), + SimpleParser.readArrayLiteral("{\"{\\\"k\\\":\\\"\\\"}\"}")); + assertEquals(ImmutableList.of("--foo"), SimpleParser.readArrayLiteral("{--foo}")); + assertEquals(ImmutableList.of("a", "--", "b"), SimpleParser.readArrayLiteral("{a, --, b}")); + assertEquals(ImmutableList.of("/*comment*/"), SimpleParser.readArrayLiteral("{/*comment*/}")); + assertEquals(ImmutableList.of("C:\\user"), SimpleParser.readArrayLiteral("{\"C:\\\\user\"}")); + assertEquals( + ImmutableList.of("a\\n", "b\\u1234"), + SimpleParser.readArrayLiteral("{\"a\\\\n\", \"b\\\\u1234\"}")); + assertEquals(ImmutableList.of(",", "}"), SimpleParser.readArrayLiteral("{\\,, \\}}")); + assertEquals( + Arrays.asList("foo", "null", null), + SimpleParser.readArrayLiteral("{\"foo\", \\null, null}")); + assertEquals(ImmutableList.of("1", "2"), SimpleParser.readArrayLiteral("[1:2]={1, 2}")); + + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{1,}")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{,1}")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{1,,2}")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{\"foo\"bar}")); + assertThrows(PGException.class, () -> SimpleParser.readArrayLiteral("{foo\"bar}")); } @Test From 7aca314a1ccda16546ff20754377503e032109b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 28 Sep 2026 15:39:48 +0200 Subject: [PATCH 2/2] fix: parse and validate array dimensions --- .../pgadapter/parsers/ArrayLiteralParser.java | 38 +++++++++++++--- .../parsers/ArrayLiteralParserTest.java | 45 +++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java index db5a76b7c9..fd36977789 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParser.java @@ -28,7 +28,8 @@ * arrayfuncs.c}. Specifically: * *
    - *
  • Supports optional dimension decorations (e.g. {@code [1:2]={...}}). + *
  • Supports optional dimension decorations (e.g. {@code [1:2]={...}}) and verifies that parsed + * element count matches declared dimensions. *
  • Quoted elements preserve adjacent quotes (unlike SQL string literals) and only unescape * literal backslash escapes {@code \c -> c}. *
  • Unquoted elements are not parsed as SQL expressions or comments, preserving tokens like @@ -66,7 +67,7 @@ private List parse() { throw PGExceptionFactory.newPGException( "Missing '{' at start of array value: " + expression, SQLState.InvalidParameterValue); } - skipDimensions(); + int expectedElements = parseDimensions(); if (position >= length || expression.charAt(position) != '{') { throw PGExceptionFactory.newPGException( "Missing '{' at start of array value: " + expression, SQLState.InvalidParameterValue); @@ -76,11 +77,17 @@ private List parse() { skipWhitespace(); if (position < length && expression.charAt(position) == '}') { position++; // skip '}' + if (expectedElements >= 0) { + throw PGExceptionFactory.newPGException( + "Specified array dimensions do not match array contents: " + expression, + SQLState.InvalidParameterValue); + } checkEndOfArray(); return new ArrayList<>(0); } - List result = new ArrayList<>(); + List result = + expectedElements > 0 ? new ArrayList<>(expectedElements) : new ArrayList<>(); while (true) { skipWhitespace(); if (position >= length) { @@ -109,6 +116,11 @@ private List parse() { } } + if (expectedElements >= 0 && result.size() != expectedElements) { + throw PGExceptionFactory.newPGException( + "Specified array dimensions do not match array contents: " + expression, + SQLState.InvalidParameterValue); + } checkEndOfArray(); return result; } @@ -128,16 +140,16 @@ private static boolean isWhitespace(char character) { || character == '\f'; } - private void skipDimensions() { + private int parseDimensions() { if (expression.charAt(position) != '[') { - return; + return -1; } int closeBracket = expression.indexOf(']', position); if (closeBracket == -1) { throw PGExceptionFactory.newPGException( "Missing ']' in array dimensions: " + expression, SQLState.InvalidParameterValue); } - validateDimension(expression.substring(position + 1, closeBracket)); + int expectedElements = validateDimension(expression.substring(position + 1, closeBracket)); position = closeBracket + 1; skipWhitespace(); if (position < length && expression.charAt(position) == '[') { @@ -151,9 +163,10 @@ private void skipDimensions() { } position++; // skip '=' skipWhitespace(); + return expectedElements; } - private void validateDimension(String dimension) { + private int validateDimension(String dimension) { int colonIndex = dimension.indexOf(':'); try { if (colonIndex == -1) { @@ -163,6 +176,11 @@ private void validateDimension(String dimension) { "Upper bound cannot be less than lower bound: " + expression, SQLState.InvalidParameterValue); } + if (upper > Integer.MAX_VALUE) { + throw PGExceptionFactory.newPGException( + "Invalid array dimensions: " + expression, SQLState.InvalidParameterValue); + } + return (int) upper; } else { long lower = Long.parseLong(dimension.substring(0, colonIndex).trim()); long upper = Long.parseLong(dimension.substring(colonIndex + 1).trim()); @@ -171,6 +189,12 @@ private void validateDimension(String dimension) { "Upper bound cannot be less than lower bound: " + expression, SQLState.InvalidParameterValue); } + long count = upper - lower + 1; + if (count < 1 || count > Integer.MAX_VALUE) { + throw PGExceptionFactory.newPGException( + "Invalid array dimensions: " + expression, SQLState.InvalidParameterValue); + } + return (int) count; } } catch (NumberFormatException exception) { throw PGExceptionFactory.newPGException( diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java index a89cb27683..48f1085364 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayLiteralParserTest.java @@ -167,6 +167,41 @@ public void testDimensionDecorations() { ArrayLiteralParser.readArrayLiteral("[-2:2]={1, 2, 3, 4, 5}")); assertEquals( ImmutableList.of("1", "2"), ArrayLiteralParser.readArrayLiteral(" [1:2] = {1, 2} ")); + assertEquals(ImmutableList.of("1", "2"), ArrayLiteralParser.readArrayLiteral("[0:1]={1, 2}")); + assertEquals(ImmutableList.of("1"), ArrayLiteralParser.readArrayLiteral("[0:0]={1}")); + assertEquals(ImmutableList.of("1"), ArrayLiteralParser.readArrayLiteral("[1:1]={1}")); + } + + @Test + public void testDimensionLengthMismatch() { + PGException exception = + assertThrows( + PGException.class, () -> ArrayLiteralParser.readArrayLiteral("[1:2]={1, 2, 3}")); + assertEquals(SQLState.InvalidParameterValue, exception.getSQLState()); + assertEquals( + "Specified array dimensions do not match array contents: [1:2]={1, 2, 3}", + exception.getMessage()); + + exception = + assertThrows(PGException.class, () -> ArrayLiteralParser.readArrayLiteral("[1:2]={}")); + assertEquals(SQLState.InvalidParameterValue, exception.getSQLState()); + assertEquals( + "Specified array dimensions do not match array contents: [1:2]={}", exception.getMessage()); + + exception = + assertThrows(PGException.class, () -> ArrayLiteralParser.readArrayLiteral("[3]={1, 2}")); + assertEquals(SQLState.InvalidParameterValue, exception.getSQLState()); + assertEquals( + "Specified array dimensions do not match array contents: [3]={1, 2}", + exception.getMessage()); + + exception = + assertThrows( + PGException.class, () -> ArrayLiteralParser.readArrayLiteral("[-2:1]={1, 2, 3}")); + assertEquals(SQLState.InvalidParameterValue, exception.getSQLState()); + assertEquals( + "Specified array dimensions do not match array contents: [-2:1]={1, 2, 3}", + exception.getMessage()); } @Test @@ -287,6 +322,16 @@ public void testErrorConditions() { assertInvalidParameter("[1:2:3]={1}"); assertInvalidParameter("[-1]={1}"); assertInvalidParameter("[0]={1}"); + + // Dimension length does not match parsed elements + assertInvalidParameter("[1:2]={1, 2, 3}"); + assertInvalidParameter("[1:2]={1}"); + assertInvalidParameter("[1:2]={}"); + assertInvalidParameter("[3]={1, 2}"); + assertInvalidParameter("[3]={1, 2, 3, 4}"); + assertInvalidParameter("[3]={}"); + assertInvalidParameter("[-2:1]={1, 2, 3}"); + assertInvalidParameter("[-2:1]={1, 2, 3, 4, 5}"); } private static void assertInvalidParameter(String expression) {