From 19b79764294e938ad85d02b7c0662db6ec3afeda Mon Sep 17 00:00:00 2001
From: Aravind Pedapudi <aravindp1510@gmail.com>
Date: Wed, 28 Feb 2024 15:53:14 +0530
Subject: [PATCH] feat: support float32 type (#2894)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: float32 changes with unit and integration tests

* Update formatting and clirr

* Update the hashCode logic to account for NaN equality

* Prevent FLOAT32 integration tests from running on emulator and production

* Fix integration tests for FLOAT32

* Update float32UntypedParameters test to work with PG dialect too

* Split the parameters test in ITQueryTest into supported + currently-unsupported tests.

* Split the Mutation.isNaN method to make it more readable

* test: added some additional tests

* Update to resolve comments on PR#2894.

Major change: Ensures that the new methods in interfaces do not break for older clients.

Minor changes: remove double cast; remove dependency on Truth assertions; remove unnecessary logic in Mutations::isNaN

* Un-ignore the skipped FLOAT32 tests as the backend fixes have been deployed

* Un-ignore the float32 tests in ITQueryTest

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Knut Olav Løite <koloite@gmail.com>
---
 .../clirr-ignored-differences.xml             |  46 +-
 .../cloud/spanner/AbstractResultSet.java      |  76 +++-
 .../cloud/spanner/AbstractStructReader.java   |  51 +++
 .../cloud/spanner/ForwardingStructReader.java |  36 ++
 .../com/google/cloud/spanner/GrpcStruct.java  |  38 +-
 .../com/google/cloud/spanner/Mutation.java    |  18 +-
 .../com/google/cloud/spanner/ResultSets.java  |  30 ++
 .../java/com/google/cloud/spanner/Struct.java |  20 +
 .../google/cloud/spanner/StructReader.java    |  54 +++
 .../java/com/google/cloud/spanner/Type.java   |  17 +-
 .../java/com/google/cloud/spanner/Value.java  | 198 ++++++++-
 .../com/google/cloud/spanner/ValueBinder.java |  25 ++
 .../connection/DirectExecuteResultSet.java    |  36 ++
 .../ReplaceableForwardingResultSet.java       |  36 ++
 .../AbstractStructReaderTypesTest.java        |  36 ++
 .../cloud/spanner/GrpcResultSetTest.java      |  39 ++
 .../cloud/spanner/MockSpannerServiceImpl.java |   4 +
 .../google/cloud/spanner/MutationTest.java    |  56 ++-
 .../spanner/RandomResultSetGenerator.java     |   8 +
 .../google/cloud/spanner/ReadAsyncTest.java   |   5 +-
 .../cloud/spanner/ReadFormatTestRunner.java   |   6 +
 .../google/cloud/spanner/ResultSetsTest.java  |  28 ++
 .../com/google/cloud/spanner/TypeTest.java    |  20 +
 .../google/cloud/spanner/ValueBinderTest.java |  16 +
 .../com/google/cloud/spanner/ValueTest.java   | 151 +++++++
 .../connection/ChecksumResultSetTest.java     |  14 +
 .../cloud/spanner/it/ITAsyncExamplesTest.java |   5 +-
 .../cloud/spanner/it/ITFloat32Test.java       | 415 ++++++++++++++++++
 .../google/cloud/spanner/it/ITQueryTest.java  |  83 ++++
 29 files changed, 1544 insertions(+), 23 deletions(-)
 create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java

diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index fbbc0153f89..eaf7637b0b9 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -506,6 +506,48 @@
     <method>com.google.cloud.spanner.connection.StatementResult execute(com.google.cloud.spanner.Statement, java.util.Set)</method>
   </difference>
 
+  <!-- FLOAT32 -->
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>float getFloat(int)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>float getFloat(java.lang.String)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>float[] getFloatArray(int)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>float[] getFloatArray(java.lang.String)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>java.util.List getFloatList(int)</method>
+  </difference>
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/spanner/StructReader</className>
+    <method>java.util.List getFloatList(java.lang.String)</method>
+  </difference>
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/spanner/Value</className>
+    <method>float getFloat32()</method>
+  </difference>
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/spanner/Value</className>
+    <method>java.util.List getFloat32Array()</method>
+  </difference>
+
   <!-- (Internal change, use stream timeout) -->
   <difference>
     <differenceType>7012</differenceType>
@@ -569,7 +611,7 @@
     <method>void setSpan(io.opencensus.trace.Span)</method>
     <to>void setSpan(com.google.cloud.spanner.ISpan)</to>
   </difference>
-  
+
   <!-- Added DirectedReadOptions -->
   <difference>
     <differenceType>7012</differenceType>
@@ -580,5 +622,5 @@
     <differenceType>7012</differenceType>
     <className>com/google/cloud/spanner/connection/Connection</className>
     <method>void setDirectedRead(com.google.spanner.v1.DirectedReadOptions)</method>
-  </difference>  
+  </difference>
 </differences>
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
index 6cce03e72cb..2cf93fb92ec 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
@@ -173,16 +173,44 @@ static double valueProtoToFloat64(com.google.protobuf.Value proto) {
     return proto.getNumberValue();
   }
 
+  static float valueProtoToFloat32(com.google.protobuf.Value proto) {
+    if (proto.getKindCase() == KindCase.STRING_VALUE) {
+      switch (proto.getStringValue()) {
+        case "-Infinity":
+          return Float.NEGATIVE_INFINITY;
+        case "Infinity":
+          return Float.POSITIVE_INFINITY;
+        case "NaN":
+          return Float.NaN;
+        default:
+          // Fall-through to handling below to produce an error.
+      }
+    }
+    if (proto.getKindCase() != KindCase.NUMBER_VALUE) {
+      throw newSpannerException(
+          ErrorCode.INTERNAL,
+          "Invalid value for column type "
+              + Type.float32()
+              + " expected NUMBER_VALUE or STRING_VALUE with value one of"
+              + " \"Infinity\", \"-Infinity\", or \"NaN\" but was "
+              + proto.getKindCase()
+              + (proto.getKindCase() == KindCase.STRING_VALUE
+                  ? " with value \"" + proto.getStringValue() + "\""
+                  : ""));
+    }
+    return (float) proto.getNumberValue();
+  }
+
   static NullPointerException throwNotNull(int columnIndex) {
     throw new NullPointerException(
         "Cannot call array getter for column " + columnIndex + " with null elements");
   }
 
   /**
-   * Memory-optimized base class for {@code ARRAY<INT64>} and {@code ARRAY<FLOAT64>} types. Both of
-   * these involve conversions from the type yielded by JSON parsing, which are {@code String} and
-   * {@code BigDecimal} respectively. Rather than construct new wrapper objects for each array
-   * element, we use primitive arrays and a {@code BitSet} to track nulls.
+   * Memory-optimized base class for {@code ARRAY<INT64>}, {@code ARRAY<FLOAT32>} and {@code
+   * ARRAY<FLOAT64>} types. All of these involve conversions from the type yielded by JSON parsing,
+   * which are {@code String} and {@code BigDecimal} respectively. Rather than construct new wrapper
+   * objects for each array element, we use primitive arrays and a {@code BitSet} to track nulls.
    */
   abstract static class PrimitiveArray<T, A> extends AbstractList<T> {
     private final A data;
@@ -264,6 +292,31 @@ Long get(long[] array, int i) {
     }
   }
 
+  static class Float32Array extends PrimitiveArray<Float, float[]> {
+    Float32Array(ListValue protoList) {
+      super(protoList);
+    }
+
+    Float32Array(float[] data, BitSet nulls) {
+      super(data, nulls, data.length);
+    }
+
+    @Override
+    float[] newArray(int size) {
+      return new float[size];
+    }
+
+    @Override
+    void setProto(float[] array, int i, com.google.protobuf.Value protoValue) {
+      array[i] = valueProtoToFloat32(protoValue);
+    }
+
+    @Override
+    Float get(float[] array, int i) {
+      return array[i];
+    }
+  }
+
   static class Float64Array extends PrimitiveArray<Double, double[]> {
     Float64Array(ListValue protoList) {
       super(protoList);
@@ -306,6 +359,11 @@ protected long getLongInternal(int columnIndex) {
     return currRow().getLongInternal(columnIndex);
   }
 
+  @Override
+  protected float getFloatInternal(int columnIndex) {
+    return currRow().getFloatInternal(columnIndex);
+  }
+
   @Override
   protected double getDoubleInternal(int columnIndex) {
     return currRow().getDoubleInternal(columnIndex);
@@ -382,6 +440,16 @@ protected List<Long> getLongListInternal(int columnIndex) {
     return currRow().getLongListInternal(columnIndex);
   }
 
+  @Override
+  protected float[] getFloatArrayInternal(int columnIndex) {
+    return currRow().getFloatArrayInternal(columnIndex);
+  }
+
+  @Override
+  protected List<Float> getFloatListInternal(int columnIndex) {
+    return currRow().getFloatListInternal(columnIndex);
+  }
+
   @Override
   protected double[] getDoubleArrayInternal(int columnIndex) {
     return currRow().getDoubleArrayInternal(columnIndex);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java
index ef6f63d52ea..a11c573233b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java
@@ -43,6 +43,10 @@ public abstract class AbstractStructReader implements StructReader {
 
   protected abstract long getLongInternal(int columnIndex);
 
+  protected float getFloatInternal(int columnIndex) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
   protected abstract double getDoubleInternal(int columnIndex);
 
   protected abstract BigDecimal getBigDecimalInternal(int columnIndex);
@@ -94,6 +98,14 @@ protected Value getValueInternal(int columnIndex) {
 
   protected abstract List<Long> getLongListInternal(int columnIndex);
 
+  protected float[] getFloatArrayInternal(int columnIndex) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  protected List<Float> getFloatListInternal(int columnIndex) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
   protected abstract double[] getDoubleArrayInternal(int columnIndex);
 
   protected abstract List<Double> getDoubleListInternal(int columnIndex);
@@ -164,6 +176,19 @@ public long getLong(String columnName) {
     return getLongInternal(columnIndex);
   }
 
+  @Override
+  public float getFloat(int columnIndex) {
+    checkNonNullOfType(columnIndex, Type.float32(), columnIndex);
+    return getFloatInternal(columnIndex);
+  }
+
+  @Override
+  public float getFloat(String columnName) {
+    int columnIndex = getColumnIndex(columnName);
+    checkNonNullOfType(columnIndex, Type.float32(), columnName);
+    return getFloatInternal(columnIndex);
+  }
+
   @Override
   public double getDouble(int columnIndex) {
     checkNonNullOfType(columnIndex, Type.float64(), columnIndex);
@@ -368,6 +393,32 @@ public List<Long> getLongList(String columnName) {
     return getLongListInternal(columnIndex);
   }
 
+  @Override
+  public float[] getFloatArray(int columnIndex) {
+    checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnIndex);
+    return getFloatArrayInternal(columnIndex);
+  }
+
+  @Override
+  public float[] getFloatArray(String columnName) {
+    int columnIndex = getColumnIndex(columnName);
+    checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnName);
+    return getFloatArrayInternal(columnIndex);
+  }
+
+  @Override
+  public List<Float> getFloatList(int columnIndex) {
+    checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnIndex);
+    return getFloatListInternal(columnIndex);
+  }
+
+  @Override
+  public List<Float> getFloatList(String columnName) {
+    int columnIndex = getColumnIndex(columnName);
+    checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnName);
+    return getFloatListInternal(columnIndex);
+  }
+
   @Override
   public double[] getDoubleArray(int columnIndex) {
     checkNonNullOfType(columnIndex, Type.array(Type.float64()), columnIndex);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
index 97c39c00a8d..b3e37ffcddb 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
@@ -125,6 +125,18 @@ public long getLong(String columnName) {
     return delegate.get().getLong(columnName);
   }
 
+  @Override
+  public float getFloat(int columnIndex) {
+    checkValidState();
+    return delegate.get().getFloat(columnIndex);
+  }
+
+  @Override
+  public float getFloat(String columnName) {
+    checkValidState();
+    return delegate.get().getFloat(columnName);
+  }
+
   @Override
   public double getDouble(int columnIndex) {
     checkValidState();
@@ -267,6 +279,30 @@ public List<Long> getLongList(String columnName) {
     return delegate.get().getLongList(columnName);
   }
 
+  @Override
+  public float[] getFloatArray(int columnIndex) {
+    checkValidState();
+    return delegate.get().getFloatArray(columnIndex);
+  }
+
+  @Override
+  public float[] getFloatArray(String columnName) {
+    checkValidState();
+    return delegate.get().getFloatArray(columnName);
+  }
+
+  @Override
+  public List<Float> getFloatList(int columnIndex) {
+    checkValidState();
+    return delegate.get().getFloatList(columnIndex);
+  }
+
+  @Override
+  public List<Float> getFloatList(String columnName) {
+    checkValidState();
+    return delegate.get().getFloatList(columnName);
+  }
+
   @Override
   public double[] getDoubleArray(int columnIndex) {
     checkValidState();
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java
index 6be649ae7c9..a6769acfadf 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java
@@ -17,6 +17,7 @@
 package com.google.cloud.spanner;
 
 import static com.google.cloud.spanner.AbstractResultSet.throwNotNull;
+import static com.google.cloud.spanner.AbstractResultSet.valueProtoToFloat32;
 import static com.google.cloud.spanner.AbstractResultSet.valueProtoToFloat64;
 import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
 import static com.google.common.base.Preconditions.checkArgument;
@@ -24,6 +25,7 @@
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.AbstractResultSet.Float32Array;
 import com.google.cloud.spanner.AbstractResultSet.Float64Array;
 import com.google.cloud.spanner.AbstractResultSet.Int64Array;
 import com.google.cloud.spanner.AbstractResultSet.LazyByteArray;
@@ -83,6 +85,9 @@ private Object writeReplace() {
         case FLOAT64:
           builder.set(fieldName).to((Double) value);
           break;
+        case FLOAT32:
+          builder.set(fieldName).to((Float) value);
+          break;
         case NUMERIC:
           builder.set(fieldName).to((BigDecimal) value);
           break;
@@ -135,6 +140,9 @@ private Object writeReplace() {
             case FLOAT64:
               builder.set(fieldName).toFloat64Array((Iterable<Double>) value);
               break;
+            case FLOAT32:
+              builder.set(fieldName).toFloat32Array((Iterable<Float>) value);
+              break;
             case NUMERIC:
               builder.set(fieldName).toNumericArray((Iterable<BigDecimal>) value);
               break;
@@ -259,6 +267,8 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot
         return Long.parseLong(proto.getStringValue());
       case FLOAT64:
         return valueProtoToFloat64(proto);
+      case FLOAT32:
+        return valueProtoToFloat32(proto);
       case NUMERIC:
         checkType(fieldType, proto, KindCase.STRING_VALUE);
         return new BigDecimal(proto.getStringValue());
@@ -310,11 +320,13 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) {
     switch (elementType.getCode()) {
       case INT64:
       case ENUM:
-        // For int64/float64/enum types, use custom containers.  These avoid wrapper object
-        // creation for non-null arrays.
+        // For int64/float64/float32/enum types, use custom containers.
+        // These avoid wrapper object creation for non-null arrays.
         return new Int64Array(listValue);
       case FLOAT64:
         return new Float64Array(listValue);
+      case FLOAT32:
+        return new Float32Array(listValue);
       case BOOL:
       case NUMERIC:
       case PG_NUMERIC:
@@ -418,6 +430,12 @@ protected double getDoubleInternal(int columnIndex) {
     return (Double) rowData.get(columnIndex);
   }
 
+  @Override
+  protected float getFloatInternal(int columnIndex) {
+    ensureDecoded(columnIndex);
+    return (Float) rowData.get(columnIndex);
+  }
+
   @Override
   protected BigDecimal getBigDecimalInternal(int columnIndex) {
     ensureDecoded(columnIndex);
@@ -537,6 +555,8 @@ protected Value getValueInternal(int columnIndex) {
         return Value.pgNumeric(isNull ? null : getStringInternal(columnIndex));
       case FLOAT64:
         return Value.float64(isNull ? null : getDoubleInternal(columnIndex));
+      case FLOAT32:
+        return Value.float32(isNull ? null : getFloatInternal(columnIndex));
       case STRING:
         return Value.string(isNull ? null : getStringInternal(columnIndex));
       case JSON:
@@ -570,6 +590,8 @@ protected Value getValueInternal(int columnIndex) {
             return Value.pgNumericArray(isNull ? null : getStringListInternal(columnIndex));
           case FLOAT64:
             return Value.float64Array(isNull ? null : getDoubleListInternal(columnIndex));
+          case FLOAT32:
+            return Value.float32Array(isNull ? null : getFloatListInternal(columnIndex));
           case STRING:
             return Value.stringArray(isNull ? null : getStringListInternal(columnIndex));
           case JSON:
@@ -652,6 +674,18 @@ protected Float64Array getDoubleListInternal(int columnIndex) {
     return (Float64Array) rowData.get(columnIndex);
   }
 
+  @Override
+  protected float[] getFloatArrayInternal(int columnIndex) {
+    ensureDecoded(columnIndex);
+    return getFloatListInternal(columnIndex).toPrimitiveArray(columnIndex);
+  }
+
+  @Override
+  protected Float32Array getFloatListInternal(int columnIndex) {
+    ensureDecoded(columnIndex);
+    return (Float32Array) rowData.get(columnIndex);
+  }
+
   @Override
   @SuppressWarnings("unchecked") // We know ARRAY<NUMERIC> produces a List<BigDecimal>.
   protected List<BigDecimal> getBigDecimalListInternal(int columnIndex) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java
index 73995a20df8..6c869c549fc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java
@@ -364,6 +364,8 @@ public int hashCode() {
    * mutation equality to check for modifications before committing. We noticed that when NaNs where
    * used the template would always indicate a modification was present, when it turned out not to
    * be the case. For more information see b/206339664.
+   *
+   * <p>Similar change is being done while calculating `Value.hashCode()`.
    */
   private boolean areValuesEqual(List<Value> values, List<Value> otherValues) {
     if (values == null && otherValues == null) {
@@ -385,9 +387,19 @@ private boolean areValuesEqual(List<Value> values, List<Value> otherValues) {
   }
 
   private boolean isNaN(Value value) {
-    return !value.isNull()
-        && value.getType().equals(Type.float64())
-        && Double.isNaN(value.getFloat64());
+    return !value.isNull() && (isFloat64NaN(value) || isFloat32NaN(value));
+  }
+
+  // Checks if the Float64 value is either a "Double" or a "Float" NaN.
+  // Refer the comment above `areValuesEqual` for more details.
+  private boolean isFloat64NaN(Value value) {
+    return value.getType().equals(Type.float64()) && Double.isNaN(value.getFloat64());
+  }
+
+  // Checks if the Float32 value is either a "Double" or a "Float" NaN.
+  // Refer the comment above `areValuesEqual` for more details.
+  private boolean isFloat32NaN(Value value) {
+    return value.getType().equals(Type.float32()) && Float.isNaN(value.getFloat32());
   }
 
   static void toProto(Iterable<Mutation> mutations, List<com.google.spanner.v1.Mutation> out) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java
index a6cc7c729e5..3d12cf5ad2c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java
@@ -236,6 +236,16 @@ public long getLong(String columnName) {
       return getCurrentRowAsStruct().getLong(columnName);
     }
 
+    @Override
+    public float getFloat(int columnIndex) {
+      return getCurrentRowAsStruct().getFloat(columnIndex);
+    }
+
+    @Override
+    public float getFloat(String columnName) {
+      return getCurrentRowAsStruct().getFloat(columnName);
+    }
+
     @Override
     public double getDouble(int columnIndex) {
       return getCurrentRowAsStruct().getDouble(columnIndex);
@@ -388,6 +398,26 @@ public List<Long> getLongList(String columnName) {
       return getCurrentRowAsStruct().getLongList(columnName);
     }
 
+    @Override
+    public float[] getFloatArray(int columnIndex) {
+      return getCurrentRowAsStruct().getFloatArray(columnIndex);
+    }
+
+    @Override
+    public float[] getFloatArray(String columnName) {
+      return getCurrentRowAsStruct().getFloatArray(columnName);
+    }
+
+    @Override
+    public List<Float> getFloatList(int columnIndex) {
+      return getCurrentRowAsStruct().getFloatList(columnIndex);
+    }
+
+    @Override
+    public List<Float> getFloatList(String columnName) {
+      return getCurrentRowAsStruct().getFloatList(columnName);
+    }
+
     @Override
     public double[] getDoubleArray(int columnIndex) {
       return getCurrentRowAsStruct().getDoubleArray(columnIndex);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java
index 40c30148d0e..0e65fa7f1ba 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Booleans;
 import com.google.common.primitives.Doubles;
+import com.google.common.primitives.Floats;
 import com.google.common.primitives.Longs;
 import com.google.protobuf.AbstractMessage;
 import com.google.protobuf.ProtocolMessageEnum;
@@ -180,6 +181,11 @@ protected long getLongInternal(int columnIndex) {
       return values.get(columnIndex).getInt64();
     }
 
+    @Override
+    protected float getFloatInternal(int columnIndex) {
+      return values.get(columnIndex).getFloat32();
+    }
+
     @Override
     protected double getDoubleInternal(int columnIndex) {
       return values.get(columnIndex).getFloat64();
@@ -261,6 +267,16 @@ protected List<Long> getLongListInternal(int columnIndex) {
       return values.get(columnIndex).getInt64Array();
     }
 
+    @Override
+    protected float[] getFloatArrayInternal(int columnIndex) {
+      return Floats.toArray(getFloatListInternal(columnIndex));
+    }
+
+    @Override
+    protected List<Float> getFloatListInternal(int columnIndex) {
+      return values.get(columnIndex).getFloat32Array();
+    }
+
     @Override
     protected double[] getDoubleArrayInternal(int columnIndex) {
       return Doubles.toArray(getDoubleListInternal(columnIndex));
@@ -382,6 +398,8 @@ private Object getAsObject(int columnIndex) {
       case INT64:
       case ENUM:
         return getLongInternal(columnIndex);
+      case FLOAT32:
+        return getFloatInternal(columnIndex);
       case FLOAT64:
         return getDoubleInternal(columnIndex);
       case NUMERIC:
@@ -410,6 +428,8 @@ private Object getAsObject(int columnIndex) {
           case INT64:
           case ENUM:
             return getLongListInternal(columnIndex);
+          case FLOAT32:
+            return getFloatListInternal(columnIndex);
           case FLOAT64:
             return getDoubleListInternal(columnIndex);
           case NUMERIC:
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java
index fd8cb77f397..f9967db0451 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java
@@ -123,6 +123,22 @@ public interface StructReader {
    */
   long getLong(String columnName);
 
+  /**
+   * @param columnIndex index of the column
+   * @return the value of a non-{@code NULL} column with type {@link Type#float32()}.
+   */
+  default float getFloat(int columnIndex) {
+    throw new UnsupportedOperationException("method should be overwritten");
+  }
+
+  /**
+   * @param columnName name of the column
+   * @return the value of a non-{@code NULL} column with type {@link Type#float32()}.
+   */
+  default float getFloat(String columnName) {
+    throw new UnsupportedOperationException("method should be overwritten");
+  }
+
   /**
    * @param columnIndex index of the column
    * @return the value of a non-{@code NULL} column with type {@link Type#float64()}.
@@ -361,6 +377,44 @@ default Value getValue(String columnName) {
    */
   List<Long> getLongList(String columnName);
 
+  /**
+   * @param columnIndex index of the column
+   * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())}.
+   * @throws NullPointerException if any element of the array value is {@code NULL}. If the array
+   *     may contain {@code NULL} values, use {@link #getFloatList(int)} instead.
+   */
+  default float[] getFloatArray(int columnIndex) {
+    throw new UnsupportedOperationException("method should be overwritten");
+  }
+
+  /**
+   * @param columnName name of the column
+   * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())}.
+   * @throws NullPointerException if any element of the array value is {@code NULL}. If the array
+   *     may contain {@code NULL} values, use {@link #getFloatList(String)} instead.
+   */
+  default float[] getFloatArray(String columnName) {
+    throw new UnsupportedOperationException("method should be overwritten");
+  }
+
+  /**
+   * @param columnIndex index of the column
+   * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())} The
+   *     list returned by this method is lazily constructed. Create a copy of it if you intend to
+   *     access each element in the list multiple times.
+   */
+  default List<Float> getFloatList(int columnIndex) {
+    throw new UnsupportedOperationException("method should be overwritten");
+  }
+
+  /**
+   * @param columnName name of the column
+   * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())} The
+   *     list returned by this method is lazily constructed. Create a copy of it if you intend to
+   *     access each element in the list multiple times.
+   */
+  List<Float> getFloatList(String columnName);
+
   /**
    * @param columnIndex index of the column
    * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float64())}.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java
index 348db5d04ae..5d871227f54 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java
@@ -48,6 +48,7 @@
 public final class Type implements Serializable {
   private static final Type TYPE_BOOL = new Type(Code.BOOL, null, null);
   private static final Type TYPE_INT64 = new Type(Code.INT64, null, null);
+  private static final Type TYPE_FLOAT32 = new Type(Code.FLOAT32, null, null);
   private static final Type TYPE_FLOAT64 = new Type(Code.FLOAT64, null, null);
   private static final Type TYPE_NUMERIC = new Type(Code.NUMERIC, null, null);
   private static final Type TYPE_PG_NUMERIC = new Type(Code.PG_NUMERIC, null, null);
@@ -59,6 +60,7 @@ public final class Type implements Serializable {
   private static final Type TYPE_DATE = new Type(Code.DATE, null, null);
   private static final Type TYPE_ARRAY_BOOL = new Type(Code.ARRAY, TYPE_BOOL, null);
   private static final Type TYPE_ARRAY_INT64 = new Type(Code.ARRAY, TYPE_INT64, null);
+  private static final Type TYPE_ARRAY_FLOAT32 = new Type(Code.ARRAY, TYPE_FLOAT32, null);
   private static final Type TYPE_ARRAY_FLOAT64 = new Type(Code.ARRAY, TYPE_FLOAT64, null);
   private static final Type TYPE_ARRAY_NUMERIC = new Type(Code.ARRAY, TYPE_NUMERIC, null);
   private static final Type TYPE_ARRAY_PG_NUMERIC = new Type(Code.ARRAY, TYPE_PG_NUMERIC, null);
@@ -89,9 +91,17 @@ public static Type int64() {
     return TYPE_INT64;
   }
 
+  /**
+   * Returns the descriptor for the {@code FLOAT32} type: a floating point type with the same value
+   * domain as a Java {@code float}.
+   */
+  public static Type float32() {
+    return TYPE_FLOAT32;
+  }
+
   /**
    * Returns the descriptor for the {@code FLOAT64} type: a floating point type with the same value
-   * domain as a Java {code double}.
+   * domain as a Java {@code double}.
    */
   public static Type float64() {
     return TYPE_FLOAT64;
@@ -174,6 +184,8 @@ public static Type array(Type elementType) {
         return TYPE_ARRAY_BOOL;
       case INT64:
         return TYPE_ARRAY_INT64;
+      case FLOAT32:
+        return TYPE_ARRAY_FLOAT32;
       case FLOAT64:
         return TYPE_ARRAY_FLOAT64;
       case NUMERIC:
@@ -264,6 +276,7 @@ public enum Code {
     NUMERIC(TypeCode.NUMERIC, "unknown"),
     PG_NUMERIC(TypeCode.NUMERIC, "numeric", TypeAnnotationCode.PG_NUMERIC),
     FLOAT64(TypeCode.FLOAT64, "double precision"),
+    FLOAT32(TypeCode.FLOAT32, "real"),
     STRING(TypeCode.STRING, "character varying"),
     JSON(TypeCode.JSON, "unknown"),
     PG_JSONB(TypeCode.JSON, "jsonb", TypeAnnotationCode.PG_JSONB),
@@ -565,6 +578,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) {
         return bool();
       case INT64:
         return int64();
+      case FLOAT32:
+        return float32();
       case FLOAT64:
         return float64();
       case NUMERIC:
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java
index 3f0155e4a5e..e4db5ff1469 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java
@@ -149,6 +149,20 @@ public static Value int64(long v) {
     return new Int64Impl(false, v);
   }
 
+  /**
+   * Returns a {@code FLOAT32} value.
+   *
+   * @param v the value, which may be null
+   */
+  public static Value float32(@Nullable Float v) {
+    return new Float32Impl(v == null, v == null ? 0 : v);
+  }
+
+  /** Returns a {@code FLOAT32} value. */
+  public static Value float32(float v) {
+    return new Float32Impl(false, v);
+  }
+
   /**
    * Returns a {@code FLOAT64} value.
    *
@@ -454,6 +468,40 @@ public static Value int64Array(@Nullable Iterable<Long> v) {
     return int64ArrayFactory.create(v);
   }
 
+  /**
+   * Returns an {@code ARRAY<FLOAT32>} value.
+   *
+   * @param v the source of element values, which may be null to produce a value for which {@code
+   *     isNull()} is {@code true}
+   */
+  public static Value float32Array(@Nullable float[] v) {
+    return float32Array(v, 0, v == null ? 0 : v.length);
+  }
+
+  /**
+   * Returns an {@code ARRAY<FLOAT32>} value that takes its elements from a region of an array.
+   *
+   * @param v the source of element values, which may be null to produce a value for which {@code
+   *     isNull()} is {@code true}
+   * @param pos the start position of {@code v} to copy values from. Ignored if {@code v} is {@code
+   *     null}.
+   * @param length the number of values to copy from {@code v}. Ignored if {@code v} is {@code
+   *     null}.
+   */
+  public static Value float32Array(@Nullable float[] v, int pos, int length) {
+    return float32ArrayFactory.create(v, pos, length);
+  }
+
+  /**
+   * Returns an {@code ARRAY<FLOAT32>} value.
+   *
+   * @param v the source of element values. This may be {@code null} to produce a value for which
+   *     {@code isNull()} is {@code true}. Individual elements may also be {@code null}.
+   */
+  public static Value float32Array(@Nullable Iterable<Float> v) {
+    return float32ArrayFactory.create(v);
+  }
+
   /**
    * Returns an {@code ARRAY<FLOAT64>} value.
    *
@@ -729,6 +777,13 @@ private Value() {}
    */
   public abstract long getInt64();
 
+  /**
+   * Returns the value of a {@code FLOAT32}-typed instance.
+   *
+   * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+   */
+  public abstract float getFloat32();
+
   /**
    * Returns the value of a {@code FLOAT64}-typed instance.
    *
@@ -835,6 +890,14 @@ public <T extends ProtocolMessageEnum> T getProtoEnum(
    */
   public abstract List<Long> getInt64Array();
 
+  /**
+   * Returns the value of an {@code ARRAY<FLOAT32>}-typed instance. While the returned list itself
+   * will never be {@code null}, elements of that list may be null.
+   *
+   * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+   */
+  public abstract List<Float> getFloat32Array();
+
   /**
    * Returns the value of an {@code ARRAY<FLOAT64>}-typed instance. While the returned list itself
    * will never be {@code null}, elements of that list may be null.
@@ -1052,6 +1115,23 @@ Value newValue(boolean isNull, BitSet nulls, long[] values) {
           return new Int64ArrayImpl(isNull, nulls, values);
         }
       };
+  private static final PrimitiveArrayValueFactory<float[], Float> float32ArrayFactory =
+      new PrimitiveArrayValueFactory<float[], Float>() {
+        @Override
+        float[] newArray(int size) {
+          return new float[size];
+        }
+
+        @Override
+        void set(float[] arr, int i, Float value) {
+          arr[i] = value;
+        }
+
+        @Override
+        Value newValue(boolean isNull, BitSet nulls, float[] values) {
+          return new Float32ArrayImpl(isNull, nulls, values);
+        }
+      };
   private static final PrimitiveArrayValueFactory<double[], Double> float64ArrayFactory =
       new PrimitiveArrayValueFactory<double[], Double>() {
         @Override
@@ -1122,6 +1202,11 @@ public long getInt64() {
       throw defaultGetter(Type.int64());
     }
 
+    @Override
+    public float getFloat32() {
+      throw defaultGetter(Type.float32());
+    }
+
     @Override
     public double getFloat64() {
       throw defaultGetter(Type.float64());
@@ -1181,6 +1266,11 @@ public List<Long> getInt64Array() {
       throw defaultGetter(Type.array(Type.int64()));
     }
 
+    @Override
+    public List<Float> getFloat32Array() {
+      throw defaultGetter(Type.array(Type.float32()));
+    }
+
     @Override
     public List<Double> getFloat64Array() {
       throw defaultGetter(Type.array(Type.float64()));
@@ -1285,9 +1375,29 @@ public final boolean equals(Object o) {
 
     @Override
     public final int hashCode() {
-      int result = Objects.hash(getType(), isNull);
+      Type typeToHash = getType();
+      int valueHash = isNull ? 0 : valueHash();
+
+      /**
+       * We are relaxing equality values here, making sure that Double.NaNs and Float.NaNs are equal
+       * to each other. This is because our Cloud Spanner Import / Export template in Apache Beam
+       * uses the mutation equality to check for modifications before committing. We noticed that
+       * when NaNs where used the template would always indicate a modification was present, when it
+       * turned out not to be the case.
+       *
+       * <p>With FLOAT32 being introduced, we want to ensure the backward compatibility of the NaN
+       * equality checks that existed for FLOAT64. We're promoting the type to FLOAT64 while
+       * calculating the type hash when the value is a NaN. We're doing a similar type promotion
+       * while calculating valueHash of Float32 type. Note that this is not applicable for composite
+       * types containing FLOAT32.
+       */
+      if (type.getCode() == Type.Code.FLOAT32 && !isNull && Float.isNaN(getFloat32())) {
+        typeToHash = Type.float64();
+      }
+
+      int result = Objects.hash(typeToHash, isNull);
       if (!isNull) {
-        result = 31 * result + valueHash();
+        result = 31 * result + valueHash;
       }
       return result;
     }
@@ -1492,6 +1602,46 @@ int valueHash() {
     }
   }
 
+  private static class Float32Impl extends AbstractValue {
+    private final float value;
+
+    private Float32Impl(boolean isNull, float value) {
+      super(isNull, Type.float32());
+      this.value = value;
+    }
+
+    @Override
+    public float getFloat32() {
+      checkNotNull();
+      return value;
+    }
+
+    @Override
+    com.google.protobuf.Value valueToProto() {
+      return com.google.protobuf.Value.newBuilder().setNumberValue(value).build();
+    }
+
+    @Override
+    void valueToString(StringBuilder b) {
+      b.append(value);
+    }
+
+    @Override
+    boolean valueEquals(Value v) {
+      return ((Float32Impl) v).value == value;
+    }
+
+    @Override
+    int valueHash() {
+      // For backward compatibility of NaN equality checks with Float64 NaNs.
+      // Refer the comment in `Value.hashCode()` for more details.
+      if (!isNull() && Float.isNaN(value)) {
+        return Double.valueOf(Double.NaN).hashCode();
+      }
+      return Float.valueOf(value).hashCode();
+    }
+  }
+
   private static class Float64Impl extends AbstractValue {
     private final double value;
 
@@ -2106,6 +2256,46 @@ int arrayHash() {
     }
   }
 
+  private static class Float32ArrayImpl extends PrimitiveArrayImpl<Float> {
+    private final float[] values;
+
+    private Float32ArrayImpl(boolean isNull, BitSet nulls, float[] values) {
+      super(isNull, Type.float32(), nulls);
+      this.values = values;
+    }
+
+    @Override
+    public List<Float> getFloat32Array() {
+      return getArray();
+    }
+
+    @Override
+    boolean valueEquals(Value v) {
+      Float32ArrayImpl that = (Float32ArrayImpl) v;
+      return Arrays.equals(values, that.values);
+    }
+
+    @Override
+    int size() {
+      return values.length;
+    }
+
+    @Override
+    Float getValue(int i) {
+      return values[i];
+    }
+
+    @Override
+    com.google.protobuf.Value getValueAsProto(int i) {
+      return com.google.protobuf.Value.newBuilder().setNumberValue(values[i]).build();
+    }
+
+    @Override
+    int arrayHash() {
+      return Arrays.hashCode(values);
+    }
+  }
+
   private static class Float64ArrayImpl extends PrimitiveArrayImpl<Double> {
     private final double[] values;
 
@@ -2588,6 +2778,8 @@ private Value getValue(int fieldIndex) {
           return Value.pgJsonb(value.getPgJsonb(fieldIndex));
         case BYTES:
           return Value.bytes(value.getBytes(fieldIndex));
+        case FLOAT32:
+          return Value.float32(value.getFloat(fieldIndex));
         case FLOAT64:
           return Value.float64(value.getDouble(fieldIndex));
         case NUMERIC:
@@ -2622,6 +2814,8 @@ private Value getValue(int fieldIndex) {
               case BYTES:
               case PROTO:
                 return Value.bytesArray(value.getBytesList(fieldIndex));
+              case FLOAT32:
+                return Value.float32Array(value.getFloatList(fieldIndex));
               case FLOAT64:
                 return Value.float64Array(value.getDoubleList(fieldIndex));
               case NUMERIC:
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java
index 9915e12175a..d675686ffeb 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java
@@ -81,6 +81,16 @@ public R to(@Nullable Long value) {
     return handle(Value.int64(value));
   }
 
+  /** Binds to {@code Value.float32(value)} */
+  public R to(float value) {
+    return handle(Value.float32(value));
+  }
+
+  /** Binds to {@code Value.float32(value)} */
+  public R to(@Nullable Float value) {
+    return handle(Value.float32(value));
+  }
+
   /** Binds to {@code Value.float64(value)} */
   public R to(double value) {
     return handle(Value.float64(value));
@@ -198,6 +208,21 @@ public R toInt64Array(@Nullable Iterable<Long> values) {
     return handle(Value.int64Array(values));
   }
 
+  /** Binds to {@code Value.float32Array(values)} */
+  public R toFloat32Array(@Nullable float[] values) {
+    return handle(Value.float32Array(values));
+  }
+
+  /** Binds to {@code Value.float32Array(values, pos, length)} */
+  public R toFloat32Array(@Nullable float[] values, int pos, int length) {
+    return handle(Value.float32Array(values, pos, length));
+  }
+
+  /** Binds to {@code Value.float32Array(values)} */
+  public R toFloat32Array(@Nullable Iterable<Float> values) {
+    return handle(Value.float32Array(values));
+  }
+
   /** Binds to {@code Value.float64Array(values)} */
   public R toFloat64Array(@Nullable double[] values) {
     return handle(Value.float64Array(values));
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
index 1b15ec50822..b5e4060ddd8 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
@@ -180,6 +180,12 @@ public long getLong(String columnName) {
     return delegate.getLong(columnName);
   }
 
+  @Override
+  public float getFloat(int columnIndex) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloat(columnIndex);
+  }
+
   @Override
   public double getDouble(int columnIndex) {
     Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
@@ -198,6 +204,12 @@ public BigDecimal getBigDecimal(int columnIndex) {
     return delegate.getBigDecimal(columnIndex);
   }
 
+  @Override
+  public float getFloat(String columnName) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloat(columnName);
+  }
+
   @Override
   public double getDouble(String columnName) {
     Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
@@ -336,6 +348,30 @@ public List<Long> getLongList(String columnName) {
     return delegate.getLongList(columnName);
   }
 
+  @Override
+  public float[] getFloatArray(int columnIndex) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloatArray(columnIndex);
+  }
+
+  @Override
+  public float[] getFloatArray(String columnName) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloatArray(columnName);
+  }
+
+  @Override
+  public List<Float> getFloatList(int columnIndex) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloatList(columnIndex);
+  }
+
+  @Override
+  public List<Float> getFloatList(String columnName) {
+    Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+    return delegate.getFloatList(columnName);
+  }
+
   @Override
   public double[] getDoubleArray(int columnIndex) {
     Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
index a8de14e5121..bd7c794a0fa 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
@@ -189,6 +189,18 @@ public long getLong(String columnName) {
     return delegate.getLong(columnName);
   }
 
+  @Override
+  public float getFloat(int columnIndex) {
+    checkClosed();
+    return delegate.getFloat(columnIndex);
+  }
+
+  @Override
+  public float getFloat(String columnName) {
+    checkClosed();
+    return delegate.getFloat(columnName);
+  }
+
   @Override
   public double getDouble(int columnIndex) {
     checkClosed();
@@ -345,6 +357,30 @@ public List<Long> getLongList(String columnName) {
     return delegate.getLongList(columnName);
   }
 
+  @Override
+  public float[] getFloatArray(int columnIndex) {
+    checkClosed();
+    return delegate.getFloatArray(columnIndex);
+  }
+
+  @Override
+  public float[] getFloatArray(String columnName) {
+    checkClosed();
+    return delegate.getFloatArray(columnName);
+  }
+
+  @Override
+  public List<Float> getFloatList(int columnIndex) {
+    checkClosed();
+    return delegate.getFloatList(columnIndex);
+  }
+
+  @Override
+  public List<Float> getFloatList(String columnName) {
+    checkClosed();
+    return delegate.getFloatList(columnName);
+  }
+
   @Override
   public double[] getDoubleArray(int columnIndex) {
     checkClosed();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java
index 4fc3c67ceba..16dd51a36a3 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java
@@ -58,6 +58,11 @@ protected long getLongInternal(int columnIndex) {
       return 0;
     }
 
+    @Override
+    protected float getFloatInternal(int columnIndex) {
+      return 0f;
+    }
+
     @Override
     protected double getDoubleInternal(int columnIndex) {
       return 0;
@@ -134,6 +139,16 @@ protected List<Long> getLongListInternal(int columnIndex) {
       return null;
     }
 
+    @Override
+    protected float[] getFloatArrayInternal(int columnIndex) {
+      return null;
+    }
+
+    @Override
+    protected List<Float> getFloatListInternal(int columnIndex) {
+      return null;
+    }
+
     @Override
     protected double[] getDoubleArrayInternal(int columnIndex) {
       return null;
@@ -222,6 +237,13 @@ public static Collection<Object[]> parameters() {
             Collections.singletonList("getValue")
           },
           {Type.int64(), "getLongInternal", 123L, "getLong", Collections.singletonList("getValue")},
+          {
+            Type.float32(),
+            "getFloatInternal",
+            2.0f,
+            "getFloat",
+            Collections.singletonList("getValue")
+          },
           {
             Type.float64(),
             "getDoubleInternal",
@@ -306,6 +328,20 @@ public static Collection<Object[]> parameters() {
             "getLongList",
             Arrays.asList("getLongArray", "getValue")
           },
+          {
+            Type.array(Type.float32()),
+            "getFloatArrayInternal",
+            new float[] {1.0f, 2.0f},
+            "getFloatArray",
+            Arrays.asList("getFloatList", "getValue")
+          },
+          {
+            Type.array(Type.float32()),
+            "getFloatListInternal",
+            Arrays.asList(2.0f, 4.0f),
+            "getFloatList",
+            Arrays.asList("getFloatArray", "getValue")
+          },
           {
             Type.array(Type.float64()),
             "getDoubleArrayInternal",
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java
index cb73618d998..5e6a4ffc2ca 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java
@@ -537,6 +537,8 @@ public void serialization() {
         Value.int64(null),
         Value.float64(1.0),
         Value.float64(null),
+        Value.float32(1.0f),
+        Value.float32(null),
         Value.bytes(ByteArray.fromBase64("abcd")),
         Value.bytesFromBase64(
             Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8))),
@@ -554,6 +556,8 @@ public void serialization() {
         Value.int64Array((long[]) null),
         Value.float64Array(new double[] {1.1, 2.2, 3.3}),
         Value.float64Array((double[]) null),
+        Value.float32Array(new float[] {1.1f, 2.2f, 3.3f}),
+        Value.float32Array((float[]) null),
         Value.bytesArray(Arrays.asList(ByteArray.fromBase64("abcd"), null)),
         Value.bytesArrayFromBase64(
             Arrays.asList(
@@ -655,6 +659,22 @@ public void getDouble() {
     assertThat(resultSet.getDouble(0)).isWithin(0.0).of(Double.MAX_VALUE);
   }
 
+  @Test
+  public void getFloat() {
+    consumer.onPartialResultSet(
+        PartialResultSet.newBuilder()
+            .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.float32()))))
+            .addValues(Value.float32(Float.MIN_VALUE).toProto())
+            .addValues(Value.float32(Float.MAX_VALUE).toProto())
+            .build());
+    consumer.onCompleted();
+
+    assertThat(resultSet.next()).isTrue();
+    assertThat(resultSet.getFloat(0)).isWithin(0.0f).of(Float.MIN_VALUE);
+    assertThat(resultSet.next()).isTrue();
+    assertThat(resultSet.getFloat(0)).isWithin(0.0f).of(Float.MAX_VALUE);
+  }
+
   @Test
   public void getBigDecimal() {
     consumer.onPartialResultSet(
@@ -877,6 +897,25 @@ public void getDoubleArray() {
         .inOrder();
   }
 
+  @Test
+  public void getFloatArray() {
+    float[] floatArray = {Float.MAX_VALUE, Float.MIN_VALUE, 111, 333, 444, 0, -1, -2234};
+
+    consumer.onPartialResultSet(
+        PartialResultSet.newBuilder()
+            .setMetadata(
+                makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.float32())))))
+            .addValues(Value.float32Array(floatArray).toProto())
+            .build());
+    consumer.onCompleted();
+
+    assertThat(resultSet.next()).isTrue();
+    assertThat(resultSet.getFloatArray(0))
+        .usingTolerance(0.0)
+        .containsExactly(floatArray)
+        .inOrder();
+  }
+
   @Test
   public void getBigDecimalList() {
     List<BigDecimal> bigDecimalsList = new ArrayList<>();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
index f27aa405aaa..3b0b7812d23 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
@@ -1284,6 +1284,8 @@ private Statement buildStatement(
               case DATE:
                 builder.bind(fieldName).toDateArray(null);
                 break;
+              case FLOAT32:
+                builder.bind(fieldName).toFloat32Array((Iterable<Float>) null);
               case FLOAT64:
                 builder.bind(fieldName).toFloat64Array((Iterable<Double>) null);
                 break;
@@ -1327,6 +1329,8 @@ private Statement buildStatement(
           case DATE:
             builder.bind(fieldName).to((Date) null);
             break;
+          case FLOAT32:
+            builder.bind(fieldName).to((Float) null);
           case FLOAT64:
             builder.bind(fieldName).to((Double) null);
             break;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
index f38b5e47b8d..0cfb57d4a22 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
@@ -211,12 +211,37 @@ public void equalsAndHashCode() {
         Mutation.delete("T1", KeySet.singleKey(Key.of("k"))), Mutation.delete("T1", Key.of("k")));
 
     // Test NaNs
+    // Refer the comment in `Value.hashCode()` for more details on NaN equality.
     tester.addEqualityGroup(
         Mutation.newInsertBuilder("T1").set("C").to(Double.NaN).build(),
         Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Double.NaN)).build(),
         Mutation.newInsertBuilder("T1").set("C").to(Float.NaN).build(),
-        Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Float.NaN)).build());
+        Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Float.NaN)).build(),
+        Mutation.newInsertBuilder("T1").set("C").to(Value.float32(Float.NaN)).build());
 
+    // Test NaN arrays
+    tester.addEqualityGroup(
+        Mutation.newInsertBuilder("T1").set("C").toFloat32Array(new float[] {Float.NaN}).build(),
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .toFloat32Array(new float[] {Float.NaN}, 0, 1)
+            .build(),
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .toFloat32Array(Collections.singletonList(Float.NaN))
+            .build(),
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .to(Value.float32Array(new float[] {Float.NaN}))
+            .build(),
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .to(Value.float32Array(new float[] {Float.NaN}, 0, 1))
+            .build(),
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .to(Value.float32Array(Collections.singletonList(Float.NaN)))
+            .build());
     tester.addEqualityGroup(
         Mutation.newInsertBuilder("T1").set("C").toFloat64Array(new double[] {Double.NaN}).build(),
         Mutation.newInsertBuilder("T1").set("C").toFloat64Array(new double[] {Float.NaN}).build(),
@@ -270,6 +295,11 @@ public void equalsAndHashCode() {
             .set("C")
             .toFloat64Array(Arrays.asList(null, (double) Float.NaN))
             .build());
+    tester.addEqualityGroup(
+        Mutation.newInsertBuilder("T1")
+            .set("C")
+            .toFloat32Array(Arrays.asList(null, Float.NaN))
+            .build());
 
     tester.testEquals();
   }
@@ -523,11 +553,17 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) {
         .to((Long) null)
         .set("intValue")
         .to(Value.int64(1L))
-        .set("float")
+        .set("float32")
+        .to(42.1f)
+        .set("float32Null")
+        .to((Float) null)
+        .set("float32Value")
+        .to(Value.float32(10f))
+        .set("float64")
         .to(42.1)
-        .set("floatNull")
+        .set("float64Null")
         .to((Double) null)
-        .set("floatValue")
+        .set("float64Value")
         .to(Value.float64(10D))
         .set("string")
         .to("str")
@@ -583,11 +619,17 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) {
         .toInt64Array((long[]) null)
         .set("intArrValue")
         .to(Value.int64Array(ImmutableList.of(1L, 2L)))
-        .set("floatArr")
+        .set("float32Arr")
+        .toFloat32Array(new float[] {1.1f, 2.2f, 3.3f})
+        .set("float32ArrNull")
+        .toFloat32Array((float[]) null)
+        .set("float32ArrValue")
+        .to(Value.float32Array(ImmutableList.of(10.1F, 10.2F, 10.3F)))
+        .set("float64Arr")
         .toFloat64Array(new double[] {1.1, 2.2, 3.3})
-        .set("floatArrNull")
+        .set("float64ArrNull")
         .toFloat64Array((double[]) null)
-        .set("floatArrValue")
+        .set("float64ArrValue")
         .to(Value.float64Array(ImmutableList.of(10.1D, 10.2D, 10.3D)))
         .set("stringArr")
         .toStringArray(ImmutableList.of("one", "two"))
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java
index b5bbb9dd49a..6cf1d0900ab 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java
@@ -37,6 +37,7 @@ public class RandomResultSetGenerator {
         Type.newBuilder().setCode(TypeCode.BOOL).build(),
         Type.newBuilder().setCode(TypeCode.INT64).build(),
         Type.newBuilder().setCode(TypeCode.FLOAT64).build(),
+        Type.newBuilder().setCode(TypeCode.FLOAT32).build(),
         Type.newBuilder().setCode(TypeCode.STRING).build(),
         Type.newBuilder().setCode(TypeCode.BYTES).build(),
         Type.newBuilder().setCode(TypeCode.DATE).build(),
@@ -53,6 +54,10 @@ public class RandomResultSetGenerator {
             .setCode(TypeCode.ARRAY)
             .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64))
             .build(),
+        Type.newBuilder()
+            .setCode(TypeCode.ARRAY)
+            .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT32))
+            .build(),
         Type.newBuilder()
             .setCode(TypeCode.ARRAY)
             .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING))
@@ -138,6 +143,9 @@ private void setRandomValue(Value.Builder builder, Type type) {
         case FLOAT64:
           builder.setNumberValue(random.nextDouble());
           break;
+        case FLOAT32:
+          builder.setNumberValue(random.nextFloat());
+          break;
         case INT64:
           builder.setStringValue(String.valueOf(random.nextLong()));
           break;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
index bb8d1309142..6801c82b66c 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
@@ -298,9 +298,12 @@ public void readOnlyTransaction() throws Exception {
         values2 = rs.toListAsync(input -> input.getString("Value"), executor);
       }
     }
+
+    ApiFuture<List<List<String>>> allValuesAsList =
+        ApiFutures.allAsList(Arrays.asList(values1, values2));
     ApiFuture<Iterable<String>> allValues =
         ApiFutures.transform(
-            ApiFutures.allAsList(Arrays.asList(values1, values2)),
+            allValuesAsList,
             input ->
                 Iterables.mergeSorted(
                     input,
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java
index a72c9872faf..8d97d9d894b 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java
@@ -170,6 +170,9 @@ private void assertRow(Struct actualRow, JSONArray expectedRow) throws Exception
           case INT64:
             assertThat(actualRow.getLong(i)).isEqualTo(expectedRow.getLong(i));
             break;
+          case FLOAT32:
+            assertThat(actualRow.getFloat(i)).isEqualTo(expectedRow.getFloat(i));
+            break;
           case FLOAT64:
             assertThat(actualRow.getDouble(i)).isEqualTo(expectedRow.getDouble(i));
             break;
@@ -208,6 +211,9 @@ private List<?> getRawList(Struct actualRow, int index, Type elementType) {
         case INT64:
           rawList = actualRow.getLongList(index);
           break;
+        case FLOAT32:
+          rawList = actualRow.getFloatList(index);
+          break;
         case FLOAT64:
           rawList = actualRow.getDoubleList(index);
           break;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java
index 8e1f257594b..454bd3c70a4 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java
@@ -31,6 +31,7 @@
 import com.google.cloud.spanner.SingerProto.Genre;
 import com.google.cloud.spanner.SingerProto.SingerInfo;
 import com.google.common.primitives.Doubles;
+import com.google.common.primitives.Floats;
 import com.google.common.primitives.Longs;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.protobuf.AbstractMessage;
@@ -53,6 +54,7 @@ public class ResultSetsTest {
   @Test
   public void resultSetIteration() {
     double doubleVal = 1.2;
+    float floatVal = 6.626f;
     BigDecimal bigDecimalVal = BigDecimal.valueOf(123, 2);
     String stringVal = "stringVal";
     String jsonVal = "{\"color\":\"red\",\"value\":\"#f00\"}";
@@ -71,6 +73,7 @@ public void resultSetIteration() {
     boolean[] boolArray = {true, false, true, true, false};
     long[] longArray = {Long.MAX_VALUE, Long.MIN_VALUE, 0, 1, -1};
     double[] doubleArray = {Double.MIN_VALUE, Double.MAX_VALUE, 0, 1, -1, 1.2341};
+    float[] floatArray = {Float.MIN_VALUE, Float.MAX_VALUE, 0, 1, -1, 1.2341f};
     BigDecimal[] bigDecimalArray = {
       BigDecimal.valueOf(1, Integer.MAX_VALUE),
       BigDecimal.valueOf(1, Integer.MIN_VALUE),
@@ -102,6 +105,7 @@ public void resultSetIteration() {
             Type.StructField.of("f2", Type.int64()),
             Type.StructField.of("f3", Type.bool()),
             Type.StructField.of("doubleVal", Type.float64()),
+            Type.StructField.of("floatVal", Type.float32()),
             Type.StructField.of("bigDecimalVal", Type.numeric()),
             Type.StructField.of("stringVal", Type.string()),
             Type.StructField.of("jsonVal", Type.json()),
@@ -116,6 +120,7 @@ public void resultSetIteration() {
             Type.StructField.of("boolArray", Type.array(Type.bool())),
             Type.StructField.of("longArray", Type.array(Type.int64())),
             Type.StructField.of("doubleArray", Type.array(Type.float64())),
+            Type.StructField.of("floatArray", Type.array(Type.float32())),
             Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())),
             Type.StructField.of("byteArray", Type.array(Type.bytes())),
             Type.StructField.of("timestampArray", Type.array(Type.timestamp())),
@@ -138,6 +143,8 @@ public void resultSetIteration() {
             .to(Value.bool(true))
             .set("doubleVal")
             .to(Value.float64(doubleVal))
+            .set("floatVal")
+            .to(Value.float32(floatVal))
             .set("bigDecimalVal")
             .to(Value.numeric(bigDecimalVal))
             .set("stringVal")
@@ -162,6 +169,8 @@ public void resultSetIteration() {
             .to(Value.int64Array(longArray))
             .set("doubleArray")
             .to(Value.float64Array(doubleArray))
+            .set("floatArray")
+            .to(Value.float32Array(floatArray))
             .set("bigDecimalArray")
             .to(Value.numericArray(Arrays.asList(bigDecimalArray)))
             .set("byteArray")
@@ -195,6 +204,8 @@ public void resultSetIteration() {
             .to(Value.bool(null))
             .set("doubleVal")
             .to(Value.float64(doubleVal))
+            .set("floatVal")
+            .to(Value.float32(floatVal))
             .set("bigDecimalVal")
             .to(Value.numeric(bigDecimalVal))
             .set("stringVal")
@@ -219,6 +230,8 @@ public void resultSetIteration() {
             .to(Value.int64Array(longArray))
             .set("doubleArray")
             .to(Value.float64Array(doubleArray))
+            .set("floatArray")
+            .to(Value.float32Array(floatArray))
             .set("bigDecimalArray")
             .to(Value.numericArray(Arrays.asList(bigDecimalArray)))
             .set("byteArray")
@@ -274,6 +287,10 @@ public void resultSetIteration() {
     assertThat(rs.getValue("doubleVal").getFloat64()).isWithin(0.0).of(doubleVal);
     assertThat(rs.getDouble(columnIndex)).isWithin(0.0).of(doubleVal);
     assertThat(rs.getValue(columnIndex++).getFloat64()).isWithin(0.0).of(doubleVal);
+    assertThat(rs.getFloat(columnIndex)).isWithin(0.0f).of(floatVal);
+    assertThat(rs.getValue(columnIndex++).getFloat32()).isWithin(0.0f).of(floatVal);
+    assertThat(rs.getFloat("floatVal")).isWithin(0.0f).of(floatVal);
+    assertThat(rs.getValue("floatVal").getFloat32()).isWithin(0.0f).of(floatVal);
     assertThat(rs.getBigDecimal("bigDecimalVal")).isEqualTo(new BigDecimal("1.23"));
     assertThat(rs.getValue("bigDecimalVal")).isEqualTo(Value.numeric(new BigDecimal("1.23")));
     assertThat(rs.getBigDecimal(columnIndex)).isEqualTo(new BigDecimal("1.23"));
@@ -338,6 +355,17 @@ public void resultSetIteration() {
     assertThat(rs.getValue("doubleArray")).isEqualTo(Value.float64Array(doubleArray));
     assertThat(rs.getDoubleList(columnIndex++)).isEqualTo(Doubles.asList(doubleArray));
     assertThat(rs.getDoubleList("doubleArray")).isEqualTo(Doubles.asList(doubleArray));
+
+    assertThat(rs.getFloatArray(columnIndex)).usingTolerance(0.0f).containsAtLeast(floatArray);
+    assertThat(rs.getValue(columnIndex)).isEqualTo(Value.float32Array(floatArray));
+    assertThat(rs.getFloatArray("floatArray"))
+        .usingTolerance(0.0f)
+        .containsExactly(floatArray)
+        .inOrder();
+    assertThat(rs.getValue("floatArray")).isEqualTo(Value.float32Array(floatArray));
+    assertThat(rs.getFloatList(columnIndex++)).isEqualTo(Floats.asList(floatArray));
+    assertThat(rs.getFloatList("floatArray")).isEqualTo(Floats.asList(floatArray));
+
     assertThat(rs.getBigDecimalList(columnIndex)).isEqualTo(Arrays.asList(bigDecimalArray));
     assertThat(rs.getValue(columnIndex++))
         .isEqualTo(Value.numericArray(Arrays.asList(bigDecimalArray)));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java
index 11b708ed48b..6eedc058c5a 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java
@@ -110,6 +110,16 @@ Type newType() {
     }.test();
   }
 
+  @Test
+  public void float32() {
+    new ScalarTypeTester(Type.Code.FLOAT32, TypeCode.FLOAT32) {
+      @Override
+      Type newType() {
+        return Type.float32();
+      }
+    }.test();
+  }
+
   @Test
   public void float64() {
     new ScalarTypeTester(Type.Code.FLOAT64, TypeCode.FLOAT64) {
@@ -307,6 +317,16 @@ Type newElementType() {
     }.test();
   }
 
+  @Test
+  public void float32Array() {
+    new ArrayTypeTester(Type.Code.FLOAT32, TypeCode.FLOAT32, true) {
+      @Override
+      Type newElementType() {
+        return Type.float32();
+      }
+    }.test();
+  }
+
   @Test
   public void float64Array() {
     new ArrayTypeTester(Type.Code.FLOAT64, TypeCode.FLOAT64, true) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java
index d50814e84d4..204880bf7d6 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java
@@ -265,6 +265,14 @@ public static Long defaultLongWrapper() {
       return 1234L;
     }
 
+    public static float defaultFloatPrimitive() {
+      return 1.0f;
+    }
+
+    public static Float defaultFloatWrapper() {
+      return 1.0f;
+    }
+
     public static double defaultDoublePrimitive() {
       return 1.0;
     }
@@ -329,6 +337,14 @@ public static Iterable<Long> defaultLongIterable() {
       return Arrays.asList(1L, 2L);
     }
 
+    public static float[] defaultFloatArray() {
+      return new float[] {1.0f, 2.0f};
+    }
+
+    public static Iterable<Float> defaultFloatIterable() {
+      return Arrays.asList(1.0f, 2.0f);
+    }
+
     public static double[] defaultDoubleArray() {
       return new double[] {1.0, 2.0};
     }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java
index 5176013cf38..d692fc44e70 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java
@@ -185,6 +185,37 @@ public void int64WrapperNull() {
     assertEquals("NULL", v.getAsString());
   }
 
+  @Test
+  public void float32() {
+    Value v = Value.float32(1.23f);
+    assertThat(v.getType()).isEqualTo(Type.float32());
+    assertThat(v.isNull()).isFalse();
+    assertThat(v.getFloat32()).isWithin(0.0001f).of(1.23f);
+    assertThat(v.toString()).isEqualTo("1.23");
+    assertEquals("1.23", v.getAsString());
+  }
+
+  @Test
+  public void float32Wrapper() {
+    Value v = Value.float32(Float.valueOf(1.23f));
+    assertThat(v.getType()).isEqualTo(Type.float32());
+    assertThat(v.isNull()).isFalse();
+    assertThat(v.getFloat32()).isWithin(0.0001f).of(1.23f);
+    assertThat(v.toString()).isEqualTo("1.23");
+    assertEquals("1.23", v.getAsString());
+  }
+
+  @Test
+  public void float32WrapperNull() {
+    Value v = Value.float32(null);
+    assertThat(v.getType()).isEqualTo(Type.float32());
+    assertThat(v.isNull()).isTrue();
+    assertThat(v.toString()).isEqualTo(NULL_STRING);
+    IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32);
+    assertThat(e.getMessage()).contains("null value");
+    assertEquals("NULL", v.getAsString());
+  }
+
   @Test
   public void float64() {
     Value v = Value.float64(1.23);
@@ -863,6 +894,60 @@ public void int64ArrayNullTryGetBool() {
     assertThat(e.getMessage()).contains("Expected: BOOL actual: ARRAY<INT64>");
   }
 
+  @Test
+  public void float32Array() {
+    Value v = Value.float32Array(new float[] {.1f, .2f});
+    assertThat(v.isNull()).isFalse();
+    assertThat(v.getFloat32Array()).containsExactly(.1f, .2f).inOrder();
+    assertThat(v.toString()).isEqualTo("[0.1,0.2]");
+    assertEquals("[0.1,0.2]", v.getAsString());
+  }
+
+  @Test
+  public void float32ArrayRange() {
+    Value v = Value.float32Array(new float[] {.1f, .2f, .3f, .4f, .5f}, 1, 3);
+    assertThat(v.isNull()).isFalse();
+    assertThat(v.getFloat32Array()).containsExactly(.2f, .3f, .4f).inOrder();
+    assertThat(v.toString()).isEqualTo("[0.2,0.3,0.4]");
+    assertEquals("[0.2,0.3,0.4]", v.getAsString());
+  }
+
+  @Test
+  public void float32ArrayNull() {
+    Value v = Value.float32Array((float[]) null);
+    assertThat(v.isNull()).isTrue();
+    assertThat(v.toString()).isEqualTo(NULL_STRING);
+    IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32Array);
+    assertThat(e.getMessage()).contains("null value");
+    assertEquals("NULL", v.getAsString());
+  }
+
+  @Test
+  public void float32ArrayWrapper() {
+    Value v = Value.float32Array(Arrays.asList(.1f, null, .3f));
+    assertThat(v.isNull()).isFalse();
+    assertThat(v.getFloat32Array()).containsExactly(.1f, null, .3f).inOrder();
+    assertThat(v.toString()).isEqualTo("[0.1,NULL,0.3]");
+    assertEquals("[0.1,NULL,0.3]", v.getAsString());
+  }
+
+  @Test
+  public void float32ArrayWrapperNull() {
+    Value v = Value.float32Array((Iterable<Float>) null);
+    assertThat(v.isNull()).isTrue();
+    assertThat(v.toString()).isEqualTo(NULL_STRING);
+    IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32Array);
+    assertThat(e.getMessage()).contains("null value");
+    assertEquals("NULL", v.getAsString());
+  }
+
+  @Test
+  public void float32ArrayTryGetFloat64Array() {
+    Value value = Value.float32Array(Collections.singletonList(.1f));
+    IllegalStateException e = assertThrows(IllegalStateException.class, value::getFloat64Array);
+    assertThat(e.getMessage()).contains("Expected: ARRAY<FLOAT64> actual: ARRAY<FLOAT32>");
+  }
+
   @Test
   public void float64Array() {
     Value v = Value.float64Array(new double[] {.1, .2});
@@ -1426,6 +1511,13 @@ public void testValueToProto() {
         com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(),
         Value.int64(null).toProto());
 
+    assertEquals(
+        com.google.protobuf.Value.newBuilder().setNumberValue(3.14f).build(),
+        Value.float32(3.14f).toProto());
+    assertEquals(
+        com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(),
+        Value.float32(null).toProto());
+
     assertEquals(
         com.google.protobuf.Value.newBuilder().setNumberValue(3.14d).build(),
         Value.float64(3.14d).toProto());
@@ -1512,6 +1604,18 @@ public void testValueToProto() {
                                 .build())))
             .build(),
         Value.int64Array(Arrays.asList(1L, null)).toProto());
+    assertEquals(
+        com.google.protobuf.Value.newBuilder()
+            .setListValue(
+                ListValue.newBuilder()
+                    .addAllValues(
+                        Arrays.asList(
+                            com.google.protobuf.Value.newBuilder().setNumberValue(3.14f).build(),
+                            com.google.protobuf.Value.newBuilder()
+                                .setNullValue(NullValue.NULL_VALUE)
+                                .build())))
+            .build(),
+        Value.float32Array(Arrays.asList(3.14f, null)).toProto());
     assertEquals(
         com.google.protobuf.Value.newBuilder()
             .setListValue(
@@ -1667,6 +1771,29 @@ public void testValueToProto() {
             .build(),
         Value.struct(Struct.newBuilder().add(Value.int64Array(Arrays.asList(1L, null))).build())
             .toProto());
+    assertEquals(
+        com.google.protobuf.Value.newBuilder()
+            .setListValue(
+                ListValue.newBuilder()
+                    .addValues(
+                        com.google.protobuf.Value.newBuilder()
+                            .setListValue(
+                                ListValue.newBuilder()
+                                    .addAllValues(
+                                        Arrays.asList(
+                                            com.google.protobuf.Value.newBuilder()
+                                                .setNumberValue(3.14f)
+                                                .build(),
+                                            com.google.protobuf.Value.newBuilder()
+                                                .setNullValue(NullValue.NULL_VALUE)
+                                                .build()))
+                                    .build())
+                            .build())
+                    .build())
+            .build(),
+        Value.struct(
+                Struct.newBuilder().add(Value.float32Array(Arrays.asList(3.14f, null))).build())
+            .toProto());
     assertEquals(
         com.google.protobuf.Value.newBuilder()
             .setListValue(
@@ -1872,6 +1999,10 @@ public void testEqualsHashCode() {
     tester.addEqualityGroup(Value.int64(456));
     tester.addEqualityGroup(Value.int64(null));
 
+    tester.addEqualityGroup(Value.float32(1.23f), Value.float32(Float.valueOf(1.23f)));
+    tester.addEqualityGroup(Value.float32(4.56f));
+    tester.addEqualityGroup(Value.float32(null));
+
     tester.addEqualityGroup(Value.float64(1.23), Value.float64(Double.valueOf(1.23)));
     tester.addEqualityGroup(Value.float64(4.56));
     tester.addEqualityGroup(Value.float64(null));
@@ -1938,6 +2069,14 @@ public void testEqualsHashCode() {
     tester.addEqualityGroup(Value.int64Array(Collections.singletonList(3L)));
     tester.addEqualityGroup(Value.int64Array((Iterable<Long>) null));
 
+    tester.addEqualityGroup(
+        Value.float32Array(Arrays.asList(.1f, .2f)),
+        Value.float32Array(new float[] {.1f, .2f}),
+        Value.float32Array(new float[] {.0f, .1f, .2f, .3f}, 1, 2),
+        Value.float32Array(plainIterable(.1f, .2f)));
+    tester.addEqualityGroup(Value.float32Array(Collections.singletonList(.3f)));
+    tester.addEqualityGroup(Value.float32Array((Iterable<Float>) null));
+
     tester.addEqualityGroup(
         Value.float64Array(Arrays.asList(.1, .2)),
         Value.float64Array(new double[] {.1, .2}),
@@ -2009,6 +2148,11 @@ public void testGetAsString() {
     assertEquals(String.valueOf(Long.MAX_VALUE), Value.int64(Long.MAX_VALUE).getAsString());
     assertEquals(String.valueOf(Long.MIN_VALUE), Value.int64(Long.MIN_VALUE).getAsString());
 
+    assertEquals("3.14", Value.float32(3.14f).getAsString());
+    assertEquals("NaN", Value.float32(Float.NaN).getAsString());
+    assertEquals(String.valueOf(Float.MIN_VALUE), Value.float32(Float.MIN_VALUE).getAsString());
+    assertEquals(String.valueOf(Float.MAX_VALUE), Value.float32(Float.MAX_VALUE).getAsString());
+
     assertEquals("3.14", Value.float64(3.14d).getAsString());
     assertEquals("NaN", Value.float64(Double.NaN).getAsString());
     assertEquals(String.valueOf(Double.MIN_VALUE), Value.float64(Double.MIN_VALUE).getAsString());
@@ -2052,6 +2196,9 @@ public void serialization() {
     reserializeAndAssert(Value.int64(123));
     reserializeAndAssert(Value.int64(null));
 
+    reserializeAndAssert(Value.float32(1.23f));
+    reserializeAndAssert(Value.float32(null));
+
     reserializeAndAssert(Value.float64(1.23));
     reserializeAndAssert(Value.float64(null));
 
@@ -2089,6 +2236,10 @@ public void serialization() {
     reserializeAndAssert(Value.int64Array(new long[] {1L, 2L}));
     reserializeAndAssert(Value.int64Array((Iterable<Long>) null));
 
+    reserializeAndAssert(Value.float32Array(new float[] {.1f, .2f}));
+    reserializeAndAssert(Value.float32Array(BrokenSerializationList.of(.1f, .2f, .3f)));
+    reserializeAndAssert(Value.float32Array((Iterable<Float>) null));
+
     reserializeAndAssert(Value.float64Array(new double[] {.1, .2}));
     reserializeAndAssert(Value.float64Array(BrokenSerializationList.of(.1, .2, .3)));
     reserializeAndAssert(Value.float64Array((Iterable<Double>) null));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java
index 4e8fb0cfcbb..759f058aa03 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java
@@ -57,6 +57,8 @@ public class ChecksumResultSetTest {
           .to(2 * 2)
           .set("doubleVal")
           .to(Value.float64(3.14d * 2d))
+          .set("floatVal")
+          .to(Value.float32(3.14f * 3f))
           .set("bigDecimalVal")
           .to(Value.numeric(BigDecimal.valueOf(123 * 2, 2)))
           .set("pgNumericVal")
@@ -83,6 +85,8 @@ public class ChecksumResultSetTest {
           .to(Value.int64Array(Arrays.asList(2L, null, 1L, 0L)))
           .set("doubleArray")
           .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d, 10.1d)))
+          .set("floatArray")
+          .to(Value.float32Array(Arrays.asList(2.71f, null, 6.6626f, 10.1f)))
           .set("bigDecimalArray")
           .to(Value.numericArray(Arrays.asList(BigDecimal.TEN, null, BigDecimal.ONE)))
           .set("pgNumericArray")
@@ -128,6 +132,7 @@ public void testRetry() {
             Type.StructField.of("boolVal", Type.bool()),
             Type.StructField.of("longVal", Type.int64()),
             Type.StructField.of("doubleVal", Type.float64()),
+            Type.StructField.of("floatVal", Type.float32()),
             Type.StructField.of("bigDecimalVal", Type.numeric()),
             Type.StructField.of("pgNumericVal", Type.pgNumeric()),
             Type.StructField.of("stringVal", Type.string()),
@@ -143,6 +148,7 @@ public void testRetry() {
             Type.StructField.of("boolArray", Type.array(Type.bool())),
             Type.StructField.of("longArray", Type.array(Type.int64())),
             Type.StructField.of("doubleArray", Type.array(Type.float64())),
+            Type.StructField.of("floatArray", Type.array(Type.float32())),
             Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())),
             Type.StructField.of("pgNumericArray", Type.array(Type.pgNumeric())),
             Type.StructField.of("byteArray", Type.array(Type.bytes())),
@@ -164,6 +170,8 @@ public void testRetry() {
             .to(2)
             .set("doubleVal")
             .to(Value.float64(3.14d))
+            .set("floatVal")
+            .to(Value.float32(2.71f))
             .set("bigDecimalVal")
             .to(Value.numeric(BigDecimal.valueOf(123, 2)))
             .set("pgNumericVal")
@@ -190,6 +198,8 @@ public void testRetry() {
             .to(Value.int64Array(Arrays.asList(1L, null, 2L)))
             .set("doubleArray")
             .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d)))
+            .set("floatArray")
+            .to(Value.float32Array(Arrays.asList(2.71f, null, 6.6626f)))
             .set("bigDecimalArray")
             .to(Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)))
             .set("pgNumericArray")
@@ -238,6 +248,8 @@ public void testRetry() {
             .to((Long) null)
             .set("doubleVal")
             .to((Double) null)
+            .set("floatVal")
+            .to((Float) null)
             .set("bigDecimalVal")
             .to((BigDecimal) null)
             .set("pgNumericVal")
@@ -264,6 +276,8 @@ public void testRetry() {
             .toInt64Array((Iterable<Long>) null)
             .set("doubleArray")
             .toFloat64Array((Iterable<Double>) null)
+            .set("floatArray")
+            .toFloat32Array((Iterable<Float>) null)
             .set("bigDecimalArray")
             .toNumericArray(null)
             .set("pgNumericArray")
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java
index 5a22c64009f..87f26da9049 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java
@@ -344,9 +344,12 @@ public void readOnlyTransaction() throws Exception {
         values2 = rs.toListAsync(input -> input.getString("StringValue"), executor);
       }
     }
+
+    ApiFuture<List<List<String>>> allAsListValues =
+        ApiFutures.allAsList(Arrays.asList(values1, values2));
     ApiFuture<Iterable<String>> allValues =
         ApiFutures.transform(
-            ApiFutures.allAsList(Arrays.asList(values1, values2)),
+            allAsListValues,
             input ->
                 Iterables.mergeSorted(
                     input, Comparator.comparing(o -> Integer.valueOf(o.substring(1)))),
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java
new file mode 100644
index 00000000000..de2640b86cd
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2024 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.it;
+
+import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.Key;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.TimestampBound;
+import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.connection.ConnectionOptions;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@Category(ParallelIntegrationTest.class)
+@RunWith(Parameterized.class)
+public class ITFloat32Test {
+
+  @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+
+  @Parameterized.Parameters(name = "Dialect = {0}")
+  public static List<DialectTestParameter> data() {
+    return Arrays.asList(
+        new DialectTestParameter(Dialect.GOOGLE_STANDARD_SQL),
+        new DialectTestParameter(Dialect.POSTGRESQL));
+  }
+
+  @Parameterized.Parameter() public DialectTestParameter dialect;
+
+  private static DatabaseClient googleStandardSQLClient;
+  private static DatabaseClient postgreSQLClient;
+
+  private static final String[] GOOGLE_STANDARD_SQL_SCHEMA =
+      new String[] {
+        "CREATE TABLE T ("
+            + "  Key                 STRING(MAX) NOT NULL,"
+            + "  Float32Value        FLOAT32,"
+            + "  Float32ArrayValue   ARRAY<FLOAT32>,"
+            + ") PRIMARY KEY (Key)"
+      };
+
+  private static final String[] POSTGRESQL_SCHEMA =
+      new String[] {
+        "CREATE TABLE T ("
+            + "  Key                 VARCHAR PRIMARY KEY,"
+            + "  Float32Value        REAL,"
+            + "  Float32ArrayValue   REAL[]"
+            + ")"
+      };
+
+  private static DatabaseClient client;
+
+  private static boolean isUsingCloudDevel() {
+    String jobType = System.getenv("JOB_TYPE");
+
+    // Assumes that the jobType contains the string "cloud-devel" to signal that
+    // the environment is cloud-devel.
+    return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel");
+  }
+
+  @BeforeClass
+  public static void setUpDatabase()
+      throws ExecutionException, InterruptedException, TimeoutException {
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+
+    Database googleStandardSQLDatabase =
+        env.getTestHelper().createTestDatabase(GOOGLE_STANDARD_SQL_SCHEMA);
+
+    googleStandardSQLClient = env.getTestHelper().getDatabaseClient(googleStandardSQLDatabase);
+
+    Database postgreSQLDatabase =
+        env.getTestHelper()
+            .createTestDatabase(Dialect.POSTGRESQL, Arrays.asList(POSTGRESQL_SCHEMA));
+    postgreSQLClient = env.getTestHelper().getDatabaseClient(postgreSQLDatabase);
+  }
+
+  @Before
+  public void before() {
+    client =
+        dialect.dialect == Dialect.GOOGLE_STANDARD_SQL ? googleStandardSQLClient : postgreSQLClient;
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    ConnectionOptions.closeSpanner();
+  }
+
+  /** Sequence used to generate unique keys. */
+  private static int seq;
+
+  private static String uniqueString() {
+    return String.format("k%04d", seq++);
+  }
+
+  private String lastKey;
+
+  private Timestamp write(Mutation m) {
+    return client.write(Collections.singletonList(m));
+  }
+
+  private Mutation.WriteBuilder baseInsert() {
+    return Mutation.newInsertOrUpdateBuilder("T").set("Key").to(lastKey = uniqueString());
+  }
+
+  private Struct readRow(String table, String key, String... columns) {
+    return client
+        .singleUse(TimestampBound.strong())
+        .readRow(table, Key.of(key), Arrays.asList(columns));
+  }
+
+  private Struct readLastRow(String... columns) {
+    return readRow("T", lastKey, columns);
+  }
+
+  @Test
+  public void writeFloat32() {
+    write(baseInsert().set("Float32Value").to(2.0f).build());
+    Struct row = readLastRow("Float32Value");
+    assertFalse(row.isNull(0));
+    assertEquals(2.0f, row.getFloat(0), 0.0f);
+  }
+
+  @Test
+  public void writeFloat32NonNumbers() {
+
+    write(baseInsert().set("Float32Value").to(Float.NEGATIVE_INFINITY).build());
+    Struct row = readLastRow("Float32Value");
+    assertFalse(row.isNull(0));
+    assertEquals(Float.NEGATIVE_INFINITY, row.getFloat(0), 0.0f);
+
+    write(baseInsert().set("Float32Value").to(Float.POSITIVE_INFINITY).build());
+    row = readLastRow("Float32Value");
+    assertFalse(row.isNull(0));
+    assertEquals(Float.POSITIVE_INFINITY, row.getFloat(0), 0.0);
+
+    write(baseInsert().set("Float32Value").to(Float.NaN).build());
+    row = readLastRow("Float32Value");
+    assertFalse(row.isNull(0));
+    assertTrue(Float.isNaN(row.getFloat(0)));
+  }
+
+  @Test
+  public void writeFloat32Null() {
+    write(baseInsert().set("Float32Value").to((Float) null).build());
+    Struct row = readLastRow("Float32Value");
+    assertTrue(row.isNull(0));
+  }
+
+  @Test
+  public void writeFloat32ArrayNull() {
+    write(baseInsert().set("Float32ArrayValue").toFloat32Array((float[]) null).build());
+    Struct row = readLastRow("Float32ArrayValue");
+    assertTrue(row.isNull(0));
+  }
+
+  @Test
+  public void writeFloat32ArrayEmpty() {
+    write(baseInsert().set("Float32ArrayValue").toFloat32Array(new float[] {}).build());
+    Struct row = readLastRow("Float32ArrayValue");
+    assertFalse(row.isNull(0));
+    assertTrue(row.getFloatList(0).isEmpty());
+  }
+
+  @Test
+  public void writeFloat32Array() {
+    write(
+        baseInsert()
+            .set("Float32ArrayValue")
+            .toFloat32Array(Arrays.asList(null, 1.0f, 2.0f))
+            .build());
+    Struct row = readLastRow("Float32ArrayValue");
+    assertFalse(row.isNull(0));
+    assertEquals(row.getFloatList(0), Arrays.asList(null, 1.0f, 2.0f));
+    assertThrows(NullPointerException.class, () -> row.getFloatArray(0));
+  }
+
+  @Test
+  public void writeFloat32ArrayNoNulls() {
+    write(baseInsert().set("Float32ArrayValue").toFloat32Array(Arrays.asList(1.0f, 2.0f)).build());
+    Struct row = readLastRow("Float32ArrayValue");
+    assertFalse(row.isNull(0));
+    assertEquals(2, row.getFloatArray(0).length);
+    assertEquals(1.0f, row.getFloatArray(0)[0], 0.0f);
+    assertEquals(2.0f, row.getFloatArray(0)[1], 0.0f);
+  }
+
+  private String getInsertStatementWithLiterals() {
+    String statement = "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES ";
+
+    if (dialect.dialect == Dialect.POSTGRESQL) {
+      statement +=
+          "('dml1', 3.14::float8, array[1.1]::float4[]), "
+              + "('dml2', '3.14'::float4, array[3.14::float4, 3.14::float8]::float4[]), "
+              + "('dml3', 'nan'::real, array['inf'::real, (3.14::float8)::float4, 1.2, '-inf']::float4[]), "
+              + "('dml4', 1.175494e-38::real, array[1.175494e-38, 3.4028234e38, -3.4028234e38]::real[]), "
+              + "('dml5', null, null)";
+    } else {
+      statement +=
+          "('dml1', 3.14, [CAST(1.1 AS FLOAT32)]), "
+              + "('dml2', CAST('3.14' AS FLOAT32), array[CAST(3.14 AS FLOAT32), 3.14]), "
+              + "('dml3', CAST('nan' AS FLOAT32), array[CAST('inf' AS FLOAT32), CAST(CAST(3.14 AS FLOAT64) AS FLOAT32), 1.2, CAST('-inf' AS FLOAT32)]), "
+              + "('dml4', 1.175494e-38, [CAST(1.175494e-38 AS FLOAT32), 3.4028234e38, -3.4028234e38]), "
+              + "('dml5', null, null)";
+    }
+    return statement;
+  }
+
+  @Test
+  public void float32Literals() {
+    client
+        .readWriteTransaction()
+        .run(
+            transaction -> {
+              transaction.executeUpdate(Statement.of(getInsertStatementWithLiterals()));
+              return null;
+            });
+
+    verifyContents("dml");
+  }
+
+  private String getInsertStatementWithParameters() {
+    String pgStatement =
+        "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES "
+            + "('param1', $1, $2), "
+            + "('param2', $3, $4), "
+            + "('param3', $5, $6), "
+            + "('param4', $7, $8), "
+            + "('param5', $9, $10)";
+
+    return (dialect.dialect == Dialect.POSTGRESQL) ? pgStatement : pgStatement.replace("$", "@p");
+  }
+
+  @Test
+  public void float32Parameter() {
+    client
+        .readWriteTransaction()
+        .run(
+            transaction -> {
+              transaction.executeUpdate(
+                  Statement.newBuilder(getInsertStatementWithParameters())
+                      .bind("p1")
+                      .to(Value.float32(3.14f))
+                      .bind("p2")
+                      .to(Value.float32Array(Arrays.asList(1.1f)))
+                      .bind("p3")
+                      .to(Value.float32(3.14f))
+                      .bind("p4")
+                      .to(Value.float32Array(new float[] {3.14f, 3.14f}))
+                      .bind("p5")
+                      .to(Value.float32(Float.NaN))
+                      .bind("p6")
+                      .to(
+                          Value.float32Array(
+                              Arrays.asList(
+                                  Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY)))
+                      .bind("p7")
+                      .to(Value.float32(Float.MIN_NORMAL))
+                      .bind("p8")
+                      .to(
+                          Value.float32Array(
+                              Arrays.asList(
+                                  Float.MIN_NORMAL, Float.MAX_VALUE, -1 * Float.MAX_VALUE)))
+                      .bind("p9")
+                      .to(Value.float32(null))
+                      .bind("p10")
+                      .to(Value.float32Array((float[]) null))
+                      .build());
+              return null;
+            });
+
+    verifyContents("param");
+  }
+
+  private String getInsertStatementForUntypedParameters() {
+    if (dialect.dialect == Dialect.POSTGRESQL) {
+      return "INSERT INTO T (key, float32value, float32arrayvalue) VALUES "
+          + "('untyped1', ($1)::float4, ($2)::float4[])";
+    }
+    return "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES "
+        + "('untyped1', CAST(@p1 AS FLOAT32), CAST(@p2 AS ARRAY<FLOAT32>))";
+  }
+
+  @Test
+  public void float32UntypedParameter() {
+    client
+        .readWriteTransaction()
+        .run(
+            transaction -> {
+              transaction.executeUpdate(
+                  Statement.newBuilder(getInsertStatementForUntypedParameters())
+                      .bind("p1")
+                      .to(
+                          Value.untyped(
+                              com.google.protobuf.Value.newBuilder()
+                                  .setNumberValue((double) 3.14f)
+                                  .build()))
+                      .bind("p2")
+                      .to(
+                          Value.untyped(
+                              com.google.protobuf.Value.newBuilder()
+                                  .setListValue(
+                                      com.google.protobuf.ListValue.newBuilder()
+                                          .addValues(
+                                              com.google.protobuf.Value.newBuilder()
+                                                  .setNumberValue((double) Float.MIN_NORMAL)))
+                                  .build()))
+                      .build());
+              return null;
+            });
+
+    Struct row = readRow("T", "untyped1", "Float32Value", "Float32ArrayValue");
+    // Float32Value
+    assertFalse(row.isNull(0));
+    assertEquals(3.14f, row.getFloat(0), 0.00001f);
+    // Float32ArrayValue
+    assertFalse(row.isNull(1));
+    assertEquals(1, row.getFloatList(1).size());
+    assertEquals(Float.MIN_NORMAL, row.getFloatList(1).get(0), 0.00001f);
+  }
+
+  private void verifyContents(String keyPrefix) {
+    try (ResultSet resultSet =
+        client
+            .singleUse()
+            .executeQuery(
+                Statement.of(
+                    "SELECT Key AS key, Float32Value AS float32value, Float32ArrayValue AS float32arrayvalue FROM T WHERE Key LIKE '{keyPrefix}%' ORDER BY key"
+                        .replace("{keyPrefix}", keyPrefix)))) {
+
+      assertTrue(resultSet.next());
+
+      assertEquals(3.14f, resultSet.getFloat("float32value"), 0.00001f);
+      assertEquals(Value.float32(3.14f), resultSet.getValue("float32value"));
+
+      assertArrayEquals(new float[] {1.1f}, resultSet.getFloatArray("float32arrayvalue"), 0.00001f);
+
+      assertTrue(resultSet.next());
+
+      assertEquals(3.14f, resultSet.getFloat("float32value"), 0.00001f);
+      assertEquals(Arrays.asList(3.14f, 3.14f), resultSet.getFloatList("float32arrayvalue"));
+      assertEquals(
+          Value.float32Array(new float[] {3.14f, 3.14f}), resultSet.getValue("float32arrayvalue"));
+
+      assertTrue(resultSet.next());
+      assertTrue(Float.isNaN(resultSet.getFloat("float32value")));
+      assertTrue(Float.isNaN(resultSet.getValue("float32value").getFloat32()));
+      assertEquals(
+          Arrays.asList(Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY),
+          resultSet.getFloatList("float32arrayvalue"));
+      assertEquals(
+          Value.float32Array(
+              Arrays.asList(Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY)),
+          resultSet.getValue("float32arrayvalue"));
+
+      assertTrue(resultSet.next());
+      assertEquals(Float.MIN_NORMAL, resultSet.getFloat("float32value"), 0.00001f);
+      assertEquals(Float.MIN_NORMAL, resultSet.getValue("float32value").getFloat32(), 0.00001f);
+      assertEquals(3, resultSet.getFloatList("float32arrayvalue").size());
+      assertEquals(Float.MIN_NORMAL, resultSet.getFloatList("float32arrayvalue").get(0), 0.00001);
+      assertEquals(Float.MAX_VALUE, resultSet.getFloatList("float32arrayvalue").get(1), 0.00001f);
+      assertEquals(
+          -1 * Float.MAX_VALUE, resultSet.getFloatList("float32arrayvalue").get(2), 0.00001f);
+      assertEquals(3, resultSet.getValue("float32arrayvalue").getFloat32Array().size());
+
+      assertTrue(resultSet.next());
+      assertTrue(resultSet.isNull("float32value"));
+      assertTrue(resultSet.isNull("float32arrayvalue"));
+
+      assertFalse(resultSet.next());
+    }
+  }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
index a691fbf78b4..170ce75e696 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java
@@ -17,6 +17,7 @@
 package com.google.cloud.spanner.it;
 
 import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
@@ -254,6 +255,36 @@ public void bindInt64Null() {
     assertThat(row.isNull(0)).isTrue();
   }
 
+  // TODO: Remove once FLOAT32 is supported in production.
+  private static boolean isUsingCloudDevel() {
+    String jobType = System.getenv("JOB_TYPE");
+
+    // Assumes that the jobType contains the string "cloud-devel" to signal that
+    // the environment is cloud-devel.
+    return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel");
+  }
+
+  @Test
+  public void bindFloat32() {
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+
+    Struct row =
+        execute(Statement.newBuilder(selectValueQuery).bind("p1").to(2.0f), Type.float32());
+    assertThat(row.isNull(0)).isFalse();
+    assertThat(row.getFloat(0)).isWithin(0.0f).of(2.0f);
+  }
+
+  @Test
+  public void bindFloat32Null() {
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+
+    Struct row =
+        execute(Statement.newBuilder(selectValueQuery).bind("p1").to((Float) null), Type.float32());
+    assertThat(row.isNull(0)).isTrue();
+  }
+
   @Test
   public void bindFloat64() {
     Struct row = execute(Statement.newBuilder(selectValueQuery).bind("p1").to(2.0), Type.float64());
@@ -497,6 +528,58 @@ public void bindInt64ArrayNull() {
     assertThat(row.isNull(0)).isTrue();
   }
 
+  @Test
+  public void bindFloat32Array() {
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+
+    Struct row =
+        execute(
+            Statement.newBuilder(selectValueQuery)
+                .bind("p1")
+                .toFloat32Array(
+                    asList(
+                        null,
+                        1.0f,
+                        2.0f,
+                        Float.NEGATIVE_INFINITY,
+                        Float.POSITIVE_INFINITY,
+                        Float.NaN)),
+            Type.array(Type.float32()));
+    assertThat(row.isNull(0)).isFalse();
+    assertThat(row.getFloatList(0))
+        .containsExactly(
+            null, 1.0f, 2.0f, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NaN)
+        .inOrder();
+  }
+
+  @Test
+  public void bindFloat32ArrayEmpty() {
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+
+    Struct row =
+        execute(
+            Statement.newBuilder(selectValueQuery)
+                .bind("p1")
+                .toFloat32Array(Collections.emptyList()),
+            Type.array(Type.float32()));
+    assertThat(row.isNull(0)).isFalse();
+    assertThat(row.getFloatList(0)).containsExactly();
+  }
+
+  @Test
+  public void bindFloat32ArrayNull() {
+    assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator());
+    assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel());
+
+    Struct row =
+        execute(
+            Statement.newBuilder(selectValueQuery).bind("p1").toFloat32Array((float[]) null),
+            Type.array(Type.float32()));
+    assertThat(row.isNull(0)).isTrue();
+  }
+
   @Test
   public void bindFloat64Array() {
     Struct row =