Repository: GoogleCloudPlatform/pgadapter
Pull request #152 (2022-05-08 15:46:48)

fix: split statement did not correctly parse escaped quotes
Splitting multiple statements in a single SQL string did not handle escaped quotes correctly. PostgreSQL uses double quotes to escape a quote inside of a quoted string. That is;

`select ''''` will produce a string containing one single quote.

`select """Test""" from foo` will select the column named `"Test"` (case-sensitive).

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#152](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (ba72294) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/47d222952ebb86f95d5901b8bd4ea694cde301eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (47d2229) will **decrease** coverage by `0.16%`.
> The diff coverage is `100.00%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #152      +/-   ##
========================================================
- Coverage                 71.39%   71.22%   -0.17%     
+ Complexity                  774      771       -3     
========================================================
  Files                        82       82              
  Lines                      2905     2895      -10     
  Branches                    310      304       -6     
========================================================
- Hits                       2074     2062      -12     
- Misses                      701      704       +3     
+ Partials                    130      129       -1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `71.22% <100.00%> (-0.17%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...er/pgadapter/statements/IntermediateStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvSW50ZXJtZWRpYXRlU3RhdGVtZW50LmphdmE=) | `92.30% <100.00%> (+1.16%)` | :arrow_up: |
| [...cloud/spanner/pgadapter/utils/StatementParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL1N0YXRlbWVudFBhcnNlci5qYXZh) | `85.71% <100.00%> (-4.61%)` | :arrow_down: |
| [...cloud/spanner/pgadapter/parsers/NumericParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvTnVtZXJpY1BhcnNlci5qYXZh) | `83.78% <0.00%> (-5.41%)` | :arrow_down: |
| [...gle/cloud/spanner/pgadapter/ConnectionHandler.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL0Nvbm5lY3Rpb25IYW5kbGVyLmphdmE=) | `71.79% <0.00%> (-1.68%)` | :arrow_down: |
| [...r/pgadapter/wireoutput/RowDescriptionResponse.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVvdXRwdXQvUm93RGVzY3JpcHRpb25SZXNwb25zZS5qYXZh) | `81.03% <0.00%> (-0.33%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform). Last update [47d2229...ba72294](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/152?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform).


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/StatementParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/StatementParser.java
index 45d0aff0..92d4f050 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/StatementParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/StatementParser.java
@@ -26,23 +26,10 @@ public class StatementParser {
   /**
    * Simple method to escape SQL. Ultimately it is preferred that a user uses PreparedStatements but
    * for the case of psql emulation, we apply this to provide a simple layer of protection to the
-   * user. Here we simple escape the single quote if it is not currently escaped. Note that the only
-   * reason we are doing it manually is because StringEscapeUtils deprecated escapeSql.
+   * user. This method simply duplicates all single quotes (i.e. ' becomes '').
    */
   public static String singleQuoteEscape(String sql) {
-    StringBuilder result = new StringBuilder();
-    boolean currentCharacterIsEscaped = false;
-    for (char currentCharacter : sql.toCharArray()) {
-      if (currentCharacterIsEscaped) {
-        currentCharacterIsEscaped = false;
-      } else if (currentCharacter == '\\') {
-        currentCharacterIsEscaped = true;
-      } else if (currentCharacter == '\'') {
-        result.append('\\');
-      }
-      result.append(currentCharacter);
-    }
-    return result.toString();
+    return sql.replace("'", "''");
   }
 
   /** Determines the (update) command that was received from the sql string. */


Test patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
index f7ba7ae1..8f588f0b 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
@@ -70,6 +70,7 @@ public class IntermediateStatement {
 
   private static final char STATEMENT_DELIMITER = ';';
   private static final char SINGLE_QUOTE = '\'';
+  private static final char DOUBLE_QUOTE = '"';
 
   public IntermediateStatement(
       OptionsMetadata options,
@@ -135,7 +136,7 @@ public class IntermediateStatement {
     return builder.build();
   }
 
-  private static ImmutableList<String> splitStatements(String sql) {
+  static ImmutableList<String> splitStatements(String sql) {
     // First check trivial cases with only one statement.
     int firstIndexOfDelimiter = sql.indexOf(STATEMENT_DELIMITER);
     if (firstIndexOfDelimiter == -1) {
@@ -148,26 +149,33 @@ public class IntermediateStatement {
     ImmutableList.Builder<String> builder = ImmutableList.builder();
     // TODO: Fix this parsing, as it does not take all types of quotes into consideration.
     boolean insideQuotes = false;
-    boolean currentCharacterIsEscaped = false;
+    char quoteChar = 0;
     int index = 0;
     for (int i = 0; i < sql.length(); ++i) {
-      // ignore escaped quotes
-      if (sql.charAt(i) == SINGLE_QUOTE && !currentCharacterIsEscaped) {
-        insideQuotes = !insideQuotes;
-      }
-      // skip semicolon inside quotes
-      if (sql.charAt(i) == STATEMENT_DELIMITER && !insideQuotes) {
-        String stmt = sql.substring(index, i).trim();
-        // Statements with only ';' character are empty and dropped.
-        if (stmt.length() > 0) {
-          builder.add(stmt);
+      if (insideQuotes) {
+        if (sql.charAt(i) == quoteChar) {
+          if (sql.length() > (i + 1) && sql.charAt(i + 1) == quoteChar) {
+            // This is an escaped quote. Skip one ahead.
+            i++;
+          } else {
+            insideQuotes = false;
+          }
+        }
+      } else {
+        if (sql.charAt(i) == SINGLE_QUOTE || sql.charAt(i) == DOUBLE_QUOTE) {
+          quoteChar = sql.charAt(i);
+          insideQuotes = true;
+        } else {
+          // skip semicolon inside quotes
+          if (sql.charAt(i) == STATEMENT_DELIMITER) {
+            String stmt = sql.substring(index, i).trim();
+            // Statements with only ';' character are empty and dropped.
+            if (stmt.length() > 0) {
+              builder.add(stmt);
+            }
+            index = i + 1;
+          }
         }
-        index = i + 1;
-      }
-      if (currentCharacterIsEscaped) {
-        currentCharacterIsEscaped = false;
-      } else if (sql.charAt(i) == '\\') {
-        currentCharacterIsEscaped = true;
       }
     }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/PSQLTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/PSQLTest.java
index 902ef17a..f35a8fa7 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/PSQLTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/PSQLTest.java
@@ -14,6 +14,8 @@
 
 package com.google.cloud.spanner.pgadapter;
 
+import static org.junit.Assert.assertEquals;
+
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
@@ -24,7 +26,6 @@ import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.statements.MatcherStatement;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -82,7 +83,7 @@ public class PSQLTest {
             + "      AND n.nspname !~ '^pg_toast'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT"
             + " t.table_schema as \"Schema\","
             + " t.table_name as \"Name\","
@@ -96,7 +97,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -111,7 +112,7 @@ public class PSQLTest {
             + "WHERE c.relname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 2, 3;";
-    String result =
+    String expected =
         "SELECT"
             + " t.table_name as oid,"
             + " 'public' as nspname,"
@@ -126,7 +127,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -141,7 +142,7 @@ public class PSQLTest {
             + "WHERE c.relname OPERATOR(pg_catalog.~) '^(bobby'; DROP TABLE USERS; SELECT')$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 2, 3;";
-    String result =
+    String expected =
         "SELECT"
             + " t.table_name as oid,"
             + " 'public' as nspname,"
@@ -151,12 +152,12 @@ public class PSQLTest {
             + " WHERE"
             + " t.table_schema='public'"
             + " AND"
-            + " LOWER(t.table_name) = LOWER('bobby\\'; DROP TABLE USERS; SELECT\\'')";
+            + " LOWER(t.table_name) = LOWER('bobby''; DROP TABLE USERS; SELECT''')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -166,7 +167,7 @@ public class PSQLTest {
         "SELECT relchecks, relkind, relhasindex, relhasrules, reltriggers <> 0, false, false,"
             + " relhasoids, false as relispartition, '', ''\n"
             + "FROM pg_catalog.pg_class WHERE oid = '-2264987671676060158';";
-    String result =
+    String expected =
         "SELECT"
             + " 0 as relcheck,"
             + " 'r' as relkind,"
@@ -182,7 +183,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -201,7 +202,7 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_attribute a\n"
             + "WHERE a.attrelid = '-1' AND a.attnum > 0 AND NOT a.attisdropped\n"
             + "ORDER BY a.attnum;";
-    String result =
+    String expected =
         "SELECT"
             + " t.column_name as attname,"
             + " t.data_type as format_type,"
@@ -219,7 +220,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -239,7 +240,7 @@ public class PSQLTest {
             + "WHERE a.attrelid = 'bobby'; DROP TABLE USERS; SELECT'' AND a.attnum > 0 AND NOT"
             + " a.attisdropped\n"
             + "ORDER BY a.attnum;";
-    String result =
+    String expected =
         "SELECT"
             + " t.column_name as attname,"
             + " t.data_type as format_type,"
@@ -252,12 +253,12 @@ public class PSQLTest {
             + " information_schema.columns AS t"
             + " WHERE"
             + " t.table_schema='public'"
-            + " AND t.table_name = 'bobby\\'; DROP TABLE USERS; SELECT\\''";
+            + " AND t.table_name = 'bobby''; DROP TABLE USERS; SELECT'''";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -267,12 +268,12 @@ public class PSQLTest {
         "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i"
             + " WHERE c.oid=i.inhparent AND i.inhrelid = '-2264987671676060158' AND c.relkind !="
             + " 'p' ORDER BY inhseqno;";
-    String result = "SELECT 1 LIMIT 0";
+    String expected = "SELECT 1 LIMIT 0";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -282,12 +283,12 @@ public class PSQLTest {
         "SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i"
             + " WHERE c.oid=i.inhrelid AND i.inhparent = '-2264987671676060158' ORDER BY"
             + " c.relname;";
-    String result = "SELECT 1 LIMIT 0";
+    String expected = "SELECT 1 LIMIT 0";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -300,12 +301,12 @@ public class PSQLTest {
             + "       pg_catalog.array_to_string(d.datacl, '\\n') AS \"Access privileges\"\n"
             + "FROM pg_catalog.pg_database d\n"
             + "ORDER BY 1;";
-    String result = "SELECT '' AS Name";
+    String expected = "SELECT '' AS Name";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -319,12 +320,12 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_database d\n"
             + "WHERE d.datname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "ORDER BY 1;";
-    String result = "SELECT '' AS Name";
+    String expected = "SELECT '' AS Name";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -338,14 +339,14 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_database d\n"
             + "WHERE d.datname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "ORDER BY 1;";
-    String result = "SELECT '' AS Name";
+    String expected = "SELECT '' AS Name";
 
     // TODO: Add Connection#getDatabase() to Connection API and test here what happens if that
     // method throws an exception.
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -367,12 +368,12 @@ public class PSQLTest {
             + "      AND n.nspname !~ '^pg_toast'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result = "SELECT * FROM information_schema.tables";
+    String expected = "SELECT * FROM information_schema.tables";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -393,13 +394,13 @@ public class PSQLTest {
             + "  AND c.relname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT * FROM information_schema.tables WHERE LOWER(table_name) = LOWER('users')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -420,14 +421,14 @@ public class PSQLTest {
             + "  AND c.relname OPERATOR(pg_catalog.~) '^(bobby'; DROP TABLE USERS; SELECT')$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT * FROM information_schema.tables WHERE LOWER(table_name) ="
-            + " LOWER('bobby\\'; DROP TABLE USERS; SELECT\\'')";
+            + " LOWER('bobby''; DROP TABLE USERS; SELECT''')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -452,13 +453,13 @@ public class PSQLTest {
             + "      AND n.nspname !~ '^pg_toast'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT table_catalog, table_schema, table_name, index_name, index_type, parent_table_name, is_unique, is_null_filtered, index_state, spanner_is_managed FROM information_schema.indexes";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -482,14 +483,14 @@ public class PSQLTest {
             + "  AND c.relname OPERATOR(pg_catalog.~) '^(index)$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT table_catalog, table_schema, table_name, index_name, index_type, parent_table_name, is_unique, is_null_filtered, index_state, spanner_is_managed FROM information_schema.indexes WHERE LOWER(index_name) ="
             + " LOWER('index')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -513,14 +514,14 @@ public class PSQLTest {
             + "  AND c.relname OPERATOR(pg_catalog.~) '^(bobby'; DROP TABLE USERS; SELECT')$'\n"
             + "  AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "ORDER BY 1,2;";
-    String result =
+    String expected =
         "SELECT table_catalog, table_schema, table_name, index_name, index_type, parent_table_name, is_unique, is_null_filtered, index_state, spanner_is_managed FROM information_schema.indexes WHERE LOWER(index_name) ="
-            + " LOWER('bobby\\'; DROP TABLE USERS; SELECT\\'')";
+            + " LOWER('bobby''; DROP TABLE USERS; SELECT''')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -532,12 +533,12 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_namespace n\n"
             + "WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'\n"
             + "ORDER BY 1;";
-    String result = "SELECT * FROM information_schema.schemata";
+    String expected = "SELECT * FROM information_schema.schemata";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -549,13 +550,13 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_namespace n\n"
             + "WHERE n.nspname OPERATOR(pg_catalog.~) '^(schema)$'\n"
             + "ORDER BY 1;";
-    String result =
+    String expected =
         "SELECT * FROM information_schema.schemata WHERE LOWER(schema_name) = LOWER('schema')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -567,14 +568,14 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_namespace n\n"
             + "WHERE n.nspname OPERATOR(pg_catalog.~) '^(bobby'; DROP TABLE USERS; SELECT')$'\n"
             + "ORDER BY 1;";
-    String result =
-        "SELECT * FROM information_schema.schemata WHERE LOWER(schema_name) = LOWER('bobby\\'; DROP"
-            + " TABLE USERS; SELECT\\'')";
+    String expected =
+        "SELECT * FROM information_schema.schemata WHERE LOWER(schema_name) = LOWER('bobby''; DROP"
+            + " TABLE USERS; SELECT''')";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -604,7 +605,7 @@ public class PSQLTest {
             + " substring(pg_catalog.quote_ident(nspname) || '.',1,0) ="
             + " substring('',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT table_name AS quote_ident FROM information_schema.tables WHERE"
             + " table_schema = 'public' and STARTS_WITH(LOWER(table_name),"
             + " LOWER('user')) LIMIT 1000";
@@ -612,7 +613,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -641,7 +642,7 @@ public class PSQLTest {
             + " substring(pg_catalog.quote_ident(nspname) || '.',1,4) ="
             + " substring('user',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT table_name AS quote_ident FROM information_schema.tables WHERE"
             + " table_schema = 'public' and STARTS_WITH(LOWER(table_name),"
             + " LOWER('user')) LIMIT 1000";
@@ -649,7 +650,7 @@ public class PSQLTest {
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -663,14 +664,14 @@ public class PSQLTest {
             + " (pg_catalog.quote_ident(relname)='user'         OR '\"' || relname || '\"'='user') "
             + "   AND pg_catalog.pg_table_is_visible(c.oid)\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT column_name AS quote_ident FROM information_schema.columns WHERE"
             + " table_name = 'user' AND STARTS_WITH(LOWER(COLUMN_NAME), LOWER('age')) LIMIT 1000";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -698,14 +699,14 @@ public class PSQLTest {
             + " substring(pg_catalog.quote_ident(nspname) || '.',1,4) ="
             + " substring('user',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT table_name AS quote_ident FROM information_schema.tables WHERE "
             + "table_schema = 'public' AND STARTS_WITH(LOWER(table_name), LOWER('user')) LIMIT 1000";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -733,14 +734,14 @@ public class PSQLTest {
             + " substring(pg_catalog.quote_ident(nspname) || '.',1,4) ="
             + " substring('user',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT table_name AS quote_ident FROM INFORMATION_SCHEMA.TABLES WHERE"
             + " STARTS_WITH(LOWER(table_name), LOWER('user')) LIMIT 1000";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -768,14 +769,14 @@ public class PSQLTest {
             + " substring(pg_catalog.quote_ident(nspname) || '.',1,5) ="
             + " substring('index',1,pg_catalog.length(pg_catalog.quote_ident(nspname))+1)) = 1\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT index_name AS quote_ident FROM INFORMATION_SCHEMA.INDEXES WHERE"
             + " STARTS_WITH(LOWER(index_name), LOWER('index')) LIMIT 1000";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -785,14 +786,14 @@ public class PSQLTest {
         "SELECT pg_catalog.quote_ident(nspname) FROM pg_catalog.pg_namespace  WHERE"
             + " substring(pg_catalog.quote_ident(nspname),1,6)='schema'\n"
             + "LIMIT 1000";
-    String result =
+    String expected =
         "SELECT schema_name AS quote_ident FROM INFORMATION_SCHEMA.SCHEMATA WHERE"
             + " STARTS_WITH(LOWER(schema_name), LOWER('schema')) LIMIT 1000";
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
 
-    Assert.assertEquals(matcherStatement.getSql(), result);
+    assertEquals(expected, matcherStatement.getSql());
   }
 
   @Test
@@ -826,10 +827,10 @@ public class PSQLTest {
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(firstSQL), connectionHandler);
-    Assert.assertEquals(expectedFirstResult, matcherStatement.getSql());
+    assertEquals(expectedFirstResult, matcherStatement.getSql());
 
     matcherStatement = new MatcherStatement(options, parse(secondSQL), connectionHandler);
-    Assert.assertEquals(matcherStatement.getSql(), expectedSecondResult);
+    assertEquals(expectedSecondResult, matcherStatement.getSql());
   }
 
   @Test
@@ -856,6 +857,6 @@ public class PSQLTest {
 
     MatcherStatement matcherStatement =
         new MatcherStatement(options, parse(sql), connectionHandler);
-    Assert.assertEquals(matcherStatement.getSql(), expectedResult);
+    assertEquals(expectedResult, matcherStatement.getSql());
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/StatementParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/StatementParserTest.java
index 6de3e89d..79909468 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/StatementParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/StatementParserTest.java
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter;
 
+import static com.google.cloud.spanner.pgadapter.utils.StatementParser.singleQuoteEscape;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
@@ -21,7 +22,6 @@ import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
-import com.google.cloud.spanner.pgadapter.utils.StatementParser;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -148,7 +148,10 @@ public class StatementParserTest {
   @Test
   public void testEscapes() {
     String sql = "Bobby\\'O\\'Bob'; DROP TABLE USERS; select'";
-    String expectedSql = "Bobby\\'O\\'Bob\\'; DROP TABLE USERS; select\\'";
-    Assert.assertEquals(StatementParser.singleQuoteEscape(sql), expectedSql);
+    String expectedSql = "Bobby\\''O\\''Bob''; DROP TABLE USERS; select''";
+    assertEquals(expectedSql, singleQuoteEscape(sql));
+
+    assertEquals("''test''", singleQuoteEscape("'test'"));
+    assertEquals("''''test''''", singleQuoteEscape("''test''"));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
index 72599d5c..697510be 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter.statements;
 
+import static com.google.cloud.spanner.pgadapter.statements.IntermediateStatement.splitStatements;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -32,6 +33,7 @@ import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.common.collect.ImmutableList;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,6 +57,33 @@ public class IntermediateStatementTest {
 
   @Mock private Connection connection;
 
+  @Test
+  public void testSplitStatements() {
+    assertEquals(ImmutableList.of(""), splitStatements(""));
+    assertEquals(ImmutableList.of("select 1"), splitStatements("select 1"));
+    assertEquals(ImmutableList.of("select 1"), splitStatements("select 1;"));
+    assertEquals(ImmutableList.of("select 1", "select 2"), splitStatements("select 1; select 2;"));
+    assertEquals(
+        ImmutableList.of("select '1;2'", "select 2"), splitStatements("select '1;2'; select 2;"));
+    assertEquals(
+        ImmutableList.of("select \"bobby;drop table\"", "select 2"),
+        splitStatements("select \"bobby;drop table\"; select 2;"));
+    assertEquals(
+        ImmutableList.of("select '1'';\"2'", "select 2"),
+        splitStatements("select '1'';\"2'; select 2;"));
+    assertEquals(
+        ImmutableList.of("select \"a\"\";b\"", "select 2"),
+        splitStatements("select \"a\"\";b\"; select 2;"));
+    assertEquals(
+        ImmutableList.of("select '1'''", "select '2'"),
+        splitStatements("select '1'''; select '2'"));
+    assertEquals(
+        ImmutableList.of("select '1'''", "select '2'''"),
+        splitStatements("select '1'''; select '2'''"));
+    assertEquals(
+        ImmutableList.of("select 1", "select '2'';'"), splitStatements("select 1; select '2'';'"));
+  }
+
   @Test
   public void testUpdateResultCount_ResultSet() {
     when(connectionHandler.getSpannerConnection()).thenReturn(connection);