Repository: GoogleCloudPlatform/pgadapter
Pull request #262 (2022-07-05 18:35:07)

test: add integration tests using real PostgreSQL
Adds a test environment that can use a real PostgreSQL database and psql with integration tests.

This also fixes a couple of minor issues:
1. Copying boolean values using text could result in the wrong data being written in Cloud Spanner, as PGAdapter tried to parse a textual boolean value using the Java definition instead of the PostgreSQL definition. The value `t` means `true` in PostgreSQL, but is evaluated as `false` in Java.
2. Numeric values were formatted as having scale 65,535.


Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index d7c293df..345fafe8 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -35,6 +35,23 @@ jobs:
     if: needs.check-env.outputs.has-key == 'true'
     timeout-minutes: 60
     runs-on: ubuntu-latest
+    # Add a PostgreSQL Docker container
+    services:
+      # Container label
+      postgres:
+        image: postgres
+        env:
+          POSTGRES_PASSWORD: postgres
+        # Set health checks to wait until postgres has started
+        ports:
+          - 5432:5432
+        volumes:
+          - /pg:/var/run/postgresql
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
     steps:
       - id: set-manual-endpoint
         if: "${{ inputs.endpoint }}"
@@ -65,6 +82,13 @@ jobs:
         with:
           python-version: '3.9'
       - run: pip install -r ./src/test/python/requirements.txt
+      - name: Setup psql
+        run: |
+          sudo apt-get update
+          sudo apt-get install --yes --no-install-recommends postgresql-client
+      - name: Create PostgreSQL test database
+        run: |
+          psql -h /pg -U postgres -c "CREATE DATABASE pgadapter"
       - name: Run unit tests
         run: mvn test -B -Ptest-all
       - name: Setup GCloud
@@ -75,6 +99,11 @@ jobs:
           export_default_credentials: true
       - name: Run integration tests
         run: mvn verify -B -Dclirr.skip=true -DskipITs=false -DPG_ADAPTER_HOST="https://$GOOGLE_CLOUD_ENDPOINT" -DPG_ADAPTER_INSTANCE="$GOOGLE_CLOUD_INSTANCE" -DPG_ADAPTER_DATABASE="$GOOGLE_CLOUD_DATABASE"
+        env:
+          POSTGRES_HOST: /pg
+          POSTGRES_PORT: 5432
+          POSTGRES_USER: postgres
+          POSTGRES_DATABASE: pgadapter
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
index 44f7199d..c3744003 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter.parsers;
 
+import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Statement;
 import com.google.common.collect.ImmutableSet;
@@ -25,7 +26,8 @@ import org.postgresql.util.ByteConverter;
  * Parse specified data to boolean. For most cases it is simply translating from chars 't'/'f' to
  * bit, or simply returning the bit representation.
  */
-class BooleanParser extends Parser<Boolean> {
+@InternalApi
+public class BooleanParser extends Parser<Boolean> {
   private static final String TRUE_VALUE = "t";
   private static final String FALSE_VALUE = "f";
   // See https://www.postgresql.org/docs/current/datatype-boolean.html
@@ -47,13 +49,7 @@ class BooleanParser extends Parser<Boolean> {
       switch (formatCode) {
         case TEXT:
           String stringValue = new String(item, UTF8).toLowerCase(Locale.ENGLISH);
-          if (TRUE_VALUES.contains(stringValue)) {
-            this.item = true;
-          } else if (FALSE_VALUES.contains(stringValue)) {
-            this.item = false;
-          } else {
-            throw new IllegalArgumentException(stringValue + " is not a valid boolean value");
-          }
+          this.item = toBoolean(stringValue);
           break;
         case BINARY:
           this.item = ByteConverter.bool(item, 0);
@@ -64,6 +60,17 @@ class BooleanParser extends Parser<Boolean> {
     }
   }
 
+  /** Converts the string to a boolean value according to the PostgreSQL specs. */
+  public static boolean toBoolean(String value) {
+    if (TRUE_VALUES.contains(value)) {
+      return true;
+    } else if (FALSE_VALUES.contains(value)) {
+      return false;
+    } else {
+      throw new IllegalArgumentException(value + " is not a valid boolean value");
+    }
+  }
+
   @Override
   protected String stringParse() {
     if (this.item == null) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
index dc1579a1..7793d815 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
@@ -29,6 +29,7 @@ import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.UpdateCount;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -537,7 +538,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
           case BOOL:
             builder
                 .set(columnName)
-                .to(recordValue == null ? null : Boolean.parseBoolean(recordValue));
+                .to(recordValue == null ? null : BooleanParser.toBoolean(recordValue));
             break;
           case INT64:
             builder.set(columnName).to(recordValue == null ? null : Long.parseLong(recordValue));
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/PgJdbcCatalog.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/PgJdbcCatalog.java
index 55affa4e..8012b173 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/PgJdbcCatalog.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/PgJdbcCatalog.java
@@ -229,7 +229,7 @@ public class PgJdbcCatalog {
           + "           WHEN DATA_TYPE = 'timestamp with time zone' THEN 1184\n"
           + "           WHEN DATA_TYPE = 'timestamp with time zone[]' THEN 1185\n"
           + "           END AS atttypid,\n"
-          + "       DATA_TYPE AS atttypid, 0 as atttypmod,\n"
+          + "       DATA_TYPE AS atttypid, -1 as atttypmod,\n"
           + "       CASE\n"
           + "           WHEN DATA_TYPE LIKE 'ARRAY' THEN 0\n"
           + "           WHEN DATA_TYPE = 'boolean' THEN NULL\n"
@@ -453,7 +453,7 @@ public class PgJdbcCatalog {
   public static final String PG_JDBC_GET_BEST_ROW_IDENTIFIER_PREFIX =
       "SELECT a.attname, a.atttypid, atttypmod FROM pg_catalog.pg_class ct   JOIN pg_catalog.pg_attribute a ON (ct.oid = a.attrelid)   JOIN pg_catalog.pg_namespace n ON (ct.relnamespace = n.oid)   JOIN (SELECT i.indexrelid, i.indrelid, i.indisprimary,              information_schema._pg_expandarray(i.indkey) AS keys         FROM pg_catalog.pg_index i) i     ON (a.attnum = (i.keys).x AND a.attrelid = i.indrelid) WHERE true";
   public static final String PG_JDBC_GET_BEST_ROW_IDENTIFIER_REPLACEMENT =
-      "select '' as attname, 0 as atttypid, 0 as atttypmod from (select 1) t where false";
+      "select '' as attname, 0 as atttypid, -1 as atttypmod from (select 1) t where false";
 
   private static final String PG_JDBC_PK_QUERY_INNER_PREFIX_42_3 =
       "SELECT NULL AS TABLE_CAT, n.nspname AS TABLE_SCHEM, "
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionResponse.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionResponse.java
index 98055212..3968107c 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionResponse.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionResponse.java
@@ -100,7 +100,8 @@ public class RowDescriptionResponse extends WireOutput {
       this.outputStream.writeInt(oidType);
       this.outputStream.writeShort(getOidTypeSize(oidType));
       // The type modifier. The meaning of the modifier is type-specific.
-      this.outputStream.writeInt(DEFAULT_FLAG);
+      // No modifier is indicated by -1.
+      this.outputStream.writeInt(-1);
       short format =
           this.statement == null
               ? defaultFormat.getCode()


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
new file mode 100644
index 00000000..c7c9251b
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
@@ -0,0 +1,393 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter;
+
+import static com.google.cloud.spanner.pgadapter.PgAdapterTestEnv.DEFAULT_DATA_MODEL;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.spanner.Database;
+import com.google.common.base.Strings;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This integration test assumes that a local PostgreSQL server is available, and uses that server
+ * to compare the behavior of PGAdapter with a real PostgreSQL server. It for example generates
+ * random data that is inserted into the local PostgreSQL database, copies this to Cloud Spanner,
+ * and then compares the data in the two databases afterwards.
+ */
+@Category(IntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITPsqlTest implements IntegrationTest {
+  private static final PgAdapterTestEnv testEnv = new PgAdapterTestEnv();
+  private static Database database;
+
+  private static final String POSTGRES_HOST = System.getenv("POSTGRES_HOST");
+  private static final String POSTGRES_PORT = System.getenv("POSTGRES_PORT");
+  private static final String POSTGRES_USER = System.getenv("POSTGRES_USER");
+  private static final String POSTGRES_DATABASE = System.getenv("POSTGRES_DATABASE");
+  private static final String POSTGRES_PASSWORD = System.getenv("POSTGRES_PASSWORD");
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    assumeTrue("This test requires psql to be installed", isPsqlAvailable());
+    assumeTrue("This test requires bash to be installed", isBashAvailable());
+    assumeFalse(
+        "This test the environment variable POSTGRES_HOST to point to a valid PostgreSQL host",
+        Strings.isNullOrEmpty(POSTGRES_HOST));
+    assumeFalse(
+        "This test the environment variable POSTGRES_PORT to point to a valid PostgreSQL port number",
+        Strings.isNullOrEmpty(POSTGRES_PORT));
+    assumeFalse(
+        "This test the environment variable POSTGRES_USER to point to a valid PostgreSQL user",
+        Strings.isNullOrEmpty(POSTGRES_USER));
+    assumeFalse(
+        "This test the environment variable POSTGRES_DATABASE to point to a valid PostgreSQL database",
+        Strings.isNullOrEmpty(POSTGRES_PORT));
+
+    // Make sure the PG JDBC driver is loaded.
+    Class.forName("org.postgresql.Driver");
+
+    dropDataModel();
+    createDataModel();
+
+    testEnv.setUp();
+    database = testEnv.createDatabase(DEFAULT_DATA_MODEL);
+    testEnv.startPGAdapterServer(Collections.emptyList());
+  }
+
+  @AfterClass
+  public static void teardown() throws Exception {
+    testEnv.stopPGAdapterServer();
+    testEnv.cleanUp();
+    dropDataModel();
+  }
+
+  private static boolean isPsqlAvailable() {
+    ProcessBuilder builder = new ProcessBuilder();
+    String[] psqlCommand = new String[] {"psql", "--version"};
+    builder.command(psqlCommand);
+    try {
+      Process process = builder.start();
+      int res = process.waitFor();
+
+      return res == 0;
+    } catch (Exception ignored) {
+      return false;
+    }
+  }
+
+  private static boolean isBashAvailable() {
+    ProcessBuilder builder = new ProcessBuilder();
+    String[] psqlCommand = new String[] {"bash", "--version"};
+    builder.command(psqlCommand);
+    try {
+      Process process = builder.start();
+      int res = process.waitFor();
+
+      return res == 0;
+    } catch (Exception ignored) {
+      return false;
+    }
+  }
+
+  private static ProcessBuilder setPgPassword(ProcessBuilder builder) {
+    if (POSTGRES_PASSWORD != null) {
+      builder.environment().put("PGPASSWORD", POSTGRES_PASSWORD);
+    }
+    return builder;
+  }
+
+  private static void createDataModel() throws Exception {
+    String[] createTablesCommand =
+        new String[] {
+          "psql",
+          "-h",
+          POSTGRES_HOST,
+          "-p",
+          POSTGRES_PORT,
+          "-U",
+          POSTGRES_USER,
+          "-d",
+          POSTGRES_DATABASE,
+          "-c",
+          String.join(";", DEFAULT_DATA_MODEL) + ";\n"
+        };
+    ProcessBuilder builder = new ProcessBuilder().command(createTablesCommand);
+    setPgPassword(builder);
+    Process process = builder.start();
+
+    int res = process.waitFor();
+    assertEquals(0, res);
+  }
+
+  private static void dropDataModel() throws Exception {
+    String[] dropTablesCommand =
+        new String[] {
+          "psql",
+          "-h",
+          POSTGRES_HOST,
+          "-p",
+          POSTGRES_PORT,
+          "-U",
+          POSTGRES_USER,
+          "-d",
+          POSTGRES_DATABASE,
+          "-c",
+          "drop table if exists all_types; drop table if exists numbers;"
+        };
+    ProcessBuilder builder = new ProcessBuilder().command(dropTablesCommand);
+    setPgPassword(builder);
+    Process process = builder.start();
+
+    int res = process.waitFor();
+    assertEquals(0, res);
+  }
+
+  private String createJdbcUrlForLocalPg() {
+    if (POSTGRES_HOST.startsWith("/")) {
+      return String.format(
+          "jdbc:postgresql://localhost/%s?"
+              + "socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg"
+              + "&socketFactoryArg=%s/.s.PGSQL.%s",
+          POSTGRES_DATABASE, POSTGRES_HOST, POSTGRES_PORT);
+    }
+    return String.format(
+        "jdbc:postgresql://%s:%s/%s", POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DATABASE);
+  }
+
+  private String createJdbcUrlForPGAdapter() {
+    if (POSTGRES_HOST.startsWith("/")) {
+      return String.format(
+          "jdbc:postgresql://localhost/%s?"
+              + "&socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg"
+              + "&socketFactoryArg=/tmp/.s.PGSQL.%d",
+          database.getId().getDatabase(), testEnv.getPGAdapterPort());
+    }
+    return String.format(
+        "jdbc:postgresql://%s/%s",
+        testEnv.getPGAdapterHostAndPort(), database.getId().getDatabase());
+  }
+
+  @Test
+  public void testRealPostgreSQLHelloWorld() throws Exception {
+    ProcessBuilder builder = new ProcessBuilder();
+    String[] psqlCommand =
+        new String[] {
+          "psql",
+          "-h",
+          POSTGRES_HOST,
+          "-p",
+          POSTGRES_PORT,
+          "-U",
+          POSTGRES_USER,
+          "-d",
+          POSTGRES_DATABASE
+        };
+    builder.command(psqlCommand);
+    setPgPassword(builder);
+    Process process = builder.start();
+    String errors;
+    String output;
+
+    try (OutputStreamWriter writer = new OutputStreamWriter(process.getOutputStream());
+        BufferedReader reader =
+            new BufferedReader(new InputStreamReader(process.getInputStream()));
+        BufferedReader errorReader =
+            new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+      writer.write("SELECT 'Hello World!' AS hello;\n");
+      writer.write("\\q\n");
+      writer.flush();
+      errors = errorReader.lines().collect(Collectors.joining("\n"));
+      output = reader.lines().collect(Collectors.joining("\n"));
+    }
+
+    assertEquals("", errors);
+    assertEquals("    hello     \n" + "--------------\n" + " Hello World!\n" + "(1 row)\n", output);
+    int res = process.waitFor();
+    assertEquals(0, res);
+  }
+
+  @Test
+  public void testCopyFromPostgreSQLToCloudSpanner() throws Exception {
+    int numRows = 100;
+
+    // Generate 100 random rows.
+    copyRandomRowsToPostgreSQL(numRows);
+
+    // Verify that we have 100 rows in PostgreSQL.
+    try (Connection connection =
+        DriverManager.getConnection(createJdbcUrlForLocalPg(), POSTGRES_USER, POSTGRES_PASSWORD)) {
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select count(*) from all_types")) {
+        assertTrue(resultSet.next());
+        assertEquals(numRows, resultSet.getLong(1));
+        assertFalse(resultSet.next());
+      }
+    }
+
+    // COPY the rows to Cloud Spanner.
+    ProcessBuilder builder = new ProcessBuilder();
+    builder.command(
+        "bash",
+        "-c",
+        "psql"
+            + " -h "
+            + POSTGRES_HOST
+            + " -p "
+            + POSTGRES_PORT
+            + " -U "
+            + POSTGRES_USER
+            + " -d "
+            + POSTGRES_DATABASE
+            + " -c \"copy all_types to stdout\" "
+            + "  | psql "
+            + " -h "
+            + (POSTGRES_HOST.startsWith("/") ? "/tmp" : testEnv.getPGAdapterHost())
+            + " -p "
+            + testEnv.getPGAdapterPort()
+            + " -d "
+            + database.getId().getDatabase()
+            + " -c \"copy all_types from stdin;\"\n");
+    setPgPassword(builder);
+    Process process = builder.start();
+    int res = process.waitFor();
+    assertEquals(0, res);
+
+    // Verify that we now also have 100 rows in Spanner.
+    try (Connection connection = DriverManager.getConnection(createJdbcUrlForPGAdapter())) {
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select count(*) from all_types")) {
+        assertTrue(resultSet.next());
+        assertEquals(numRows, resultSet.getLong(1));
+        assertFalse(resultSet.next());
+      }
+    }
+
+    // Verify that the rows in both databases are equal.
+    try (Connection pgConnection =
+            DriverManager.getConnection(
+                createJdbcUrlForLocalPg(), POSTGRES_USER, POSTGRES_PASSWORD);
+        Connection spangresConnection = DriverManager.getConnection(createJdbcUrlForPGAdapter())) {
+      try (ResultSet pgResultSet =
+              pgConnection
+                  .createStatement()
+                  .executeQuery("select * from all_types order by col_bigint");
+          ResultSet spangresResultSet =
+              spangresConnection
+                  .createStatement()
+                  .executeQuery("select * from all_types order by col_bigint")) {
+        assertEquals(
+            pgResultSet.getMetaData().getColumnCount(),
+            spangresResultSet.getMetaData().getColumnCount());
+        while (pgResultSet.next() && spangresResultSet.next()) {
+          for (int col = 1; col <= pgResultSet.getMetaData().getColumnCount(); col++) {
+            switch (pgResultSet.getMetaData().getColumnType(col)) {
+              case Types.BINARY:
+                // We can't use equals on byte arrays.
+                assertArrayEquals(
+                    String.format(
+                        "Column %s mismatch:\nPG: %s\nCS: %s",
+                        pgResultSet.getMetaData().getColumnName(col),
+                        Arrays.toString(pgResultSet.getBytes(col)),
+                        Arrays.toString(spangresResultSet.getBytes(col))),
+                    pgResultSet.getBytes(col),
+                    spangresResultSet.getBytes(col));
+                break;
+              case Types.INTEGER:
+                // Spangres accepts `int4` as a data type in DDL statements, but it is converted to
+                // `int8` by the backend. This means that there is a slight difference between these
+                // two in the local database and Cloud Spanner.
+                assertEquals(
+                    String.format(
+                        "Column %s mismatch:\nPG: %s\nCS: %s",
+                        pgResultSet.getMetaData().getColumnName(col),
+                        pgResultSet.getObject(col),
+                        spangresResultSet.getObject(col)),
+                    pgResultSet.getLong(col),
+                    spangresResultSet.getLong(col));
+                assertEquals(pgResultSet.wasNull(), spangresResultSet.wasNull());
+                break;
+              default:
+                assertEquals(
+                    String.format(
+                        "Column %s mismatch:\nPG: %s\nCS: %s",
+                        pgResultSet.getMetaData().getColumnName(col),
+                        pgResultSet.getObject(col),
+                        spangresResultSet.getObject(col)),
+                    pgResultSet.getObject(col),
+                    spangresResultSet.getObject(col));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private void copyRandomRowsToPostgreSQL(int numRows) throws Exception {
+    // Generate random rows and copy these into the all_types table in a local PostgreSQL database.
+    ProcessBuilder builder = new ProcessBuilder();
+    builder.command(
+        "bash",
+        "-c",
+        "psql"
+            + " -h "
+            + POSTGRES_HOST
+            + " -p "
+            + POSTGRES_PORT
+            + " -U "
+            + POSTGRES_USER
+            + " -d "
+            + POSTGRES_DATABASE
+            + " -c \"copy (select (random()*9223372036854775807)::bigint, "
+            + "  random()<0.5, md5(random()::text || clock_timestamp()::text)::bytea, random()*123456789, "
+            + "  (random()*999999)::int, (random()*999999999999)::numeric, now()-random()*interval '250 year', "
+            + "  (current_date-random()*interval '400 year')::date, md5(random()::text || clock_timestamp()::text)::varchar "
+            + String.format("  from generate_series(1, %d) s(i)) to stdout\" ", numRows)
+            + "  | psql "
+            + " -h "
+            + POSTGRES_HOST
+            + " -p "
+            + POSTGRES_PORT
+            + " -U "
+            + POSTGRES_USER
+            + " -d "
+            + POSTGRES_DATABASE
+            + " -c \"copy all_types from stdin;\"\n");
+    setPgPassword(builder);
+    Process process = builder.start();
+    int res = process.waitFor();
+    assertEquals(0, res);
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java
index cdf70613..39036341 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java
@@ -159,10 +159,10 @@ public final class ITQueryTest implements IntegrationTest {
 
     // Check exact message results.
     byte[] rowDescriptionData = {
-      0, 4, 105, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 8, 0, 0, 0, 0, 0, 0, 110, 97, 109, 101,
-      0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 19, -1, -1, 0, 0, 0, 0, 0, 0, 97, 103, 101, 0, 0, 0, 0, 0, 0, 0,
-      0, 0, 0, 20, 0, 8, 0, 0, 0, 0, 0, 0, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, -92, -1,
-      -1, 0, 0, 0, 0, 0, 0
+      0, 4, 105, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 8, -1, -1, -1, -1, 0, 0, 110, 97, 109,
+      101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 19, -1, -1, -1, -1, -1, -1, 0, 0, 97, 103, 101, 0, 0, 0, 0,
+      0, 0, 0, 0, 0, 0, 20, 0, 8, -1, -1, -1, -1, 0, 0, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+      6, -92, -1, -1, -1, -1, -1, -1, 0, 0
     };
     byte[] dataRow0 = {
       0, 4, 0, 0, 0, 1, 49, 0, 0, 0, 1, 49, 0, 0, 0, 1, 49, 0, 0, 0, 11, 49, 50, 51, 52, 53, 46, 54,
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java
new file mode 100644
index 00000000..9574b8b8
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java
@@ -0,0 +1,43 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.parsers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BooleanParserTest {
+
+  @Test
+  public void testToBoolean() {
+    assertTrue(BooleanParser.toBoolean("true"));
+    assertTrue(BooleanParser.toBoolean("tru"));
+    assertTrue(BooleanParser.toBoolean("tr"));
+    assertTrue(BooleanParser.toBoolean("t"));
+
+    assertFalse(BooleanParser.toBoolean("false"));
+    assertFalse(BooleanParser.toBoolean("fals"));
+    assertFalse(BooleanParser.toBoolean("fal"));
+    assertFalse(BooleanParser.toBoolean("fa"));
+    assertFalse(BooleanParser.toBoolean("f"));
+
+    assertThrows(IllegalArgumentException.class, () -> BooleanParser.toBoolean("foo"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionTest.java
index c1b786c3..32a02752 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireoutput/RowDescriptionTest.java
@@ -155,7 +155,7 @@ public final class RowDescriptionTest {
     // type size
     assertEquals(8, outputReader.readShort());
     // type modifier
-    assertEquals(DEFAULT_FLAG, outputReader.readInt());
+    assertEquals(-1, outputReader.readInt());
     // format code
     assertEquals(0, outputReader.readShort());
   }
@@ -208,7 +208,7 @@ public final class RowDescriptionTest {
     // type size
     assertEquals(-1, outputReader.readShort());
     // type modifier
-    assertEquals(DEFAULT_FLAG, outputReader.readInt());
+    assertEquals(-1, outputReader.readInt());
     // format code
     assertEquals(0, outputReader.readShort());
 
@@ -228,7 +228,7 @@ public final class RowDescriptionTest {
     // type size
     assertEquals(-1, outputReader.readShort());
     // type modifier
-    assertEquals(DEFAULT_FLAG, outputReader.readInt());
+    assertEquals(-1, outputReader.readInt());
     // format code
     assertEquals(1, outputReader.readShort());
   }
@@ -279,7 +279,7 @@ public final class RowDescriptionTest {
     // type size
     assertEquals(8, outputReader.readShort());
     // type modifier
-    assertEquals(DEFAULT_FLAG, outputReader.readInt());
+    assertEquals(-1, outputReader.readInt());
     // format code
     assertEquals(1, outputReader.readShort());
   }
@@ -335,7 +335,7 @@ public final class RowDescriptionTest {
       // type size
       assertEquals(8, outputReader.readShort());
       // type modifier
-      assertEquals(DEFAULT_FLAG, outputReader.readInt());
+      assertEquals(-1, outputReader.readInt());
       // format code
       assertEquals(i, outputReader.readShort());
     }