Skip to content

Commit f403d99

Browse files
authored
fix: PostgreSQL supports newline in quoted literals and identifiers (#1731)
* fix: PostgreSQL supports newline in quoted literals and identifiers PostgreSQL supports newline characters in string literals and quoted identifiers. Trying to execute a statement with a string literal or quoted identifier that contained a newline character would cause an 'Unclosed string literal' error. Fixes #1730 * fix: typo
1 parent 468d849 commit f403d99

File tree

4 files changed

+126
-138
lines changed

4 files changed

+126
-138
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ private boolean statementStartsWith(String sql, Iterable<String> checkStatements
460460
static final char HYPHEN = '-';
461461
static final char DASH = '#';
462462
static final char SLASH = '/';
463-
static final char ASTERIKS = '*';
463+
static final char ASTERISK = '*';
464464
static final char DOLLAR = '$';
465465

466466
/**

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java

Lines changed: 87 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -56,95 +56,44 @@ protected boolean supportsExplain() {
5656
@Override
5757
String removeCommentsAndTrimInternal(String sql) {
5858
Preconditions.checkNotNull(sql);
59-
String currentTag = null;
60-
boolean isInQuoted = false;
6159
boolean isInSingleLineComment = false;
6260
int multiLineCommentLevel = 0;
63-
char startQuote = 0;
64-
boolean lastCharWasEscapeChar = false;
6561
StringBuilder res = new StringBuilder(sql.length());
6662
int index = 0;
6763
while (index < sql.length()) {
6864
char c = sql.charAt(index);
69-
if (isInQuoted) {
70-
if ((c == '\n' || c == '\r') && startQuote != DOLLAR) {
71-
throw SpannerExceptionFactory.newSpannerException(
72-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
73-
} else if (c == startQuote) {
74-
if (c == DOLLAR) {
75-
// Check if this is the end of the current dollar quoted string.
76-
String tag = parseDollarQuotedString(sql, index + 1);
77-
if (tag != null && tag.equals(currentTag)) {
78-
index += tag.length() + 1;
79-
res.append(c);
80-
res.append(tag);
81-
isInQuoted = false;
82-
startQuote = 0;
83-
}
84-
} else if (lastCharWasEscapeChar) {
85-
lastCharWasEscapeChar = false;
86-
} else if (sql.length() > index + 1 && sql.charAt(index + 1) == startQuote) {
87-
// This is an escaped quote (e.g. 'foo''bar')
88-
res.append(c);
89-
index++;
90-
} else {
91-
isInQuoted = false;
92-
startQuote = 0;
93-
}
94-
} else if (c == '\\') {
95-
lastCharWasEscapeChar = true;
96-
} else {
97-
lastCharWasEscapeChar = false;
65+
if (isInSingleLineComment) {
66+
if (c == '\n') {
67+
isInSingleLineComment = false;
68+
// Include the line feed in the result.
69+
res.append(c);
70+
}
71+
} else if (multiLineCommentLevel > 0) {
72+
if (sql.length() > index + 1 && c == ASTERISK && sql.charAt(index + 1) == SLASH) {
73+
multiLineCommentLevel--;
74+
index++;
75+
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) {
76+
multiLineCommentLevel++;
77+
index++;
9878
}
99-
res.append(c);
10079
} else {
101-
// We are not in a quoted string.
102-
if (isInSingleLineComment) {
103-
if (c == '\n') {
104-
isInSingleLineComment = false;
105-
// Include the line feed in the result.
106-
res.append(c);
107-
}
108-
} else if (multiLineCommentLevel > 0) {
109-
if (sql.length() > index + 1 && c == ASTERIKS && sql.charAt(index + 1) == SLASH) {
110-
multiLineCommentLevel--;
111-
index++;
112-
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERIKS) {
113-
multiLineCommentLevel++;
114-
index++;
115-
}
80+
// Check for -- which indicates the start of a single-line comment.
81+
if (sql.length() > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN) {
82+
// This is a single line comment.
83+
isInSingleLineComment = true;
84+
index += 2;
85+
continue;
86+
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) {
87+
multiLineCommentLevel++;
88+
index += 2;
89+
continue;
11690
} else {
117-
// Check for -- which indicates the start of a single-line comment.
118-
if (sql.length() > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN) {
119-
// This is a single line comment.
120-
isInSingleLineComment = true;
121-
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERIKS) {
122-
multiLineCommentLevel++;
123-
index++;
124-
} else {
125-
if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE) {
126-
isInQuoted = true;
127-
startQuote = c;
128-
} else if (c == DOLLAR) {
129-
currentTag = parseDollarQuotedString(sql, index + 1);
130-
if (currentTag != null) {
131-
isInQuoted = true;
132-
startQuote = DOLLAR;
133-
index += currentTag.length() + 1;
134-
res.append(c);
135-
res.append(currentTag);
136-
}
137-
}
138-
res.append(c);
139-
}
91+
index = skip(sql, index, res);
92+
continue;
14093
}
14194
}
14295
index++;
14396
}
144-
if (isInQuoted) {
145-
throw SpannerExceptionFactory.newSpannerException(
146-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
147-
}
14897
if (multiLineCommentLevel > 0) {
14998
throw SpannerExceptionFactory.newSpannerException(
15099
ErrorCode.INVALID_ARGUMENT,
@@ -184,73 +133,78 @@ String removeStatementHint(String sql) {
184133
ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramChar, String sql) {
185134
Preconditions.checkNotNull(sql);
186135
final String namedParamPrefix = "$";
187-
String currentTag = null;
188-
boolean isInQuoted = false;
189-
char startQuote = 0;
190-
boolean lastCharWasEscapeChar = false;
191136
StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(paramChar, sql));
192137
int index = 0;
193138
int paramIndex = 1;
194139
while (index < sql.length()) {
195140
char c = sql.charAt(index);
196-
if (isInQuoted) {
197-
if ((c == '\n' || c == '\r') && startQuote != DOLLAR) {
198-
throw SpannerExceptionFactory.newSpannerException(
199-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
200-
} else if (c == startQuote) {
201-
if (c == DOLLAR) {
202-
// Check if this is the end of the current dollar quoted string.
203-
String tag = parseDollarQuotedString(sql, index + 1);
204-
if (tag != null && tag.equals(currentTag)) {
205-
index += tag.length() + 1;
206-
named.append(c);
207-
named.append(tag);
208-
isInQuoted = false;
209-
startQuote = 0;
210-
}
211-
} else if (lastCharWasEscapeChar) {
212-
lastCharWasEscapeChar = false;
213-
} else if (sql.length() > index + 1 && sql.charAt(index + 1) == startQuote) {
214-
// This is an escaped quote (e.g. 'foo''bar')
215-
named.append(c);
216-
index++;
217-
} else {
218-
isInQuoted = false;
219-
startQuote = 0;
141+
if (c == paramChar) {
142+
named.append(namedParamPrefix).append(paramIndex);
143+
paramIndex++;
144+
index++;
145+
} else {
146+
index = skip(sql, index, named);
147+
}
148+
}
149+
return new ParametersInfo(paramIndex - 1, named.toString());
150+
}
151+
152+
private int skip(String sql, int currentIndex, StringBuilder result) {
153+
char currentChar = sql.charAt(currentIndex);
154+
if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) {
155+
result.append(currentChar);
156+
return skipQuoted(sql, currentIndex, currentChar, result);
157+
} else if (currentChar == DOLLAR) {
158+
String dollarTag = parseDollarQuotedString(sql, currentIndex + 1);
159+
if (dollarTag != null) {
160+
result.append(currentChar).append(dollarTag).append(currentChar);
161+
return skipQuoted(
162+
sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result);
163+
}
164+
}
165+
166+
result.append(currentChar);
167+
return currentIndex + 1;
168+
}
169+
170+
private int skipQuoted(String sql, int startIndex, char startQuote, StringBuilder result) {
171+
return skipQuoted(sql, startIndex, startQuote, null, result);
172+
}
173+
174+
private int skipQuoted(
175+
String sql, int startIndex, char startQuote, String dollarTag, StringBuilder result) {
176+
boolean lastCharWasEscapeChar = false;
177+
int currentIndex = startIndex + 1;
178+
while (currentIndex < sql.length()) {
179+
char currentChar = sql.charAt(currentIndex);
180+
if (currentChar == startQuote) {
181+
if (currentChar == DOLLAR) {
182+
// Check if this is the end of the current dollar quoted string.
183+
String tag = parseDollarQuotedString(sql, currentIndex + 1);
184+
if (tag != null && tag.equals(dollarTag)) {
185+
result.append(currentChar).append(tag).append(currentChar);
186+
return currentIndex + tag.length() + 2;
220187
}
221-
} else if (c == '\\') {
222-
lastCharWasEscapeChar = true;
223-
} else {
188+
} else if (lastCharWasEscapeChar) {
224189
lastCharWasEscapeChar = false;
225-
}
226-
named.append(c);
227-
} else {
228-
if (c == paramChar) {
229-
named.append(namedParamPrefix + paramIndex);
230-
paramIndex++;
190+
} else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) {
191+
// This is an escaped quote (e.g. 'foo''bar')
192+
result.append(currentChar).append(currentChar);
193+
currentIndex += 2;
194+
continue;
231195
} else {
232-
if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE) {
233-
isInQuoted = true;
234-
startQuote = c;
235-
} else if (c == DOLLAR) {
236-
currentTag = parseDollarQuotedString(sql, index + 1);
237-
if (currentTag != null) {
238-
isInQuoted = true;
239-
startQuote = DOLLAR;
240-
index += currentTag.length() + 1;
241-
named.append(c);
242-
named.append(currentTag);
243-
}
244-
}
245-
named.append(c);
196+
result.append(currentChar);
197+
return currentIndex + 1;
246198
}
199+
} else if (currentChar == '\\') {
200+
lastCharWasEscapeChar = true;
201+
} else {
202+
lastCharWasEscapeChar = false;
247203
}
248-
index++;
249-
}
250-
if (isInQuoted) {
251-
throw SpannerExceptionFactory.newSpannerException(
252-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
204+
currentIndex++;
205+
result.append(currentChar);
253206
}
254-
return new ParametersInfo(paramIndex - 1, named.toString());
207+
throw SpannerExceptionFactory.newSpannerException(
208+
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
255209
}
256210
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ String removeCommentsAndTrimInternal(String sql) {
106106
res.append(c);
107107
}
108108
} else if (isInMultiLineComment) {
109-
if (sql.length() > index + 1 && c == ASTERIKS && sql.charAt(index + 1) == SLASH) {
109+
if (sql.length() > index + 1 && c == ASTERISK && sql.charAt(index + 1) == SLASH) {
110110
isInMultiLineComment = false;
111111
index++;
112112
}
@@ -115,7 +115,7 @@ String removeCommentsAndTrimInternal(String sql) {
115115
|| (sql.length() > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN)) {
116116
// This is a single line comment.
117117
isInSingleLineComment = true;
118-
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERIKS) {
118+
} else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERISK) {
119119
isInMultiLineComment = true;
120120
index++;
121121
} else {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,33 @@ public void testRemoveComments() {
127127
parser.removeCommentsAndTrim(
128128
"/* This\nis\na\nmulti\nline\ncomment */\nSELECT * FROM FOO"))
129129
.isEqualTo("SELECT * FROM FOO");
130+
131+
assertEquals(
132+
"SELECT \"FOO\" FROM \"BAR\" WHERE name='test'",
133+
parser.removeCommentsAndTrim(
134+
"-- Single line comment\nSELECT \"FOO\" FROM \"BAR\" WHERE name='test'"));
135+
assertEquals(
136+
"SELECT \"FOO\" FROM \"BAR\" WHERE name='test' and id=1",
137+
parser.removeCommentsAndTrim(
138+
"/* Multi\n"
139+
+ "line\n"
140+
+ "comment\n"
141+
+ "*/SELECT \"FOO\" FROM \"BAR\" WHERE name='test' and id=1"));
142+
143+
if (dialect == Dialect.POSTGRESQL) {
144+
// PostgreSQL allows string literals and quoted identifiers to contain newline characters.
145+
assertEquals(
146+
"SELECT \"FOO\nBAR\" FROM \"BAR\" WHERE name='test\ntest'",
147+
parser.removeCommentsAndTrim(
148+
"-- Single line comment\nSELECT \"FOO\nBAR\" FROM \"BAR\" WHERE name='test\ntest'"));
149+
assertEquals(
150+
"SELECT \"FOO\nBAR\" FROM \"BAR\" WHERE name='test\ntest' and id=1",
151+
parser.removeCommentsAndTrim(
152+
"/* Multi\n"
153+
+ "line\n"
154+
+ "comment\n"
155+
+ "*/SELECT \"FOO\nBAR\" FROM \"BAR\" WHERE name='test\ntest' and id=1"));
156+
}
130157
}
131158

132159
@Test
@@ -1221,9 +1248,16 @@ public void testPostgreSQLDialectDialectConvertPositionalParametersToNamedParame
12211248
.sqlWithNamedParameters)
12221249
.isEqualTo("$1$$?it\\'?s \n ?it\\'?s$$$2");
12231250

1224-
assertUnclosedLiteral("?'?it\\'?s \n ?it\\'?s'?");
1251+
// Note: PostgreSQL allows a single-quoted string literal to contain line feeds.
1252+
assertEquals(
1253+
"$1'?it\\'?s \n ?it\\'?s'$2",
1254+
parser.convertPositionalParametersToNamedParameters('?', "?'?it\\'?s \n ?it\\'?s'?")
1255+
.sqlWithNamedParameters);
12251256
assertUnclosedLiteral("?'?it\\'?s \n ?it\\'?s?");
1226-
assertUnclosedLiteral("?'''?it\\'?s \n ?it\\'?s'?");
1257+
assertEquals(
1258+
"$1'''?it\\'?s \n ?it\\'?s'$2",
1259+
parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\'?s \n ?it\\'?s'?")
1260+
.sqlWithNamedParameters);
12271261

12281262
assertThat(
12291263
parser.convertPositionalParametersToNamedParameters(

0 commit comments

Comments
 (0)