Repository: GoogleCloudPlatform/pgadapter
Pull request #470 (2022-11-08 19:48:47)

feat: use session timezone to format timestamps
Use the session timezone to format timestamps in the given timezone.


Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 5d25b1a1..1bbe6a00 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -75,7 +75,7 @@ jobs:
         uses: actions/setup-java@v3
         with:
           distribution: zulu
-          java-version: 8
+          java-version: 11
       - run: java -version
       - name: Setup Go
         uses: actions/setup-go@v3
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
index 54994532..f824654e 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
@@ -42,6 +42,11 @@ import org.json.simple.JSONObject;
 /** Metadata extractor for CLI. */
 public class OptionsMetadata {
 
+  /** Returns true if the current JVM is Java 8. */
+  public static boolean isJava8() {
+    return System.getProperty("java.version").startsWith("1.8");
+  }
+
   public enum SslMode {
     /** Disables SSL connections. This is the default. */
     Disable {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
index 4b5003ef..50c417de 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
@@ -21,6 +21,7 @@ import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
 import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.LinkedList;
@@ -39,8 +40,10 @@ public class ArrayParser extends Parser<List<?>> {
 
   private final Type arrayElementType;
   private final boolean isStringEquivalent;
+  private final SessionState sessionState;
 
-  public ArrayParser(ResultSet item, int position) {
+  public ArrayParser(ResultSet item, int position, SessionState sessionState) {
+    this.sessionState = sessionState;
     if (item != null) {
       this.arrayElementType = item.getColumnType(position).getArrayElementType();
       if (this.arrayElementType.getCode() == Code.ARRAY) {
@@ -133,7 +136,9 @@ public class ArrayParser extends Parser<List<?>> {
     List<String> results = new LinkedList<>();
     for (Object currentItem : this.item) {
       results.add(
-          stringify(Parser.create(currentItem, this.arrayElementType.getCode()).stringParse()));
+          stringify(
+              Parser.create(currentItem, this.arrayElementType.getCode(), sessionState)
+                  .stringParse()));
     }
     return results.stream()
         .collect(Collectors.joining(ARRAY_DELIMITER, PG_ARRAY_OPEN, PG_ARRAY_CLOSE));
@@ -147,7 +152,9 @@ public class ArrayParser extends Parser<List<?>> {
     List<String> results = new LinkedList<>();
     for (Object currentItem : this.item) {
       results.add(
-          stringify(Parser.create(currentItem, this.arrayElementType.getCode()).spannerParse()));
+          stringify(
+              Parser.create(currentItem, this.arrayElementType.getCode(), sessionState)
+                  .spannerParse()));
     }
     return results.stream()
         .collect(Collectors.joining(ARRAY_DELIMITER, SPANNER_ARRAY_OPEN, SPANNER_ARRAY_CLOSE));
@@ -169,7 +176,9 @@ public class ArrayParser extends Parser<List<?>> {
         if (currentItem == null) {
           arrayStream.write(IntegerParser.binaryParse(-1));
         } else {
-          byte[] data = Parser.create(currentItem, this.arrayElementType.getCode()).binaryParse();
+          byte[] data =
+              Parser.create(currentItem, this.arrayElementType.getCode(), sessionState)
+                  .binaryParse();
           arrayStream.write(IntegerParser.binaryParse(data.length));
           arrayStream.write(data);
         }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
index 0b7964f1..bdc3e4d6 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
@@ -22,6 +22,7 @@ import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
 import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Set;
@@ -79,11 +80,11 @@ public abstract class Parser<T> {
    * @param item The data to be parsed
    * @param oidType The type of the designated data
    * @param formatCode The format of the data to be parsed
-   * @param guessTypes The OIDs of the types that may be 'guessed' based on the input value
+   * @param sessionState The session state to use when parsing and converting
    * @return The parser object for the designated data type.
    */
   public static Parser<?> create(
-      Set<Integer> guessTypes, byte[] item, int oidType, FormatCode formatCode) {
+      SessionState sessionState, byte[] item, int oidType, FormatCode formatCode) {
     switch (oidType) {
       case Oid.BOOL:
       case Oid.BIT:
@@ -110,17 +111,17 @@ public abstract class Parser<T> {
         return new StringParser(item, formatCode);
       case Oid.TIMESTAMP:
       case Oid.TIMESTAMPTZ:
-        return new TimestampParser(item, formatCode);
+        return new TimestampParser(item, formatCode, sessionState);
       case Oid.JSONB:
         return new JsonbParser(item, formatCode);
       case Oid.UNSPECIFIED:
         // Try to guess the type based on the value. Use an unspecified parser if no type could be
         // determined.
-        int type = guessType(guessTypes, item, formatCode);
+        int type = guessType(sessionState.getGuessTypes(), item, formatCode);
         if (type == Oid.UNSPECIFIED) {
           return new UnspecifiedParser(item, formatCode);
         }
-        return create(guessTypes, item, type, formatCode);
+        return create(sessionState, item, type, formatCode);
       default:
         throw new IllegalArgumentException("Unsupported parameter type: " + oidType);
     }
@@ -134,7 +135,8 @@ public abstract class Parser<T> {
    * @param columnarPosition Column from the result to be parsed.
    * @return The parser object for the designated data type.
    */
-  public static Parser<?> create(ResultSet result, Type type, int columnarPosition) {
+  public static Parser<?> create(
+      ResultSet result, Type type, int columnarPosition, SessionState sessionState) {
     switch (type.getCode()) {
       case BOOL:
         return new BooleanParser(result, columnarPosition);
@@ -151,11 +153,11 @@ public abstract class Parser<T> {
       case STRING:
         return new StringParser(result, columnarPosition);
       case TIMESTAMP:
-        return new TimestampParser(result, columnarPosition);
+        return new TimestampParser(result, columnarPosition, sessionState);
       case PG_JSONB:
         return new JsonbParser(result, columnarPosition);
       case ARRAY:
-        return new ArrayParser(result, columnarPosition);
+        return new ArrayParser(result, columnarPosition, sessionState);
       case NUMERIC:
       case JSON:
       case STRUCT:
@@ -171,7 +173,7 @@ public abstract class Parser<T> {
    * @param typeCode The type of the object to be parsed.
    * @return The parser object for the designated data type.
    */
-  protected static Parser<?> create(Object result, Code typeCode) {
+  protected static Parser<?> create(Object result, Code typeCode, SessionState sessionState) {
     switch (typeCode) {
       case BOOL:
         return new BooleanParser(result);
@@ -188,7 +190,7 @@ public abstract class Parser<T> {
       case STRING:
         return new StringParser(result);
       case TIMESTAMP:
-        return new TimestampParser(result);
+        return new TimestampParser(result, sessionState);
       case PG_JSONB:
         return new JsonbParser(result);
       case NUMERIC:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
index 802122fe..7b5961c5 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
@@ -22,8 +22,13 @@ import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.common.base.Preconditions;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
@@ -54,24 +59,40 @@ public class TimestampParser extends Parser<Timestamp> {
 
   private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(TIMESTAMP_REGEX);
 
-  private static final DateTimeFormatter TIMESTAMP_FORMATTER =
+  private static final DateTimeFormatter TIMESTAMP_OUTPUT_FORMATTER =
       new DateTimeFormatterBuilder()
           .parseLenient()
           .parseCaseInsensitive()
           .appendPattern("yyyy-MM-dd HH:mm:ss")
           .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
-          .appendOffset("+HH:mm", "Z")
+          // Java 8 does not support seconds in timezone offset.
+          .appendOffset(OptionsMetadata.isJava8() ? "+HH:mm" : "+HH:mm:ss", "+00")
           .toFormatter();
 
-  TimestampParser(ResultSet item, int position) {
+  private static final DateTimeFormatter TIMESTAMP_INPUT_FORMATTER =
+      new DateTimeFormatterBuilder()
+          .parseLenient()
+          .parseCaseInsensitive()
+          .appendPattern("yyyy-MM-dd HH:mm:ss")
+          .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
+          // Java 8 does not support seconds in timezone offset.
+          .appendOffset(OptionsMetadata.isJava8() ? "+HH:mm" : "+HH:mm:ss", "+00:00:00")
+          .toFormatter();
+
+  private final SessionState sessionState;
+
+  TimestampParser(ResultSet item, int position, SessionState sessionState) {
     this.item = item.getTimestamp(position);
+    this.sessionState = sessionState;
   }
 
-  TimestampParser(Object item) {
+  TimestampParser(Object item, SessionState sessionState) {
     this.item = (Timestamp) item;
+    this.sessionState = sessionState;
   }
 
-  TimestampParser(byte[] item, FormatCode formatCode) {
+  TimestampParser(byte[] item, FormatCode formatCode, SessionState sessionState) {
+    this.sessionState = sessionState;
     if (item != null) {
       switch (formatCode) {
         case TEXT:
@@ -103,7 +124,7 @@ public class TimestampParser extends Parser<Timestamp> {
   public static Timestamp toTimestamp(String value) {
     try {
       String stringValue = toPGString(value);
-      TemporalAccessor temporalAccessor = TIMESTAMP_FORMATTER.parse(stringValue);
+      TemporalAccessor temporalAccessor = TIMESTAMP_INPUT_FORMATTER.parse(stringValue);
       return Timestamp.ofTimeSecondsAndNanos(
           temporalAccessor.getLong(ChronoField.INSTANT_SECONDS),
           temporalAccessor.get(ChronoField.NANO_OF_SECOND));
@@ -125,7 +146,7 @@ public class TimestampParser extends Parser<Timestamp> {
 
   @Override
   public String stringParse() {
-    return this.item == null ? null : toPGString(this.item.toString());
+    return this.item == null ? null : toPGString(this.item, sessionState.getTimezone());
   }
 
   @Override
@@ -150,12 +171,13 @@ public class TimestampParser extends Parser<Timestamp> {
     return result;
   }
 
-  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+  public static byte[] convertToPG(
+      ResultSet resultSet, int position, DataFormat format, ZoneId zoneId) {
     switch (format) {
       case SPANNER:
         return resultSet.getTimestamp(position).toString().getBytes(StandardCharsets.UTF_8);
       case POSTGRESQL_TEXT:
-        return toPGString(resultSet.getTimestamp(position).toString())
+        return toPGString(resultSet.getTimestamp(position), zoneId)
             .getBytes(StandardCharsets.UTF_8);
       case POSTGRESQL_BINARY:
         return convertToPG(resultSet.getTimestamp(position));
@@ -174,6 +196,13 @@ public class TimestampParser extends Parser<Timestamp> {
     return value.replace(TIMESTAMP_SEPARATOR, EMPTY_SPACE).replace(ZERO_TIMEZONE, PG_ZERO_TIMEZONE);
   }
 
+  private static String toPGString(Timestamp value, ZoneId zoneId) {
+    OffsetDateTime offsetDateTime =
+        OffsetDateTime.ofInstant(
+            Instant.ofEpochSecond(value.getSeconds(), value.getNanos()), zoneId);
+    return TIMESTAMP_OUTPUT_FORMATTER.format(offsetDateTime);
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
index 63300cbf..cc39550d 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
@@ -25,10 +25,8 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Scanner;
+import java.time.ZoneId;
+import java.util.*;
 import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 
@@ -380,7 +378,7 @@ public class PGSetting {
     }
     if (this.vartype != null) {
       // Check validity of the value.
-      checkValidValue(value);
+      value = checkValidValue(value);
     }
     this.setting = value;
   }
@@ -420,7 +418,7 @@ public class PGSetting {
     }
   }
 
-  private void checkValidValue(String value) {
+  private String checkValidValue(String value) {
     if ("bool".equals(this.vartype)) {
       // Just verify that it is a valid boolean. This will throw an IllegalArgumentException if
       // setting is not a valid boolean value.
@@ -445,7 +443,32 @@ public class PGSetting {
         && !upperCaseEnumVals.contains(
             MoreObjects.firstNonNull(value, "").toUpperCase(Locale.ENGLISH))) {
       throw invalidEnumError(getCasePreservingKey(), value, enumVals);
+    } else if ("TimeZone".equals(this.name)) {
+      try {
+        value = convertToValidZoneId(value);
+        ZoneId.of(value);
+      } catch (Exception ignore) {
+        throw invalidValueError(this.name, value);
+      }
+    }
+    return value;
+  }
+
+  static String convertToValidZoneId(String value) {
+    if ("utc".equalsIgnoreCase(value)) {
+      return "UTC";
     }
+    for (String zoneId : ZoneId.getAvailableZoneIds()) {
+      if (zoneId.equalsIgnoreCase(value)) {
+        return zoneId;
+      }
+    }
+    for (Map.Entry<String, String> shortId : ZoneId.SHORT_IDS.entrySet()) {
+      if (shortId.getKey().equalsIgnoreCase(value)) {
+        return shortId.getValue();
+      }
+    }
+    return value;
   }
 
   static SpannerException invalidBoolError(String key) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
index 060f564c..2af43c4d 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
@@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
 import com.google.common.collect.Sets;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -166,6 +167,10 @@ public class SessionState {
   }
 
   private static String toKey(String extension, String name) {
+    if (extension == null && "timezone".equalsIgnoreCase(name)) {
+      // TimeZone is the only special setting that uses CamelCase.
+      return "TimeZone";
+    }
     return extension == null
         ? name.toLowerCase(Locale.ROOT)
         : extension.toLowerCase(Locale.ROOT) + "." + name.toLowerCase(Locale.ROOT);
@@ -393,6 +398,30 @@ public class SessionState {
         () -> DdlTransactionMode.valueOf(setting.getBootVal()));
   }
 
+  /** Returns the {@link ZoneId} of the current timezone for this session. */
+  public ZoneId getTimezone() {
+    PGSetting setting = internalGet(toKey(null, "timezone"), false);
+    if (setting == null) {
+      return ZoneId.systemDefault();
+    }
+    String id =
+        tryGetFirstNonNull(
+            ZoneId.systemDefault().getId(),
+            setting::getSetting,
+            setting::getResetVal,
+            setting::getBootVal);
+
+    return zoneIdFromString(id);
+  }
+
+  private ZoneId zoneIdFromString(String value) {
+    try {
+      return ZoneId.of(value);
+    } catch (Throwable ignore) {
+      return ZoneId.systemDefault();
+    }
+  }
+
   /**
    * Returns a set of OIDs that PGAdapter should try to guess if it receives an untyped parameter
    * value. This is needed because some clients (JDBC) deliberately send parameters without a type
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyToStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyToStatement.java
index 06361ece..584bd185 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyToStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyToStatement.java
@@ -28,6 +28,7 @@ import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.DescribePortalMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.parsers.Parser;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement.Format;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement.ParsedCopyStatement;
 import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
@@ -245,11 +246,17 @@ public class CopyToStatement extends IntermediatePortalStatement {
 
   CopyDataResponse createDataResponse(ResultSet resultSet) {
     String[] data = new String[resultSet.getColumnCount()];
+    SessionState sessionState =
+        getConnectionHandler()
+            .getExtendedQueryProtocolHandler()
+            .getBackendConnection()
+            .getSessionState();
     for (int col = 0; col < resultSet.getColumnCount(); col++) {
       if (resultSet.isNull(col)) {
         data[col] = null;
       } else {
-        Parser<?> parser = Parser.create(resultSet, resultSet.getColumnType(col), col);
+        Parser<?> parser =
+            Parser.create(resultSet, resultSet.getColumnType(col), col, sessionState);
         data[col] = parser.stringParse();
       }
     }
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 36fdff29..50c6bcf5 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
@@ -29,6 +29,7 @@ import com.google.cloud.spanner.pgadapter.metadata.DescribeStatementMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.parsers.Parser;
 import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -129,19 +130,16 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
     portal.setParameterFormatCodes(parameterFormatCodes);
     portal.setResultFormatCodes(resultFormatCodes);
     Statement.Builder builder = this.originalStatement.toBuilder();
+    SessionState sessionState =
+        connectionHandler
+            .getExtendedQueryProtocolHandler()
+            .getBackendConnection()
+            .getSessionState();
     for (int index = 0; index < parameters.length; index++) {
       short formatCode = portal.getParameterFormatCode(index);
       int type = this.parseType(parameters, index);
       Parser<?> parser =
-          Parser.create(
-              connectionHandler
-                  .getExtendedQueryProtocolHandler()
-                  .getBackendConnection()
-                  .getSessionState()
-                  .getGuessTypes(),
-              parameters[index],
-              type,
-              FormatCode.of(formatCode));
+          Parser.create(sessionState, parameters[index], type, FormatCode.of(formatCode));
       parser.bind(builder, "p" + (index + 1));
     }
     this.statement = builder.build();
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
index 7e7aa8da..48211567 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
@@ -29,6 +29,7 @@ import com.google.cloud.spanner.pgadapter.parsers.LongParser;
 import com.google.cloud.spanner.pgadapter.parsers.NumericParser;
 import com.google.cloud.spanner.pgadapter.parsers.StringParser;
 import com.google.cloud.spanner.pgadapter.parsers.TimestampParser;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.CopyToStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
 import com.google.common.base.Preconditions;
@@ -44,6 +45,7 @@ public class Converter implements AutoCloseable {
   private final QueryMode mode;
   private final OptionsMetadata options;
   private final ResultSet resultSet;
+  private final SessionState sessionState;
 
   public Converter(
       IntermediateStatement statement,
@@ -54,6 +56,12 @@ public class Converter implements AutoCloseable {
     this.mode = mode;
     this.options = options;
     this.resultSet = resultSet;
+    this.sessionState =
+        statement
+            .getConnectionHandler()
+            .getExtendedQueryProtocolHandler()
+            .getBackendConnection()
+            .getSessionState();
   }
 
   @Override
@@ -86,7 +94,7 @@ public class Converter implements AutoCloseable {
             fixedFormat == null
                 ? DataFormat.getDataFormat(column_index, statement, mode, options)
                 : fixedFormat;
-        byte[] column = Converter.convertToPG(this.resultSet, column_index, format);
+        byte[] column = Converter.convertToPG(this.resultSet, column_index, format, sessionState);
         outputStream.writeInt(column.length);
         outputStream.write(column);
       }
@@ -107,7 +115,8 @@ public class Converter implements AutoCloseable {
    * @param format The {@link DataFormat} format to use to encode the data.
    * @return a byte array containing the data in the specified format.
    */
-  public static byte[] convertToPG(ResultSet result, int position, DataFormat format) {
+  public static byte[] convertToPG(
+      ResultSet result, int position, DataFormat format, SessionState sessionState) {
     Preconditions.checkArgument(!result.isNull(position), "Column may not contain a null value");
     Type type = result.getColumnType(position);
     switch (type.getCode()) {
@@ -126,11 +135,11 @@ public class Converter implements AutoCloseable {
       case STRING:
         return StringParser.convertToPG(result, position);
       case TIMESTAMP:
-        return TimestampParser.convertToPG(result, position, format);
+        return TimestampParser.convertToPG(result, position, format, sessionState.getTimezone());
       case PG_JSONB:
         return JsonbParser.convertToPG(result, position, format);
       case ARRAY:
-        ArrayParser arrayParser = new ArrayParser(result, position);
+        ArrayParser arrayParser = new ArrayParser(result, position, sessionState);
         return arrayParser.parse(format);
       case NUMERIC:
       case JSON:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BootstrapMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BootstrapMessage.java
index 8a5b20b7..8cc11a6c 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BootstrapMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BootstrapMessage.java
@@ -23,11 +23,12 @@ import com.google.cloud.spanner.pgadapter.wireoutput.ParameterStatusResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse.Status;
 import java.io.DataOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TimeZone;
 
 /**
  * This represents all messages which occur before {@link ControlMessage} type messages. Those
@@ -108,30 +109,63 @@ public abstract class BootstrapMessage extends WireMessage {
   public static void sendStartupMessage(
       DataOutputStream output, int connectionId, int secret, SessionState sessionState)
       throws Exception {
-    new AuthenticationOkResponse(output).send();
-    new KeyDataResponse(output, connectionId, secret).send();
+    new AuthenticationOkResponse(output).send(false);
+    new KeyDataResponse(output, connectionId, secret).send(false);
     new ParameterStatusResponse(
             output,
-            "server_version".getBytes(),
-            sessionState.get(null, "server_version").getSetting().getBytes())
-        .send();
-    new ParameterStatusResponse(output, "application_name".getBytes(), "PGAdapter".getBytes())
-        .send();
-    new ParameterStatusResponse(output, "is_superuser".getBytes(), "false".getBytes()).send();
-    new ParameterStatusResponse(output, "session_authorization".getBytes(), "PGAdapter".getBytes())
-        .send();
-    new ParameterStatusResponse(output, "integer_datetimes".getBytes(), "on".getBytes()).send();
-    new ParameterStatusResponse(output, "server_encoding".getBytes(), "UTF8".getBytes()).send();
-    new ParameterStatusResponse(output, "client_encoding".getBytes(), "UTF8".getBytes()).send();
-    new ParameterStatusResponse(output, "DateStyle".getBytes(), "ISO,YMD".getBytes()).send();
-    new ParameterStatusResponse(output, "IntervalStyle".getBytes(), "iso_8601".getBytes()).send();
-    new ParameterStatusResponse(output, "standard_conforming_strings".getBytes(), "on".getBytes())
-        .send();
+            "server_version".getBytes(StandardCharsets.UTF_8),
+            sessionState.get(null, "server_version").getSetting().getBytes(StandardCharsets.UTF_8))
+        .send(false);
     new ParameterStatusResponse(
             output,
-            "TimeZone".getBytes(),
-            TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).getBytes())
-        .send();
+            "application_name".getBytes(StandardCharsets.UTF_8),
+            "PGAdapter".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "is_superuser".getBytes(StandardCharsets.UTF_8),
+            "false".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "session_authorization".getBytes(StandardCharsets.UTF_8),
+            "PGAdapter".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "integer_datetimes".getBytes(StandardCharsets.UTF_8),
+            "on".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "server_encoding".getBytes(StandardCharsets.UTF_8),
+            "UTF8".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "client_encoding".getBytes(StandardCharsets.UTF_8),
+            "UTF8".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "DateStyle".getBytes(StandardCharsets.UTF_8),
+            "ISO,YMD".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "IntervalStyle".getBytes(StandardCharsets.UTF_8),
+            "iso_8601".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "standard_conforming_strings".getBytes(StandardCharsets.UTF_8),
+            "on".getBytes(StandardCharsets.UTF_8))
+        .send(false);
+    new ParameterStatusResponse(
+            output,
+            "TimeZone".getBytes(StandardCharsets.UTF_8),
+            ZoneId.systemDefault().getId().getBytes(StandardCharsets.UTF_8))
+        .send(false);
     new ReadyResponse(output, Status.IDLE).send();
   }
 }


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
index ed9be3d4..3662376e 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
@@ -855,6 +855,7 @@ public class AbortedMockServerTest extends AbstractMockServerTest {
 
       final int fetchSize = 3;
       try (Connection connection = createConnection(binaryTransferEnable)) {
+        connection.createStatement().execute("set time zone utc");
         connection.setAutoCommit(false);
         connection.unwrap(PGConnection.class).setPrepareThreshold(binary ? -1 : 5);
         try (PreparedStatement statement = connection.prepareStatement(SELECT_RANDOM.getSql())) {
@@ -1364,7 +1365,7 @@ public class AbortedMockServerTest extends AbstractMockServerTest {
   public void testSetTimeZone() throws SQLException {
     try (Connection connection = createConnection()) {
       connection.createStatement().execute("set time zone 'IST'");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
     }
   }
 
@@ -1372,7 +1373,7 @@ public class AbortedMockServerTest extends AbstractMockServerTest {
   public void testSetTimeZoneToDefault() throws SQLException {
     try (Connection connection = createConnection("?options=-c%%20timezone=IST")) {
       connection.createStatement().execute("set time zone default");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
     }
   }
 
@@ -1380,7 +1381,7 @@ public class AbortedMockServerTest extends AbstractMockServerTest {
   public void testSetTimeZoneToLocal() throws SQLException {
     try (Connection connection = createConnection("?options=-c%%20timezone=IST")) {
       connection.createStatement().execute("set time zone local");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
     }
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
index b3d1f3df..73954c35 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
@@ -208,6 +208,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
         StatementResult.query(Statement.of("select * from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut("COPY all_types TO STDOUT", writer);
@@ -240,6 +241,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
             Statement.of("select col_bigint, col_varchar from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut("COPY all_types (col_bigint, col_varchar) TO STDOUT", writer);
@@ -271,6 +273,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
         StatementResult.query(Statement.of("select * from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
@@ -288,6 +291,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
         StatementResult.query(Statement.of("select * from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
@@ -307,6 +311,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
             Statement.of("select col_bigint from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
@@ -327,6 +332,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
             Statement.of("select * from all_types order by col_bigint"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
@@ -346,6 +352,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
         StatementResult.query(Statement.of("select * from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
@@ -363,6 +370,7 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
         StatementResult.query(Statement.of("select * from all_types"), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'UTC'");
       CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
       StringWriter writer = new StringWriter();
       copyManager.copyOut(
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
index 36da2afd..56f25f52 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
@@ -340,6 +340,7 @@ public class ITPsqlTest implements IntegrationTest {
     Tuple<String, String> result =
         runUsingPsql(
             ImmutableList.of(
+                "set time zone 'UTC';\n",
                 "prepare insert_row as "
                     + "insert into all_types values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);\n",
                 "prepare find_row as "
@@ -365,7 +366,8 @@ public class ITPsqlTest implements IntegrationTest {
     String output = result.x(), errors = result.y();
     assertEquals("", errors);
     assertEquals(
-        "PREPARE\n"
+        "SET\n"
+            + "PREPARE\n"
             + "PREPARE\n"
             + " col_bigint | col_bool | col_bytea | col_float8 | col_int | col_numeric | col_timestamptz | col_date | col_varchar | col_jsonb \n"
             + "------------+----------+-----------+------------+---------+-------------+-----------------+----------+-------------+-----------\n"
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
index 0cd5c708..4217e8d2 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -69,10 +69,7 @@ import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
-import java.util.Base64;
-import java.util.Calendar;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.junit.BeforeClass;
@@ -2450,7 +2447,17 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
   public void testSetTimeZone() throws SQLException {
     try (Connection connection = DriverManager.getConnection(createUrl())) {
       connection.createStatement().execute("set time zone 'IST'");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
+    }
+  }
+
+  @Test
+  public void testSetTimeZoneToServerDefault() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'atlantic/faeroe'");
+      verifySettingValue(connection, "timezone", "Atlantic/Faeroe");
+      connection.createStatement().execute("set time zone default");
+      verifySettingValue(connection, "timezone", TimeZone.getDefault().getID());
     }
   }
 
@@ -2459,7 +2466,7 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     try (Connection connection =
         DriverManager.getConnection(createUrl() + "?options=-c%20timezone=IST")) {
       connection.createStatement().execute("set time zone default");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
     }
   }
 
@@ -2468,7 +2475,7 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     try (Connection connection =
         DriverManager.getConnection(createUrl() + "?options=-c%20timezone=IST")) {
       connection.createStatement().execute("set time zone local");
-      verifySettingValue(connection, "timezone", "IST");
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
     }
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
index 6e967d5c..ecfe42a4 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
@@ -23,10 +23,14 @@ import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Value;
 import com.google.spanner.v1.CommitRequest;
 import com.google.spanner.v1.ExecuteSqlRequest;
 import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
 import com.google.spanner.v1.RollbackRequest;
+import com.google.spanner.v1.TypeCode;
 import io.grpc.Status;
 import java.math.BigDecimal;
 import java.sql.Connection;
@@ -530,4 +534,96 @@ public class JdbcSimpleModeMockServerTest extends AbstractMockServerTest {
       }
     }
   }
+
+  @Test
+  public void testGetTimezoneStringUtc() throws SQLException {
+    String sql = "select '2022-01-01 10:00:00+01'::timestamptz";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            com.google.cloud.spanner.Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(createMetadata(ImmutableList.of(TypeCode.TIMESTAMP)))
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(
+                            Value.newBuilder().setStringValue("2022-01-01T09:00:00Z").build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone utc");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals("2022-01-01 09:00:00+00", resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testGetTimezoneStringEuropeAmsterdam() throws SQLException {
+    String sql = "select '2022-01-01 10:00:00Z'::timestamptz";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            com.google.cloud.spanner.Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(createMetadata(ImmutableList.of(TypeCode.TIMESTAMP)))
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(
+                            Value.newBuilder().setStringValue("2022-01-01T10:00:00Z").build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'Europe/Amsterdam'");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals("2022-01-01 11:00:00+01", resultSet.getString(1));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testGetTimezoneStringAmericaLosAngeles() throws SQLException {
+    String sql = "select '1883-11-18 00:00:00Z'::timestamptz";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            com.google.cloud.spanner.Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(createMetadata(ImmutableList.of(TypeCode.TIMESTAMP)))
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(
+                            Value.newBuilder().setStringValue("1883-11-18T00:00:00Z").build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'America/Los_Angeles'");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        if (OptionsMetadata.isJava8()) {
+          // Java8 does not support timezone offsets with second precision.
+          assertEquals("1883-11-17 16:07:02-07:52", resultSet.getString(1));
+        } else {
+          assertEquals("1883-11-17 16:07:02-07:52:58", resultSet.getString(1));
+        }
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testSetInvalidTimezone() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      SQLException exception =
+          assertThrows(
+              SQLException.class,
+              () -> connection.createStatement().execute("set time zone 'foo'"));
+      assertEquals(
+          "ERROR: invalid value for parameter \"TimeZone\": \"foo\"", exception.getMessage());
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java
index d8fb1d9e..c2ae5b68 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParserTest.java
@@ -15,6 +15,8 @@
 package com.google.cloud.spanner.pgadapter.parsers;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
@@ -25,7 +27,9 @@ import com.google.cloud.spanner.Struct;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.StructField;
 import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.common.collect.ImmutableList;
+import java.time.ZoneId;
 import java.util.Arrays;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -39,7 +43,9 @@ public class ArrayParserTest {
   public void testInt64StringParse() {
     ArrayParser parser =
         new ArrayParser(
-            createArrayResultSet(Type.int64(), Value.int64Array(Arrays.asList(1L, null, 2L))), 0);
+            createArrayResultSet(Type.int64(), Value.int64Array(Arrays.asList(1L, null, 2L))),
+            0,
+            mock(SessionState.class));
 
     assertEquals("{1,NULL,2}", parser.stringParse());
   }
@@ -49,7 +55,8 @@ public class ArrayParserTest {
     ArrayParser parser =
         new ArrayParser(
             createArrayResultSet(Type.bool(), Value.boolArray(Arrays.asList(true, null, false))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{t,NULL,f}", parser.stringParse());
   }
@@ -62,7 +69,8 @@ public class ArrayParserTest {
                 Type.bytes(),
                 Value.bytesArray(
                     Arrays.asList(ByteArray.copyFrom("test1"), null, ByteArray.copyFrom("test2")))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{\"\\\\x7465737431\",NULL,\"\\\\x7465737432\"}", parser.stringParse());
   }
@@ -73,7 +81,8 @@ public class ArrayParserTest {
         new ArrayParser(
             createArrayResultSet(
                 Type.float64(), Value.float64Array(Arrays.asList(3.14, null, 6.626))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{3.14,NULL,6.626}", parser.stringParse());
   }
@@ -84,7 +93,8 @@ public class ArrayParserTest {
         new ArrayParser(
             createArrayResultSet(
                 Type.pgNumeric(), Value.pgNumericArray(Arrays.asList("3.14", null, "6.626"))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{3.14,NULL,6.626}", parser.stringParse());
   }
@@ -98,13 +108,17 @@ public class ArrayParserTest {
                 Value.dateArray(
                     Arrays.asList(
                         Date.parseDate("2022-07-08"), null, Date.parseDate("2000-01-01")))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{\"2022-07-08\",NULL,\"2000-01-01\"}", parser.stringParse());
   }
 
   @Test
   public void testTimestampStringParse() {
+    SessionState sessionState = mock(SessionState.class);
+    when(sessionState.getTimezone()).thenReturn(ZoneId.of("UTC"));
+
     ArrayParser parser =
         new ArrayParser(
             createArrayResultSet(
@@ -114,7 +128,8 @@ public class ArrayParserTest {
                         Timestamp.parseTimestamp("2022-07-08T07:00:02.123456789Z"),
                         null,
                         Timestamp.parseTimestamp("2000-01-01T00:00:00Z")))),
-            0);
+            0,
+            sessionState);
 
     assertEquals(
         "{\"2022-07-08 07:00:02.123456789+00\",NULL,\"2000-01-01 00:00:00+00\"}",
@@ -127,7 +142,8 @@ public class ArrayParserTest {
         new ArrayParser(
             createArrayResultSet(
                 Type.string(), Value.stringArray(Arrays.asList("test1", null, "test2"))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{\"test1\",NULL,\"test2\"}", parser.stringParse());
   }
@@ -141,7 +157,8 @@ public class ArrayParserTest {
                 Type.json(),
                 Value.jsonArray(
                     Arrays.asList("{\"key\": \"value1\"}}", null, "{\"key\": \"value2\"}"))),
-            0);
+            0,
+            mock(SessionState.class));
 
     assertEquals("{\"test1\",NULL,\"test2\"}", parser.stringParse());
   }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
index 5633d4d7..56b537fa 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.google.cloud.ByteArray;
@@ -32,13 +33,12 @@ import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
-import com.google.common.collect.ImmutableSet;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import java.math.BigDecimal;
 import java.util.Arrays;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
 import org.postgresql.core.Oid;
 import org.postgresql.util.ByteConverter;
 
@@ -61,13 +61,13 @@ public class ParserTest {
   }
 
   private void validateCreateBinary(byte[] item, int oid, Object value) {
-    Parser<?> binary = Parser.create(ImmutableSet.of(), item, oid, FormatCode.BINARY);
+    Parser<?> binary = Parser.create(mock(SessionState.class), item, oid, FormatCode.BINARY);
 
     assertParserValueEqual(binary, value);
   }
 
   private void validateCreateText(byte[] item, int oid, Object value) {
-    Parser<?> text = Parser.create(ImmutableSet.of(), item, oid, FormatCode.TEXT);
+    Parser<?> text = Parser.create(mock(SessionState.class), item, oid, FormatCode.TEXT);
 
     assertParserValueEqual(text, value);
   }
@@ -245,7 +245,7 @@ public class ParserTest {
 
     byte[] byteResult = {-1, -1, -38, 1, -93, -70, 48, 0};
 
-    TimestampParser parsedValue = new TimestampParser(value);
+    TimestampParser parsedValue = new TimestampParser(value, mock(SessionState.class));
 
     assertArrayEquals(byteResult, parsedValue.parse(DataFormat.POSTGRESQL_BINARY));
     validateCreateBinary(byteResult, Oid.TIMESTAMP, value);
@@ -313,11 +313,11 @@ public class ParserTest {
       '[', '"', 'a', 'b', 'c', '"', ',', '"', 'd', 'e', 'f', '"', ',', '"', 'j', 'h', 'i', '"', ']'
     };
 
-    ResultSet resultSet = Mockito.mock(ResultSet.class);
+    ResultSet resultSet = mock(ResultSet.class);
     when(resultSet.getColumnType(0)).thenReturn(Type.array(Type.string()));
     when(resultSet.getValue(0)).thenReturn(Value.stringArray(Arrays.asList(value)));
 
-    ArrayParser parser = new ArrayParser(resultSet, 0);
+    ArrayParser parser = new ArrayParser(resultSet, 0, mock(SessionState.class));
 
     validate(parser, byteResult, stringResult, spannerResult);
   }
@@ -339,21 +339,21 @@ public class ParserTest {
     byte[] stringResult = {'{', '1', ',', '2', ',', '3', '}'};
     byte[] spannerResult = {'[', '1', ',', '2', ',', '3', ']'};
 
-    ResultSet resultSet = Mockito.mock(ResultSet.class);
+    ResultSet resultSet = mock(ResultSet.class);
     when(resultSet.getColumnType(0)).thenReturn(Type.array(Type.int64()));
     when(resultSet.getValue(0)).thenReturn(Value.int64Array(Arrays.asList(value)));
 
-    ArrayParser parser = new ArrayParser(resultSet, 0);
+    ArrayParser parser = new ArrayParser(resultSet, 0, mock(SessionState.class));
 
     validate(parser, byteResult, stringResult, spannerResult);
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void testArrayArrayParsingFails() {
-    ResultSet resultSet = Mockito.mock(ResultSet.class);
+    ResultSet resultSet = mock(ResultSet.class);
     when(resultSet.getColumnType(0)).thenReturn(Type.array(Type.array(Type.int64())));
 
-    new ArrayParser(resultSet, 0);
+    new ArrayParser(resultSet, 0, mock(SessionState.class));
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
index f169aa64..932fb5d1 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
@@ -19,6 +19,8 @@ import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.ErrorCode;
@@ -31,8 +33,10 @@ import com.google.cloud.spanner.Type.StructField;
 import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.common.collect.ImmutableList;
 import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
 import java.util.Random;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -58,8 +62,11 @@ public class TimestampParserTest {
         assertThrows(SpannerException.class, () -> TimestampParser.toTimestamp(new byte[4]));
     assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode());
 
-    assertArrayEquals(data, new TimestampParser(TimestampParser.toTimestamp(data)).binaryParse());
-    assertNull(new TimestampParser(null).binaryParse());
+    assertArrayEquals(
+        data,
+        new TimestampParser(TimestampParser.toTimestamp(data), mock(SessionState.class))
+            .binaryParse());
+    assertNull(new TimestampParser(null, mock(SessionState.class)).binaryParse());
   }
 
   @Test
@@ -68,9 +75,10 @@ public class TimestampParserTest {
         "2022-07-08T07:22:59.123456789Z",
         new TimestampParser(
                 "2022-07-08 07:22:59.123456789+00".getBytes(StandardCharsets.UTF_8),
-                FormatCode.TEXT)
+                FormatCode.TEXT,
+                mock(SessionState.class))
             .spannerParse());
-    assertNull(new TimestampParser(null).spannerParse());
+    assertNull(new TimestampParser(null, mock(SessionState.class)).spannerParse());
 
     ResultSet resultSet =
         ResultSets.forRows(
@@ -83,18 +91,23 @@ public class TimestampParserTest {
     resultSet.next();
     assertArrayEquals(
         "2022-07-08T07:22:59.123456789Z".getBytes(StandardCharsets.UTF_8),
-        TimestampParser.convertToPG(resultSet, 0, DataFormat.SPANNER));
+        TimestampParser.convertToPG(resultSet, 0, DataFormat.SPANNER, ZoneId.of("UTC")));
   }
 
   @Test
   public void testStringParse() {
+    SessionState sessionState = mock(SessionState.class);
+    when(sessionState.getTimezone()).thenReturn(ZoneId.of("+00"));
     assertEquals(
         "2022-07-08 07:22:59.123456789+00",
-        new TimestampParser(Timestamp.parseTimestamp("2022-07-08T07:22:59.123456789Z"))
+        new TimestampParser(
+                Timestamp.parseTimestamp("2022-07-08T07:22:59.123456789Z"), sessionState)
             .stringParse());
-    assertNull(new TimestampParser(null).stringParse());
+    assertNull(new TimestampParser(null, sessionState).stringParse());
     assertThrows(
         PGException.class,
-        () -> new TimestampParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
+        () ->
+            new TimestampParser(
+                "foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT, sessionState));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
index c0cd0ca9..4a48dee6 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
@@ -35,6 +35,8 @@ import com.google.cloud.spanner.pgadapter.statements.PgCatalog;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -848,4 +850,116 @@ public class SessionStateTest {
     state.set("spanner", "guess_types", String.format("%d,%d,foo", Oid.TIMESTAMPTZ, Oid.DATE));
     assertEquals(ImmutableSet.of(Oid.TIMESTAMPTZ, Oid.DATE), state.getGuessTypes());
   }
+
+  @Test
+  public void testGetDefaultTimeZone() {
+    Map<String, PGSetting> originalSettings = ImmutableMap.copyOf(SessionState.SERVER_SETTINGS);
+    SessionState.SERVER_SETTINGS.remove("TimeZone");
+    try {
+      OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+      SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
+      assertEquals(TimeZone.getDefault().toZoneId(), state.getTimezone());
+    } finally {
+      SessionState.SERVER_SETTINGS.putAll(originalSettings);
+    }
+  }
+
+  @Test
+  public void testTimeZoneResetVal() {
+    Map<String, PGSetting> originalSettings = ImmutableMap.copyOf(SessionState.SERVER_SETTINGS);
+    SessionState.SERVER_SETTINGS.put(
+        "TimeZone",
+        new PGSetting(
+            null,
+            "TimeZone",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            "Europe/Oslo",
+            "America/New_York",
+            null,
+            null,
+            false));
+    try {
+      OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+      SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
+      assertEquals("America/New_York", state.getTimezone().getId());
+    } finally {
+      SessionState.SERVER_SETTINGS.putAll(originalSettings);
+    }
+  }
+
+  @Test
+  public void testTimeZoneBootVal() {
+    Map<String, PGSetting> originalSettings = ImmutableMap.copyOf(SessionState.SERVER_SETTINGS);
+    SessionState.SERVER_SETTINGS.put(
+        "TimeZone",
+        new PGSetting(
+            null,
+            "TimeZone",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            "Europe/Oslo",
+            null,
+            null,
+            null,
+            false));
+    try {
+      OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+      SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
+      assertEquals("Europe/Oslo", state.getTimezone().getId());
+    } finally {
+      SessionState.SERVER_SETTINGS.putAll(originalSettings);
+    }
+  }
+
+  @Test
+  public void testGetInvalidTimeZone() {
+    Map<String, PGSetting> originalSettings = ImmutableMap.copyOf(SessionState.SERVER_SETTINGS);
+    SessionState.SERVER_SETTINGS.put(
+        "TimeZone",
+        new PGSetting(
+            null,
+            "TimeZone",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            "foo/bar",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            false));
+    try {
+      OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+      SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
+      assertEquals(TimeZone.getDefault().toZoneId(), state.getTimezone());
+    } finally {
+      SessionState.SERVER_SETTINGS.putAll(originalSettings);
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
index 9140d50c..46257c1e 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
@@ -71,6 +71,7 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -1586,9 +1587,12 @@ public class ProtocolTest {
         readUntil(outputResult, "standard_conforming_strings\0".length()));
     assertEquals("on\0", readUntil(outputResult, "on\0".length()));
     assertEquals('S', outputResult.readByte());
-    assertEquals(17, outputResult.readInt());
-    assertEquals("TimeZone\0", readUntil(outputResult, "TimeZone\0".length()));
+
     // Timezone will vary depending on the default location of the JVM that is running.
+    String timezoneIdentifier = ZoneId.systemDefault().getId();
+    int expectedLength = timezoneIdentifier.getBytes(StandardCharsets.UTF_8).length + 10 + 4;
+    assertEquals(expectedLength, outputResult.readInt());
+    assertEquals("TimeZone\0", readUntil(outputResult, "TimeZone\0".length()));
     readUntilNullTerminator(outputResult);
 
     // ReadyResponse
diff --git a/src/test/nodejs/node-postgres/src/index.ts b/src/test/nodejs/node-postgres/src/index.ts
index cc756310..cc833175 100644
--- a/src/test/nodejs/node-postgres/src/index.ts
+++ b/src/test/nodejs/node-postgres/src/index.ts
@@ -231,6 +231,7 @@ async function testReadOnlyTransactionWithError(client) {
 
 async function testCopyTo(client) {
   try {
+    await client.query("set time zone 'UTC'");
     const copyTo = require('pg-copy-streams').to;
     const stream = client.query(copyTo('COPY AllTypes TO STDOUT'));
     stream.pipe(process.stdout);
diff --git a/src/test/python/psycopg2/Batching.py b/src/test/python/psycopg2/Batching.py
index 634bbe27..b5b0226e 100644
--- a/src/test/python/psycopg2/Batching.py
+++ b/src/test/python/psycopg2/Batching.py
@@ -23,7 +23,7 @@ def create_connection(version, host, port):
     connection = pg.connect(database="my-database",
                             host=host,
                             port=port,
-                            options="-c server_version=" + version)
+                            options="-c timezone=UTC -c server_version=" + version)
     connection.autocommit = True
     return connection
   except Exception as e:
diff --git a/src/test/python/psycopg2/StatementsWithCopy.py b/src/test/python/psycopg2/StatementsWithCopy.py
index 76a2e765..d8693cb2 100644
--- a/src/test/python/psycopg2/StatementsWithCopy.py
+++ b/src/test/python/psycopg2/StatementsWithCopy.py
@@ -22,7 +22,8 @@ def create_connection(port):
     connection = pg.connect(user="postgres",
                             database="postgres",
                             host="localhost",
-                            port=port)
+                            port=port,
+                            options="-c timezone=UTC")
     connection.autocommit = True
     return connection
   except Exception as e:
diff --git a/src/test/python/psycopg2/StatementsWithNamedParameters.py b/src/test/python/psycopg2/StatementsWithNamedParameters.py
index d361c497..b1c5a69b 100644
--- a/src/test/python/psycopg2/StatementsWithNamedParameters.py
+++ b/src/test/python/psycopg2/StatementsWithNamedParameters.py
@@ -24,7 +24,7 @@ def create_connection(version, host, port):
     connection = pg.connect(database="my-database",
                             host=host,
                             port=port,
-                            options="-c server_version=" + version)
+                            options="-c timezone=UTC -c server_version=" + version)
     connection.autocommit = True
     return connection
   except Exception as e:
diff --git a/src/test/python/psycopg2/StatementsWithParameters.py b/src/test/python/psycopg2/StatementsWithParameters.py
index e1f6dc7b..bd77029f 100644
--- a/src/test/python/psycopg2/StatementsWithParameters.py
+++ b/src/test/python/psycopg2/StatementsWithParameters.py
@@ -21,7 +21,7 @@ def create_connection(version, host, port):
     connection = pg.connect(database = "my-database",
                             host = host,
                             port = port,
-                            options="-c server_version=" + version)
+                            options="-c timezone=UTC -c server_version=" + version)
     connection.autocommit = True
     return connection
   except Exception as e:
diff --git a/src/test/python/psycopg2/StatementsWithTransactions.py b/src/test/python/psycopg2/StatementsWithTransactions.py
index 85499a05..1a68a1c4 100644
--- a/src/test/python/psycopg2/StatementsWithTransactions.py
+++ b/src/test/python/psycopg2/StatementsWithTransactions.py
@@ -21,7 +21,7 @@ def create_connection(version, host, port):
     connection = pg.connect(database="my-database",
                             host=host,
                             port=port,
-                            options="-c server_version=" + version)
+                            options="-c timezone=UTC -c server_version=" + version)
     return connection
   except Exception as e:
     print(e)
diff --git a/src/test/python/psycopg2/StatementsWithoutParameters.py b/src/test/python/psycopg2/StatementsWithoutParameters.py
index b63ff163..ce19341b 100644
--- a/src/test/python/psycopg2/StatementsWithoutParameters.py
+++ b/src/test/python/psycopg2/StatementsWithoutParameters.py
@@ -22,7 +22,7 @@ def create_connection(version, host, port):
     connection = pg.connect(database="my-database",
                             host=host,
                             port=port,
-                            options="-c server_version=" + version)
+                            options="-c timezone=UTC -c server_version=" + version)
     connection.autocommit = True
     return connection
   except Exception as e: