Repository: GoogleCloudPlatform/pgadapter
Pull request #312 (2022-08-10 15:56:02)

fix: ( and ) are allowed at the end of keywords
Fixes multiple minor issues with the simple parser:
1. ( and ) are allowed at the end of a keyword (e.g. `insert into foo values(1)`
2. There must be a whitespace or a parentheses at the end of a keyword

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#312](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (5513d2a) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/9e4d391b2bf5e8c35cf85d67d9586bdda756f2a7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (9e4d391) will **increase** coverage by `0.04%`.
> The diff coverage is `80.68%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #312      +/-   ##
========================================================
+ Coverage                 83.17%   83.22%   +0.04%     
- Complexity                 1430     1451      +21     
========================================================
  Files                       109      109              
  Lines                      4982     5020      +38     
  Branches                    640      652      +12     
========================================================
+ Hits                       4144     4178      +34     
- Misses                      628      629       +1     
- Partials                    210      213       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `83.22% <80.68%> (+0.04%)` | :arrow_up: |

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/312?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...pter/statements/IntermediatePreparedStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvSW50ZXJtZWRpYXRlUHJlcGFyZWRTdGF0ZW1lbnQuamF2YQ==) | `83.24% <67.85%> (-1.03%)` | :arrow_down: |
| [...loud/spanner/pgadapter/statements/DdlExecutor.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvRGRsRXhlY3V0b3IuamF2YQ==) | `86.57% <75.00%> (ø)` | |
| [...r/pgadapter/statements/SessionStatementParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvU2Vzc2lvblN0YXRlbWVudFBhcnNlci5qYXZh) | `73.78% <87.50%> (ø)` | |
| [...oud/spanner/pgadapter/statements/SimpleParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvU2ltcGxlUGFyc2VyLmphdmE=) | `92.70% <91.66%> (+0.59%)` | :arrow_up: |
| [.../cloud/spanner/pgadapter/utils/MutationWriter.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/312/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL011dGF0aW9uV3JpdGVyLmphdmE=) | `93.05% <0.00%> (+0.26%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?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/statements/DdlExecutor.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/DdlExecutor.java
index 6b32f969..bec201df 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/DdlExecutor.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/DdlExecutor.java
@@ -188,9 +188,9 @@ class DdlExecutor {
    */
   Statement translate(ParsedStatement parsedStatement, Statement statement) {
     SimpleParser parser = new SimpleParser(parsedStatement.getSqlWithoutComments());
-    if (parser.eat("create")) {
+    if (parser.eatKeyword("create")) {
       statement = translateCreate(parser, statement);
-    } else if (parser.eat("drop")) {
+    } else if (parser.eatKeyword("drop")) {
       statement = translateDrop(parser, statement);
     }
 
@@ -198,25 +198,25 @@ class DdlExecutor {
   }
 
   Statement translateCreate(SimpleParser parser, Statement statement) {
-    if (parser.eat("table")) {
+    if (parser.eatKeyword("table")) {
       Statement createTableStatement = translateCreateTable(parser, statement);
       if (createTableStatement == null) {
         return null;
       }
       return maybeRemovePrimaryKeyConstraintName(createTableStatement);
     }
-    boolean unique = parser.eat("unique");
-    if (parser.eat("index")) {
+    boolean unique = parser.eatKeyword("unique");
+    if (parser.eatKeyword("index")) {
       return translateCreateIndex(parser, statement, unique);
     }
     return statement;
   }
 
   Statement translateDrop(SimpleParser parser, Statement statement) {
-    if (parser.eat("table")) {
+    if (parser.eatKeyword("table")) {
       return translateDropTable(parser, statement);
     }
-    if (parser.eat("index")) {
+    if (parser.eatKeyword("index")) {
       return translateDropIndex(parser, statement);
     }
     return statement;
@@ -244,7 +244,7 @@ class DdlExecutor {
       Statement statement,
       String type,
       Function<TableOrIndexName, Boolean> existsFunction) {
-    if (!parser.eat("if", "not", "exists")) {
+    if (!parser.eatKeyword("if", "not", "exists")) {
       return statement;
     }
     return maybeExecuteStatement(
@@ -256,7 +256,7 @@ class DdlExecutor {
       Statement statement,
       String type,
       Function<TableOrIndexName, Boolean> existsFunction) {
-    if (!parser.eat("if", "exists")) {
+    if (!parser.eatKeyword("if", "exists")) {
       return statement;
     }
     return maybeExecuteStatement(parser, statement, "drop", type, existsFunction);
@@ -348,25 +348,25 @@ class DdlExecutor {
     ParsedStatement parsedStatement =
         AbstractStatementParser.getInstance(Dialect.POSTGRESQL).parse(createTableStatement);
     SimpleParser parser = new SimpleParser(parsedStatement.getSqlWithoutComments());
-    parser.eat("create", "table", "if", "not", "exists");
+    parser.eatKeyword("create", "table", "if", "not", "exists");
     TableOrIndexName tableName = parser.readTableOrIndexName();
     if (tableName == null) {
       return createTableStatement;
     }
-    if (!parser.eat("(")) {
+    if (!parser.eatToken("(")) {
       return createTableStatement;
     }
     while (true) {
-      if (parser.eat(")")) {
+      if (parser.eatToken(")")) {
         break;
-      } else if (parser.eat(",")) {
+      } else if (parser.eatToken(",")) {
         continue;
-      } else if (parser.peek("constraint")) {
+      } else if (parser.peekKeyword("constraint")) {
         int positionBeforeConstraintDefinition = parser.getPos();
-        parser.eat("constraint");
+        parser.eatKeyword("constraint");
         String constraintName = unquoteIdentifier(parser.readIdentifierPart());
         int positionAfterConstraintName = parser.getPos();
-        if (parser.eat("primary", "key")) {
+        if (parser.eatKeyword("primary", "key")) {
           if (!constraintName.equalsIgnoreCase("pk_" + unquoteIdentifier(tableName.name))) {
             return createTableStatement;
           }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
index 6d92bb6b..f4ac9ac5 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
@@ -280,10 +280,10 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
   static @Nullable Statement transformInsertToSelectParams(
       Connection connection, String sql, Set<String> parameters) {
     SimpleParser parser = new SimpleParser(sql);
-    if (!parser.eat("insert")) {
+    if (!parser.eatKeyword("insert")) {
       return null;
     }
-    parser.eat("into");
+    parser.eatKeyword("into");
     TableOrIndexName table = parser.readTableOrIndexName();
     if (table == null) {
       return null;
@@ -291,21 +291,31 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
     parser.skipWhitespaces();
 
     List<String> columnsList = null;
-    if (parser.eat("(")) {
-      columnsList = parser.parseExpressionList();
-      if (!parser.eat(")")) {
-        return null;
+    int posBeforeToken = parser.getPos();
+    if (parser.eatToken("(")) {
+      if (parser.peekKeyword("select") || parser.peekToken("(")) {
+        // Revert and assume that the insert uses a select statement.
+        parser.setPos(posBeforeToken);
+      } else {
+        columnsList = parser.parseExpressionList();
+        if (!parser.eatToken(")")) {
+          return null;
+        }
       }
     }
 
     parser.skipWhitespaces();
     int potentialSelectStart = parser.getPos();
-    if (parser.eat("select")) {
-      // This is an `insert into <table> [(...)] select ...` statement. Then we can just use the
-      // select statement as the result.
-      return transformSelectToSelectParams(
-          parser.getSql().substring(potentialSelectStart), parameters);
-    } else if (!parser.eat("values")) {
+    if (!parser.eatKeyword("values")) {
+      while (parser.eatToken("(")) {
+        // ignore
+      }
+      if (parser.eatKeyword("select")) {
+        // This is an `insert into <table> [(...)] select ...` statement. Then we can just use the
+        // select statement as the result.
+        return transformSelectToSelectParams(
+            parser.getSql().substring(potentialSelectStart), parameters);
+      }
       return null;
     }
 
@@ -313,13 +323,16 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
       columnsList = getAllColumns(connection, table);
     }
     List<List<String>> rows = new ArrayList<>();
-    while (parser.eat("(")) {
+    while (parser.eatToken("(")) {
       List<String> row = parser.parseExpressionList();
-      if (row == null || row.isEmpty() || !parser.eat(")") || row.size() != columnsList.size()) {
+      if (row == null
+          || row.isEmpty()
+          || !parser.eatToken(")")
+          || row.size() != columnsList.size()) {
         return null;
       }
       rows.add(row);
-      if (!parser.eat(",")) {
+      if (!parser.eatToken(",")) {
         break;
       }
     }
@@ -377,23 +390,23 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
   @VisibleForTesting
   static Statement transformUpdateToSelectParams(String sql, Set<String> parameters) {
     SimpleParser parser = new SimpleParser(sql);
-    if (!parser.eat("update")) {
+    if (!parser.eatKeyword("update")) {
       return null;
     }
-    parser.eat("only");
+    parser.eatKeyword("only");
     TableOrIndexName table = parser.readTableOrIndexName();
     if (table == null) {
       return null;
     }
-    if (!parser.eat("set")) {
+    if (!parser.eatKeyword("set")) {
       return null;
     }
-    List<String> assignmentsList = parser.parseExpressionListUntil("where");
+    List<String> assignmentsList = parser.parseExpressionListUntilKeyword("where");
     if (assignmentsList == null || assignmentsList.isEmpty()) {
       return null;
     }
     int whereStart = parser.getPos();
-    if (!parser.eat("where")) {
+    if (!parser.eatKeyword("where")) {
       whereStart = -1;
     }
 
@@ -427,17 +440,17 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
   @VisibleForTesting
   static Statement transformDeleteToSelectParams(String sql, Set<String> parameters) {
     SimpleParser parser = new SimpleParser(sql);
-    if (!parser.eat("delete")) {
+    if (!parser.eatKeyword("delete")) {
       return null;
     }
-    parser.eat("from");
+    parser.eatKeyword("from");
     TableOrIndexName table = parser.readTableOrIndexName();
     if (table == null) {
       return null;
     }
     parser.skipWhitespaces();
     int whereStart = parser.getPos();
-    if (!parser.eat("where")) {
+    if (!parser.eatKeyword("where")) {
       // Deletes must have a where clause, otherwise there cannot be any parameters.
       return null;
     }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
index 08aaafe4..4775042b 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
@@ -258,13 +258,13 @@ public class SessionStatementParser {
       return null;
     }
     SimpleParser parser = new SimpleParser(parsedStatement.getSqlWithoutComments());
-    if (parser.eat("set")) {
+    if (parser.eatKeyword("set")) {
       return parseSetStatement(parser);
     }
-    if (parser.eat("reset")) {
+    if (parser.eatKeyword("reset")) {
       return parseResetStatement(parser);
     }
-    if (parser.eat("show")) {
+    if (parser.eatKeyword("show")) {
       return parseShowStatement(parser);
     }
 
@@ -273,11 +273,11 @@ public class SessionStatementParser {
 
   static SetStatement parseSetStatement(SimpleParser parser) {
     SetStatement.Builder builder = new Builder();
-    if (parser.eat("local")) {
+    if (parser.eatKeyword("local")) {
       builder.setLocal();
     } else {
       // Ignore, this is the default.
-      parser.eat("session");
+      parser.eatKeyword("session");
     }
     TableOrIndexName name = parser.readTableOrIndexName();
     if (name == null) {
@@ -287,7 +287,7 @@ public class SessionStatementParser {
     }
     builder.setName(name);
 
-    if (!(parser.eat("to") || parser.eat("="))) {
+    if (!(parser.eatKeyword("to") || parser.eatToken("="))) {
       throw SpannerExceptionFactory.newSpannerException(
           ErrorCode.INVALID_ARGUMENT,
           "Invalid SET statement: " + parser.getSql() + ". Expected TO or =.");
@@ -318,7 +318,7 @@ public class SessionStatementParser {
   }
 
   static ResetStatement parseResetStatement(SimpleParser parser) {
-    if (parser.eat("all")) {
+    if (parser.eatKeyword("all")) {
       String remaining = parser.parseExpression();
       if (!Strings.isNullOrEmpty(remaining)) {
         throw SpannerExceptionFactory.newSpannerException(
@@ -350,7 +350,7 @@ public class SessionStatementParser {
   }
 
   static ShowStatement parseShowStatement(SimpleParser parser) {
-    if (parser.eat("all")) {
+    if (parser.eatKeyword("all")) {
       String remaining = parser.parseExpression();
       if (!Strings.isNullOrEmpty(remaining)) {
         throw SpannerExceptionFactory.newSpannerException(
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 7ce53adf..66217005 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
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter.statements;
 
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -76,21 +77,27 @@ class SimpleParser {
     return pos;
   }
 
+  void setPos(int pos) {
+    this.pos = pos;
+  }
+
   List<String> parseExpressionList() {
-    return parseExpressionListUntil(null);
+    return parseExpressionListUntilKeyword(null);
   }
 
-  List<String> parseExpressionListUntil(@Nullable String delimiter) {
+  List<String> parseExpressionListUntilKeyword(@Nullable String keyword) {
     skipWhitespaces();
     List<String> result = new ArrayList<>();
     int start = pos;
     while (pos < sql.length()) {
-      String expression = parseExpression(delimiter);
+      String expression =
+          parseExpressionUntilKeyword(
+              keyword == null ? ImmutableList.of() : ImmutableList.of(keyword));
       if (expression == null) {
         return null;
       }
       result.add(expression);
-      if (!eat(",")) {
+      if (!eatToken(",")) {
         break;
       }
     }
@@ -101,10 +108,10 @@ class SimpleParser {
   }
 
   String parseExpression() {
-    return parseExpression(null);
+    return parseExpressionUntilKeyword(ImmutableList.of());
   }
 
-  String parseExpression(@Nullable String delimiter) {
+  String parseExpressionUntilKeyword(ImmutableList<String> keywords) {
     skipWhitespaces();
     int start = pos;
     boolean quoted = false;
@@ -129,7 +136,7 @@ class SimpleParser {
         } else if (parens == 0 && sql.charAt(pos) == ',') {
           break;
         }
-        if (delimiter != null && peek(delimiter)) {
+        if (keywords.stream().anyMatch(this::peekKeyword)) {
           break;
         }
       }
@@ -146,10 +153,13 @@ class SimpleParser {
     if (nameOrSchema == null) {
       return null;
     }
-    if (eat(".")) {
-      String name = readIdentifierPart();
-      if (name == null) {
-        name = "";
+    if (peekToken(".")) {
+      String name = "";
+      if (eatDotOperator()) {
+        name = readIdentifierPart();
+        if (name == null) {
+          name = "";
+        }
       }
       return new TableOrIndexName(nameOrSchema, name);
     }
@@ -204,24 +214,57 @@ class SimpleParser {
     return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == '$';
   }
 
-  boolean peek(String keyword) {
-    return internalEat(keyword, false);
+  boolean peekKeyword(String keyword) {
+    return peek(true, true, keyword);
+  }
+
+  boolean peekToken(String token) {
+    return peek(false, false, token);
+  }
+
+  boolean peek(boolean skipWhitespaceBefore, boolean requireWhitespaceAfter, String keyword) {
+    return internalEat(keyword, skipWhitespaceBefore, requireWhitespaceAfter, false);
+  }
+
+  boolean eatKeyword(String... keywords) {
+    return eat(true, true, keywords);
+  }
+
+  boolean eatToken(String token) {
+    return eat(true, false, token);
+  }
+
+  boolean eatDotOperator() {
+    if (eat(false, false, ".")) {
+      if (pos == sql.length() || Character.isWhitespace(sql.charAt(pos))) {
+        return false;
+      }
+      return true;
+    }
+    return false;
   }
 
-  boolean eat(String... keywords) {
+  boolean eat(boolean skipWhitespaceBefore, boolean requireWhitespaceAfter, String... keywords) {
     boolean result = true;
     for (String keyword : keywords) {
-      result &= internalEat(keyword, true);
+      result &= internalEat(keyword, skipWhitespaceBefore, requireWhitespaceAfter, true);
     }
     return result;
   }
 
-  private boolean internalEat(String keyword, boolean updatePos) {
-    skipWhitespaces();
+  private boolean internalEat(
+      String keyword,
+      boolean skipWhitespaceBefore,
+      boolean requireWhitespaceAfter,
+      boolean updatePos) {
+    if (skipWhitespaceBefore) {
+      skipWhitespaces();
+    }
     if (pos + keyword.length() > sql.length()) {
       return false;
     }
-    if (sql.substring(pos, pos + keyword.length()).equalsIgnoreCase(keyword)) {
+    if (sql.substring(pos, pos + keyword.length()).equalsIgnoreCase(keyword)
+        && (!requireWhitespaceAfter || isValidEndOfKeyword(pos + keyword.length()))) {
       if (updatePos) {
         pos = pos + keyword.length();
       }
@@ -230,6 +273,13 @@ class SimpleParser {
     return false;
   }
 
+  private boolean isValidEndOfKeyword(int index) {
+    if (sql.length() == index) {
+      return true;
+    }
+    return !isValidIdentifierChar(sql.charAt(index));
+  }
+
   void skipWhitespaces() {
     while (sql.length() > pos && Character.isWhitespace(sql.charAt(pos))) {
       pos++;


Test patch:
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 40589880..7fb35ee6 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
@@ -161,6 +161,18 @@ public class IntermediateStatementTest {
     assertEquals(
         "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
         transformInsert("insert into foo (col1, col2) values ($1, $2)").getSql());
+    assertEquals(
+        "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
+        transformInsert("insert into foo(col1, col2) values ($1, $2)").getSql());
+    assertEquals(
+        "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
+        transformInsert("insert into foo (col1, col2) values($1, $2)").getSql());
+    assertEquals(
+        "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
+        transformInsert("insert into foo(col1, col2) values($1, $2)").getSql());
+    assertEquals(
+        "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
+        transformInsert("insert foo(col1, col2) values($1, $2)").getSql());
     assertEquals(
         "select $1, $2 from (select col1=$1, col2=$2 from foo) p",
         transformInsert("insert foo (col1, col2) values ($1, $2)").getSql());
@@ -190,6 +202,9 @@ public class IntermediateStatementTest {
                 "insert into foo (col1, col2, col3, col4, col5, col6, col7, col8, col9, col10) "
                     + "values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)")
             .getSql());
+    assertEquals(
+        "select $1, $2 from (select col1=$1, col2=$2 from \"foo\") p",
+        transformInsert("insert\"foo\"(col1, col2)values($1, $2)").getSql());
   }
 
   @Test
@@ -197,6 +212,13 @@ public class IntermediateStatementTest {
     assertEquals(
         "select $1 from (select * from bar where some_col=$1) p",
         transformInsert("insert into foo select * from bar where some_col=$1").getSql());
+    assertEquals(
+        "select $1 from ((select * from bar where some_col=$1)) p",
+        transformInsert("insert into foo (select * from bar where some_col=$1)").getSql());
+    assertEquals(
+        "select $1 from ((select * from(select col1, col2 from bar) where col2=$1)) p",
+        transformInsert("insert into foo (select * from(select col1, col2 from bar) where col2=$1)")
+            .getSql());
     assertEquals(
         "select $1 from (select * from bar where some_col=$1) p",
         transformInsert("insert foo select * from bar where some_col=$1").getSql());
@@ -213,6 +235,7 @@ public class IntermediateStatementTest {
         transformInsert(
                 "insert foo (col1, col2, col3) select * from bar where some_col=$1 limit $2")
             .getSql());
+    assertNull(transformInsert("insert into foo (col1 values ('test')"));
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParserTest.java
index f0fb7aaf..045c11af 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParserTest.java
@@ -43,6 +43,10 @@ public class SessionStatementParserTest {
 
   @Test
   public void testParseShow() {
+    assertEquals(
+        new ShowStatement(new TableOrIndexName("foo", "bar")),
+        SessionStatementParser.parse(PG_PARSER.parse(Statement.of("show foo.bar"))));
+
     assertEquals(
         new ShowStatement(new TableOrIndexName("foo")),
         SessionStatementParser.parse(PG_PARSER.parse(Statement.of("show foo"))));
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 7f7b1e3f..59a97742 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
@@ -30,24 +30,63 @@ import org.junit.runners.JUnit4;
 public class SimpleParserTest {
 
   @Test
-  public void testEat() {
-    assertTrue(new SimpleParser("insert into foo").eat("insert"));
-    assertTrue(new SimpleParser("   insert   into foo").eat("insert"));
-    assertTrue(new SimpleParser("\tinsert   foo").eat("insert"));
+  public void testEatKeyword() {
+    assertTrue(new SimpleParser("insert").eatKeyword("insert"));
+    assertTrue(new SimpleParser("insert into foo").eatKeyword("insert"));
+    assertTrue(new SimpleParser("   insert   into foo").eatKeyword("insert"));
+    assertTrue(new SimpleParser("\tinsert   foo").eatKeyword("insert"));
 
-    assertFalse(new SimpleParser("inset into foo").eat("insert"));
-    assertFalse(new SimpleParser("\"insert\" into foo").eat("insert"));
+    assertFalse(new SimpleParser("inset into foo").eatKeyword("insert"));
+    assertFalse(new SimpleParser("\"insert\" into foo").eatKeyword("insert"));
 
-    assertTrue(new SimpleParser("insert into foo").eat("insert", "into"));
-    assertTrue(new SimpleParser("   insert   into foo").eat("insert", "into"));
+    assertTrue(new SimpleParser("insert into foo").eatKeyword("insert", "into"));
+    assertTrue(new SimpleParser("   insert   into foo").eatKeyword("insert", "into"));
 
-    assertFalse(new SimpleParser("\tinsert   foo").eat("insert", "into"));
-    assertFalse(new SimpleParser("inset into foo").eat("insert", "into"));
-    assertFalse(new SimpleParser("\"insert\" into foo").eat("insert", "into"));
+    assertFalse(new SimpleParser("\tinsert   foo").eatKeyword("insert", "into"));
+    assertFalse(new SimpleParser("inset into foo").eatKeyword("insert", "into"));
+    assertFalse(new SimpleParser("\"insert\" into foo").eatKeyword("insert", "into"));
+
+    assertFalse(new SimpleParser("insertinto foo").eatKeyword("insert", "into"));
+    assertFalse(new SimpleParser("insert intofoo").eatKeyword("insert", "into"));
+    assertFalse(new SimpleParser("\"insert\"into foo").eatKeyword("insert", "into"));
+
+    assertTrue(new SimpleParser("values (1, 2)").eatKeyword("values"));
+    assertTrue(new SimpleParser("values(1, 2)").eatKeyword("values"));
+    assertTrue(new SimpleParser("null)").eatKeyword("null"));
+
+    assertTrue(new SimpleParser("select\"id\"from\"foo\"").eatKeyword("select"));
+    assertTrue(new SimpleParser("select/*comment*/id from foo").eatKeyword("select"));
+    assertFalse(new SimpleParser("select$$foo$$").eatKeyword("select"));
+  }
+
+  @Test
+  public void testEatToken() {
+    assertTrue(new SimpleParser("(foo").eatToken("("));
+    assertTrue(new SimpleParser("(").eatToken("("));
+    assertTrue(new SimpleParser("( ").eatToken("("));
+
+    assertTrue(new SimpleParser("\t(   foo").eatToken("("));
+    assertFalse(new SimpleParser("foo(").eatToken("("));
+    assertFalse(new SimpleParser("").eatToken("("));
+  }
+
+  @Test
+  public void testDotOperator() {
+    assertTrue(new SimpleParser(".foo").eatDotOperator());
+    assertFalse(new SimpleParser(". foo").eatDotOperator());
+    assertFalse(new SimpleParser(" .foo").eatDotOperator());
+    assertFalse(new SimpleParser(".").eatDotOperator());
+    assertFalse(new SimpleParser(". ").eatDotOperator());
+
+    assertTrue(new SimpleParser("\t(   foo").eatToken("("));
+    assertFalse(new SimpleParser("foo(").eatToken("("));
+    assertFalse(new SimpleParser("").eatToken("("));
   }
 
   @Test
   public void testReadTableOrIndexNamePart() {
+    assertEquals("\"foo\"", new SimpleParser("\"foo\"(id)").readIdentifierPart());
+
     assertEquals("foo", new SimpleParser("foo bar").readIdentifierPart());
     assertEquals("foo", new SimpleParser("foo").readIdentifierPart());
     assertEquals("\"foo\"", new SimpleParser("\"foo\" bar").readIdentifierPart());
@@ -79,6 +118,15 @@ public class SimpleParserTest {
 
   @Test
   public void testReadTableOrIndexName() {
+    assertEquals(new TableOrIndexName("foo"), new SimpleParser("foo .bar").readTableOrIndexName());
+    // The following is an invalid name, but the simple parser should just accept this and let the
+    // backend return an error.
+    assertEquals(
+        new TableOrIndexName("foo", ""), new SimpleParser("foo. bar").readTableOrIndexName());
+    assertNull(new SimpleParser(".bar").readTableOrIndexName());
+    assertEquals(
+        new TableOrIndexName("foo", ""), new SimpleParser("foo.\"bar").readTableOrIndexName());
+
     assertEquals(new TableOrIndexName("foo"), new SimpleParser("foo bar").readTableOrIndexName());
     assertEquals(new TableOrIndexName("foo"), new SimpleParser("foo").readTableOrIndexName());
     assertEquals(