Skip to content

Commit 3247a05

Browse files
authored
refactor: move skip methods to abstract parser (#2948)
Move the PostgreSQL skip methods from the PostgreSQL parser to the abstract parser. This is step 1 in refactoring the GoogleSQL and PostgreSQL parser so they can share more code. The eventual goal is to allow the GoogleSQL parser to be able to handle SQL string without having to remove the comments from the string first.
1 parent 1e45237 commit 3247a05

File tree

2 files changed

+166
-147
lines changed

2 files changed

+166
-147
lines changed

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

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.concurrent.Callable;
4141
import java.util.logging.Level;
4242
import java.util.logging.Logger;
43+
import javax.annotation.Nullable;
4344

4445
/**
4546
* Internal class for the Spanner Connection API.
@@ -696,4 +697,169 @@ static int countOccurrencesOf(char c, String string) {
696697
public boolean checkReturningClause(String sql) {
697698
return checkReturningClauseInternal(sql);
698699
}
700+
701+
/**
702+
* Returns true for characters that can be used as the first character in unquoted identifiers.
703+
*/
704+
boolean isValidIdentifierFirstChar(char c) {
705+
return Character.isLetter(c) || c == UNDERSCORE;
706+
}
707+
708+
/** Returns true for characters that can be used in unquoted identifiers. */
709+
boolean isValidIdentifierChar(char c) {
710+
return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR;
711+
}
712+
713+
/** Reads a dollar-quoted string literal from position index in the given sql string. */
714+
String parseDollarQuotedString(String sql, int index) {
715+
// Look ahead to the next dollar sign (if any). Everything in between is the quote tag.
716+
StringBuilder tag = new StringBuilder();
717+
while (index < sql.length()) {
718+
char c = sql.charAt(index);
719+
if (c == DOLLAR) {
720+
return tag.toString();
721+
}
722+
if (!isValidIdentifierChar(c)) {
723+
break;
724+
}
725+
tag.append(c);
726+
index++;
727+
}
728+
return null;
729+
}
730+
731+
/**
732+
* Skips the next character, literal, identifier, or comment in the given sql string from the
733+
* given index. The skipped characters are added to result if it is not null.
734+
*/
735+
int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
736+
char currentChar = sql.charAt(currentIndex);
737+
if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) {
738+
appendIfNotNull(result, currentChar);
739+
return skipQuoted(sql, currentIndex, currentChar, result);
740+
} else if (currentChar == DOLLAR) {
741+
String dollarTag = parseDollarQuotedString(sql, currentIndex + 1);
742+
if (dollarTag != null) {
743+
appendIfNotNull(result, currentChar, dollarTag, currentChar);
744+
return skipQuoted(
745+
sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result);
746+
}
747+
} else if (currentChar == HYPHEN
748+
&& sql.length() > (currentIndex + 1)
749+
&& sql.charAt(currentIndex + 1) == HYPHEN) {
750+
return skipSingleLineComment(sql, currentIndex, result);
751+
} else if (currentChar == SLASH
752+
&& sql.length() > (currentIndex + 1)
753+
&& sql.charAt(currentIndex + 1) == ASTERISK) {
754+
return skipMultiLineComment(sql, currentIndex, result);
755+
}
756+
757+
appendIfNotNull(result, currentChar);
758+
return currentIndex + 1;
759+
}
760+
761+
/** Skips a single-line comment from startIndex and adds it to result if result is not null. */
762+
static int skipSingleLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
763+
int endIndex = sql.indexOf('\n', startIndex + 2);
764+
if (endIndex == -1) {
765+
endIndex = sql.length();
766+
} else {
767+
// Include the newline character.
768+
endIndex++;
769+
}
770+
appendIfNotNull(result, sql.substring(startIndex, endIndex));
771+
return endIndex;
772+
}
773+
774+
/** Skips a multi-line comment from startIndex and adds it to result if result is not null. */
775+
static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
776+
// Current position is start + '/*'.length().
777+
int pos = startIndex + 2;
778+
// PostgreSQL allows comments to be nested. That is, the following is allowed:
779+
// '/* test /* inner comment */ still a comment */'
780+
int level = 1;
781+
while (pos < sql.length()) {
782+
if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) {
783+
level++;
784+
}
785+
if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
786+
level--;
787+
if (level == 0) {
788+
pos += 2;
789+
appendIfNotNull(result, sql.substring(startIndex, pos));
790+
return pos;
791+
}
792+
}
793+
pos++;
794+
}
795+
appendIfNotNull(result, sql.substring(startIndex));
796+
return sql.length();
797+
}
798+
799+
/** Skips a quoted string from startIndex. */
800+
private int skipQuoted(
801+
String sql, int startIndex, char startQuote, @Nullable StringBuilder result) {
802+
return skipQuoted(sql, startIndex, startQuote, null, result);
803+
}
804+
805+
/**
806+
* Skips a quoted string from startIndex. The quote character is assumed to be $ if dollarTag is
807+
* not null.
808+
*/
809+
private int skipQuoted(
810+
String sql,
811+
int startIndex,
812+
char startQuote,
813+
String dollarTag,
814+
@Nullable StringBuilder result) {
815+
int currentIndex = startIndex + 1;
816+
while (currentIndex < sql.length()) {
817+
char currentChar = sql.charAt(currentIndex);
818+
if (currentChar == startQuote) {
819+
if (currentChar == DOLLAR) {
820+
// Check if this is the end of the current dollar quoted string.
821+
String tag = parseDollarQuotedString(sql, currentIndex + 1);
822+
if (tag != null && tag.equals(dollarTag)) {
823+
appendIfNotNull(result, currentChar, dollarTag, currentChar);
824+
return currentIndex + tag.length() + 2;
825+
}
826+
} else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) {
827+
// This is an escaped quote (e.g. 'foo''bar')
828+
appendIfNotNull(result, currentChar);
829+
appendIfNotNull(result, currentChar);
830+
currentIndex += 2;
831+
continue;
832+
} else {
833+
appendIfNotNull(result, currentChar);
834+
return currentIndex + 1;
835+
}
836+
}
837+
currentIndex++;
838+
appendIfNotNull(result, currentChar);
839+
}
840+
throw SpannerExceptionFactory.newSpannerException(
841+
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
842+
}
843+
844+
/** Appends the given character to result if result is not null. */
845+
private void appendIfNotNull(@Nullable StringBuilder result, char currentChar) {
846+
if (result != null) {
847+
result.append(currentChar);
848+
}
849+
}
850+
851+
/** Appends the given suffix to result if result is not null. */
852+
private static void appendIfNotNull(@Nullable StringBuilder result, String suffix) {
853+
if (result != null) {
854+
result.append(suffix);
855+
}
856+
}
857+
858+
/** Appends the given prefix, tag, and suffix to result if result is not null. */
859+
private static void appendIfNotNull(
860+
@Nullable StringBuilder result, char prefix, String tag, char suffix) {
861+
if (result != null) {
862+
result.append(prefix).append(tag).append(suffix);
863+
}
864+
}
699865
}

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

Lines changed: 0 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.HashSet;
2727
import java.util.Set;
2828
import java.util.regex.Pattern;
29-
import javax.annotation.Nullable;
3029

3130
@InternalApi
3231
public class PostgreSQLStatementParser extends AbstractStatementParser {
@@ -136,23 +135,6 @@ String removeCommentsAndTrimInternal(String sql) {
136135
return res.toString().trim();
137136
}
138137

139-
String parseDollarQuotedString(String sql, int index) {
140-
// Look ahead to the next dollar sign (if any). Everything in between is the quote tag.
141-
StringBuilder tag = new StringBuilder();
142-
while (index < sql.length()) {
143-
char c = sql.charAt(index);
144-
if (c == DOLLAR) {
145-
return tag.toString();
146-
}
147-
if (!isValidIdentifierChar(c)) {
148-
break;
149-
}
150-
tag.append(c);
151-
index++;
152-
}
153-
return null;
154-
}
155-
156138
/** PostgreSQL does not support statement hints. */
157139
@Override
158140
String removeStatementHint(String sql) {
@@ -220,135 +202,6 @@ public Set<String> getQueryParameters(String sql) {
220202
return parameters;
221203
}
222204

223-
private int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
224-
char currentChar = sql.charAt(currentIndex);
225-
if (currentChar == SINGLE_QUOTE || currentChar == DOUBLE_QUOTE) {
226-
appendIfNotNull(result, currentChar);
227-
return skipQuoted(sql, currentIndex, currentChar, result);
228-
} else if (currentChar == DOLLAR) {
229-
String dollarTag = parseDollarQuotedString(sql, currentIndex + 1);
230-
if (dollarTag != null) {
231-
appendIfNotNull(result, currentChar, dollarTag, currentChar);
232-
return skipQuoted(
233-
sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result);
234-
}
235-
} else if (currentChar == HYPHEN
236-
&& sql.length() > (currentIndex + 1)
237-
&& sql.charAt(currentIndex + 1) == HYPHEN) {
238-
return skipSingleLineComment(sql, currentIndex, result);
239-
} else if (currentChar == SLASH
240-
&& sql.length() > (currentIndex + 1)
241-
&& sql.charAt(currentIndex + 1) == ASTERISK) {
242-
return skipMultiLineComment(sql, currentIndex, result);
243-
}
244-
245-
appendIfNotNull(result, currentChar);
246-
return currentIndex + 1;
247-
}
248-
249-
static int skipSingleLineComment(String sql, int currentIndex, @Nullable StringBuilder result) {
250-
int endIndex = sql.indexOf('\n', currentIndex + 2);
251-
if (endIndex == -1) {
252-
endIndex = sql.length();
253-
} else {
254-
// Include the newline character.
255-
endIndex++;
256-
}
257-
appendIfNotNull(result, sql.substring(currentIndex, endIndex));
258-
return endIndex;
259-
}
260-
261-
static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
262-
// Current position is start + '/*'.length().
263-
int pos = startIndex + 2;
264-
// PostgreSQL allows comments to be nested. That is, the following is allowed:
265-
// '/* test /* inner comment */ still a comment */'
266-
int level = 1;
267-
while (pos < sql.length()) {
268-
if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) {
269-
level++;
270-
}
271-
if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
272-
level--;
273-
if (level == 0) {
274-
pos += 2;
275-
appendIfNotNull(result, sql.substring(startIndex, pos));
276-
return pos;
277-
}
278-
}
279-
pos++;
280-
}
281-
appendIfNotNull(result, sql.substring(startIndex));
282-
return sql.length();
283-
}
284-
285-
private int skipQuoted(
286-
String sql, int startIndex, char startQuote, @Nullable StringBuilder result) {
287-
return skipQuoted(sql, startIndex, startQuote, null, result);
288-
}
289-
290-
private int skipQuoted(
291-
String sql,
292-
int startIndex,
293-
char startQuote,
294-
String dollarTag,
295-
@Nullable StringBuilder result) {
296-
int currentIndex = startIndex + 1;
297-
while (currentIndex < sql.length()) {
298-
char currentChar = sql.charAt(currentIndex);
299-
if (currentChar == startQuote) {
300-
if (currentChar == DOLLAR) {
301-
// Check if this is the end of the current dollar quoted string.
302-
String tag = parseDollarQuotedString(sql, currentIndex + 1);
303-
if (tag != null && tag.equals(dollarTag)) {
304-
appendIfNotNull(result, currentChar, dollarTag, currentChar);
305-
return currentIndex + tag.length() + 2;
306-
}
307-
} else if (sql.length() > currentIndex + 1 && sql.charAt(currentIndex + 1) == startQuote) {
308-
// This is an escaped quote (e.g. 'foo''bar')
309-
appendIfNotNull(result, currentChar);
310-
appendIfNotNull(result, currentChar);
311-
currentIndex += 2;
312-
continue;
313-
} else {
314-
appendIfNotNull(result, currentChar);
315-
return currentIndex + 1;
316-
}
317-
}
318-
currentIndex++;
319-
appendIfNotNull(result, currentChar);
320-
}
321-
throw SpannerExceptionFactory.newSpannerException(
322-
ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql);
323-
}
324-
325-
private void appendIfNotNull(@Nullable StringBuilder result, char currentChar) {
326-
if (result != null) {
327-
result.append(currentChar);
328-
}
329-
}
330-
331-
private static void appendIfNotNull(@Nullable StringBuilder result, String suffix) {
332-
if (result != null) {
333-
result.append(suffix);
334-
}
335-
}
336-
337-
private void appendIfNotNull(
338-
@Nullable StringBuilder result, char prefix, String tag, char suffix) {
339-
if (result != null) {
340-
result.append(prefix).append(tag).append(suffix);
341-
}
342-
}
343-
344-
private boolean isValidIdentifierFirstChar(char c) {
345-
return Character.isLetter(c) || c == UNDERSCORE;
346-
}
347-
348-
private boolean isValidIdentifierChar(char c) {
349-
return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR;
350-
}
351-
352205
private boolean checkCharPrecedingReturning(char ch) {
353206
return (ch == SPACE)
354207
|| (ch == SINGLE_QUOTE)

0 commit comments

Comments
 (0)