Skip to content

Commit b782725

Browse files
authored
perf: keep comments when searching for params (googleapis#2951)
Keep all comments in the SQL string in place when converting positional parameters to named parameters. This reduces the amount of string operations that are needed for each query that is executed, and also enables actually sending comments from the client to Spanner when using positional parameters (e.g. in JDBC). This is step 3 in the refactoring to share more code between the SpannerStatementParser and PostgreSQLStatementParser.
1 parent 81ec3e0 commit b782725

File tree

5 files changed

+361
-299
lines changed

5 files changed

+361
-299
lines changed

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

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -634,36 +634,33 @@ public static class ParametersInfo {
634634

635635
/**
636636
* Converts all positional parameters (?) in the given sql string into named parameters. The
637-
* parameters are named @p1, @p2, etc. This method is used when converting a JDBC statement that
638-
* uses positional parameters to a Cloud Spanner {@link Statement} instance that requires named
639-
* parameters. The input SQL string may not contain any comments, except for PostgreSQL-dialect
640-
* SQL strings.
637+
* parameters are named @p1, @p2, etc. for GoogleSQL, and $1, $2, etc. for PostgreSQL. This method
638+
* is used when converting a JDBC statement that uses positional parameters to a Cloud Spanner
639+
* {@link Statement} instance that requires named parameters.
641640
*
642-
* @param sql The sql string that should be converted
643-
* @return A {@link ParametersInfo} object containing a string with named parameters instead of
644-
* positional parameters and the number of parameters.
645-
* @throws SpannerException If the input sql string contains an unclosed string/byte literal.
646-
*/
647-
@InternalApi
648-
abstract ParametersInfo convertPositionalParametersToNamedParametersInternal(
649-
char paramChar, String sql);
650-
651-
/**
652-
* Converts all positional parameters (?) in the given sql string into named parameters. The
653-
* parameters are named @p1, @p2, etc. This method is used when converting a JDBC statement that
654-
* uses positional parameters to a Cloud Spanner {@link Statement} instance that requires named
655-
* parameters. The input SQL string may not contain any comments. There is an exception case if
656-
* the statement starts with a GSQL comment which forces it to be interpreted as a GoogleSql
657-
* statement.
658-
*
659-
* @param sql The sql string without comments that should be converted
641+
* @param sql The sql string that should be converted to use named parameters
660642
* @return A {@link ParametersInfo} object containing a string with named parameters instead of
661643
* positional parameters and the number of parameters.
662644
* @throws SpannerException If the input sql string contains an unclosed string/byte literal.
663645
*/
664646
@InternalApi
665647
public ParametersInfo convertPositionalParametersToNamedParameters(char paramChar, String sql) {
666-
return convertPositionalParametersToNamedParametersInternal(paramChar, sql);
648+
Preconditions.checkNotNull(sql);
649+
final String namedParamPrefix = getQueryParameterPrefix();
650+
StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(paramChar, sql));
651+
int index = 0;
652+
int paramIndex = 1;
653+
while (index < sql.length()) {
654+
char c = sql.charAt(index);
655+
if (c == paramChar) {
656+
named.append(namedParamPrefix).append(paramIndex);
657+
paramIndex++;
658+
index++;
659+
} else {
660+
index = skip(sql, index, named);
661+
}
662+
}
663+
return new ParametersInfo(paramIndex - 1, named.toString());
667664
}
668665

669666
/** Convenience method that is used to estimate the number of parameters in a SQL statement. */
@@ -700,7 +697,8 @@ public boolean checkReturningClause(String sql) {
700697
}
701698

702699
/**
703-
* <<<<<<< HEAD Returns true if this dialect supports nested comments.
700+
* <<<<<<< HEAD Returns true if this dialect supports nested comments. ======= <<<<<<< HEAD
701+
* Returns true if this dialect supports nested comments. >>>>>>> main
704702
*
705703
* <ul>
706704
* <li>This method should return false for dialects that consider this to be a valid comment:
@@ -755,6 +753,9 @@ public boolean checkReturningClause(String sql) {
755753
*/
756754
abstract boolean supportsLineFeedInQuotedString();
757755

756+
/** Returns the query parameter prefix that should be used for this dialect. */
757+
abstract String getQueryParameterPrefix();
758+
758759
/**
759760
* Returns true for characters that can be used as the first character in unquoted identifiers.
760761
*/

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

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ boolean supportsLineFeedInQuotedString() {
8888
return true;
8989
}
9090

91+
@Override
92+
String getQueryParameterPrefix() {
93+
return "$";
94+
}
95+
9196
/**
9297
* Removes comments from and trims the given sql statement. PostgreSQL supports two types of
9398
* comments:
@@ -181,27 +186,6 @@ String removeStatementHint(String sql) {
181186
return sql;
182187
}
183188

184-
@InternalApi
185-
@Override
186-
ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramChar, String sql) {
187-
Preconditions.checkNotNull(sql);
188-
final String namedParamPrefix = "$";
189-
StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(paramChar, sql));
190-
int index = 0;
191-
int paramIndex = 1;
192-
while (index < sql.length()) {
193-
char c = sql.charAt(index);
194-
if (c == paramChar) {
195-
named.append(namedParamPrefix).append(paramIndex);
196-
paramIndex++;
197-
index++;
198-
} else {
199-
index = skip(sql, index, named);
200-
}
201-
}
202-
return new ParametersInfo(paramIndex - 1, named.toString());
203-
}
204-
205189
/**
206190
* Note: This is an internal API and breaking changes can be made without prior notice.
207191
*

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

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ boolean supportsLineFeedInQuotedString() {
9090
return false;
9191
}
9292

93+
@Override
94+
String getQueryParameterPrefix() {
95+
return "@p";
96+
}
97+
9398
/**
9499
* Removes comments from and trims the given sql statement. Spanner supports three types of
95100
* comments:
@@ -250,68 +255,6 @@ String removeStatementHint(String sql) {
250255
return sql;
251256
}
252257

253-
@InternalApi
254-
@Override
255-
ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramChar, String sql) {
256-
boolean isInQuoted = false;
257-
char startQuote = 0;
258-
boolean lastCharWasEscapeChar = false;
259-
boolean isTripleQuoted = false;
260-
int paramIndex = 1;
261-
StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(paramChar, sql));
262-
for (int index = 0; index < sql.length(); index++) {
263-
char c = sql.charAt(index);
264-
if (isInQuoted) {
265-
if ((c == '\n' || c == '\r') && !isTripleQuoted) {
266-
throw SpannerExceptionFactory.newSpannerException(
267-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
268-
} else if (c == startQuote) {
269-
if (lastCharWasEscapeChar) {
270-
lastCharWasEscapeChar = false;
271-
} else if (isTripleQuoted) {
272-
if (sql.length() > index + 2
273-
&& sql.charAt(index + 1) == startQuote
274-
&& sql.charAt(index + 2) == startQuote) {
275-
isInQuoted = false;
276-
startQuote = 0;
277-
isTripleQuoted = false;
278-
}
279-
} else {
280-
isInQuoted = false;
281-
startQuote = 0;
282-
}
283-
} else if (c == '\\') {
284-
lastCharWasEscapeChar = true;
285-
} else {
286-
lastCharWasEscapeChar = false;
287-
}
288-
named.append(c);
289-
} else {
290-
if (c == paramChar) {
291-
named.append("@p" + paramIndex);
292-
paramIndex++;
293-
} else {
294-
if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE || c == BACKTICK_QUOTE) {
295-
isInQuoted = true;
296-
startQuote = c;
297-
// check whether it is a triple-quote
298-
if (sql.length() > index + 2
299-
&& sql.charAt(index + 1) == startQuote
300-
&& sql.charAt(index + 2) == startQuote) {
301-
isTripleQuoted = true;
302-
}
303-
}
304-
named.append(c);
305-
}
306-
}
307-
}
308-
if (isInQuoted) {
309-
throw SpannerExceptionFactory.newSpannerException(
310-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
311-
}
312-
return new ParametersInfo(paramIndex - 1, named.toString());
313-
}
314-
315258
private boolean isReturning(String sql, int index) {
316259
return (index >= 1)
317260
&& (index + 12 <= sql.length())

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package com.google.cloud.spanner.connection;
1818

19+
import static com.google.cloud.spanner.connection.StatementParserTest.assertUnclosedLiteral;
1920
import static org.junit.Assert.assertEquals;
2021

2122
import com.google.cloud.spanner.Dialect;
23+
import com.google.cloud.spanner.connection.StatementParserTest.CommentInjector;
2224
import org.junit.Test;
2325
import org.junit.runner.RunWith;
2426
import org.junit.runners.JUnit4;
@@ -80,4 +82,158 @@ public void testSkip() {
8082
assertEquals("'foo\\''", skip("r'foo\\'' ", 1));
8183
assertEquals("'''foo\\'\\'\\'bar'''", skip("'''foo\\'\\'\\'bar''' ", 0));
8284
}
85+
86+
@Test
87+
public void testConvertPositionalParametersToNamedParameters() {
88+
AbstractStatementParser parser =
89+
AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL);
90+
91+
for (String comment :
92+
new String[] {
93+
"-- test comment\n",
94+
"/* another test comment */",
95+
"/* comment\nwith\nmultiple\nlines\n */",
96+
"/* comment /* with nested */ comment */"
97+
}) {
98+
for (CommentInjector injector : CommentInjector.values()) {
99+
assertEquals(
100+
injector.inject("select * %sfrom foo where name=@p1", comment),
101+
parser.convertPositionalParametersToNamedParameters(
102+
'?', injector.inject("select * %sfrom foo where name=?", comment))
103+
.sqlWithNamedParameters);
104+
assertEquals(
105+
injector.inject("@p1%s'?test?\"?test?\"?'@p2", comment),
106+
parser.convertPositionalParametersToNamedParameters(
107+
'?', injector.inject("?%s'?test?\"?test?\"?'?", comment))
108+
.sqlWithNamedParameters);
109+
assertEquals(
110+
injector.inject("@p1'?it\\'?s'%s@p2", comment),
111+
parser.convertPositionalParametersToNamedParameters(
112+
'?', injector.inject("?'?it\\'?s'%s?", comment))
113+
.sqlWithNamedParameters);
114+
assertEquals(
115+
injector.inject("@p1'?it\\\"?s'%s@p2", comment),
116+
parser.convertPositionalParametersToNamedParameters(
117+
'?', injector.inject("?'?it\\\"?s'%s?", comment))
118+
.sqlWithNamedParameters);
119+
assertEquals(
120+
injector.inject("@p1\"?it\\\"?s\"%s@p2", comment),
121+
parser.convertPositionalParametersToNamedParameters(
122+
'?', injector.inject("?\"?it\\\"?s\"%s?", comment))
123+
.sqlWithNamedParameters);
124+
assertEquals(
125+
injector.inject("@p1%s'''?it\\''?s'''@p2", comment),
126+
parser.convertPositionalParametersToNamedParameters(
127+
'?', injector.inject("?%s'''?it\\''?s'''?", comment))
128+
.sqlWithNamedParameters);
129+
assertEquals(
130+
injector.inject("@p1\"\"\"?it\\\"\"?s\"\"\"%s@p2", comment),
131+
parser.convertPositionalParametersToNamedParameters(
132+
'?', injector.inject("?\"\"\"?it\\\"\"?s\"\"\"%s?", comment))
133+
.sqlWithNamedParameters);
134+
135+
// GoogleSQL does not support dollar-quoted strings, so these are all ignored.
136+
assertEquals(
137+
injector.inject("@p1$$@p2it$@p3s$$%s@p4", comment),
138+
parser.convertPositionalParametersToNamedParameters(
139+
'?', injector.inject("?$$?it$?s$$%s?", comment))
140+
.sqlWithNamedParameters);
141+
assertEquals(
142+
injector.inject("@p1$tag$@p2it$$@p3s$tag$%s@p4", comment),
143+
parser.convertPositionalParametersToNamedParameters(
144+
'?', injector.inject("?$tag$?it$$?s$tag$%s?", comment))
145+
.sqlWithNamedParameters);
146+
assertEquals(
147+
injector.inject("@p1%s$$@p2it\\'?s \t ?it\\'?s'$$@p3", comment),
148+
parser.convertPositionalParametersToNamedParameters(
149+
'?', injector.inject("?%s$$?it\\'?s \t ?it\\'?s'$$?", comment))
150+
.sqlWithNamedParameters);
151+
152+
// Note: GoogleSQL does not allowa a single-quoted string literal to contain line feeds.
153+
assertUnclosedLiteral(parser, injector.inject("?'?it\\''?s \n ?it\\''?s'%s?", comment));
154+
assertEquals(
155+
"@p1'?it\\''@p2s \n @p3it\\''@p4s@p5",
156+
parser.convertPositionalParametersToNamedParameters('?', "?'?it\\''?s \n ?it\\''?s?")
157+
.sqlWithNamedParameters);
158+
assertEquals(
159+
injector.inject("@p1%s'''?it\\''?s \n ?it\\''?s'''@p2", comment),
160+
parser.convertPositionalParametersToNamedParameters(
161+
'?', injector.inject("?%s'''?it\\''?s \n ?it\\''?s'''?", comment))
162+
.sqlWithNamedParameters);
163+
164+
assertEquals(
165+
injector.inject(
166+
"select 1, @p1, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=@p2 and col2='test' and col3=@p3 and col4='?' and col5=\"?\" and col6='?''?''?'",
167+
comment),
168+
parser.convertPositionalParametersToNamedParameters(
169+
'?',
170+
injector.inject(
171+
"select 1, ?, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'",
172+
comment))
173+
.sqlWithNamedParameters);
174+
175+
assertEquals(
176+
injector.inject(
177+
"select * "
178+
+ "%sfrom foo "
179+
+ "where name=@p1 "
180+
+ "and col2 like @p2 "
181+
+ "and col3 > @p3",
182+
comment),
183+
parser.convertPositionalParametersToNamedParameters(
184+
'?',
185+
injector.inject(
186+
"select * "
187+
+ "%sfrom foo "
188+
+ "where name=? "
189+
+ "and col2 like ? "
190+
+ "and col3 > ?",
191+
comment))
192+
.sqlWithNamedParameters);
193+
assertEquals(
194+
injector.inject("select * " + "from foo " + "where id between @p1%s and @p2", comment),
195+
parser.convertPositionalParametersToNamedParameters(
196+
'?',
197+
injector.inject(
198+
"select * " + "from foo " + "where id between ?%s and ?", comment))
199+
.sqlWithNamedParameters);
200+
assertEquals(
201+
injector.inject("select * " + "from foo " + "limit @p1 %s offset @p2", comment),
202+
parser.convertPositionalParametersToNamedParameters(
203+
'?',
204+
injector.inject("select * " + "from foo " + "limit ? %s offset ?", comment))
205+
.sqlWithNamedParameters);
206+
assertEquals(
207+
injector.inject(
208+
"select * "
209+
+ "from foo "
210+
+ "where col1=@p1 "
211+
+ "and col2 like @p2 "
212+
+ " %s "
213+
+ "and col3 > @p3 "
214+
+ "and col4 < @p4 "
215+
+ "and col5 != @p5 "
216+
+ "and col6 not in (@p6, @p7, @p8) "
217+
+ "and col7 in (@p9, @p10, @p11) "
218+
+ "and col8 between @p12 and @p13",
219+
comment),
220+
parser.convertPositionalParametersToNamedParameters(
221+
'?',
222+
injector.inject(
223+
"select * "
224+
+ "from foo "
225+
+ "where col1=? "
226+
+ "and col2 like ? "
227+
+ " %s "
228+
+ "and col3 > ? "
229+
+ "and col4 < ? "
230+
+ "and col5 != ? "
231+
+ "and col6 not in (?, ?, ?) "
232+
+ "and col7 in (?, ?, ?) "
233+
+ "and col8 between ? and ?",
234+
comment))
235+
.sqlWithNamedParameters);
236+
}
237+
}
238+
}
83239
}

0 commit comments

Comments
 (0)