This is an automated email from the ASF dual-hosted git repository.

Amar3tto pushed a commit to branch snowflakeio-yaml
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/snowflakeio-yaml by this push:
     new 5bd11daeca4 Add Snowflake Read and Streaming Write
5bd11daeca4 is described below

commit 5bd11daeca49930698d14f331390d5726c00b1a3
Author: Vitaly Terentyev <[email protected]>
AuthorDate: Thu Aug 13 14:25:31 2026 +0400

    Add Snowflake Read and Streaming Write
---
 sdks/java/io/snowflake/build.gradle                |   1 +
 .../SnowflakeReadSchemaTransformProvider.java      | 285 ++++++++++++++++
 .../snowflake/SnowflakeSchemaTransformUtils.java   | 323 ++++++++++++++++++
 .../io/snowflake/SnowflakeWriteConfiguration.java  | 224 ++++++++++++
 .../SnowflakeWriteSchemaTransformProvider.java     | 317 ++++-------------
 .../SnowflakeReadSchemaTransformProviderTest.java  | 311 +++++++++++++++++
 .../SnowflakeWriteSchemaTransformProviderTest.java | 374 ++++++++++++++++-----
 sdks/python/apache_beam/yaml/standard_io.yaml      |  31 +-
 8 files changed, 1523 insertions(+), 343 deletions(-)

diff --git a/sdks/java/io/snowflake/build.gradle 
b/sdks/java/io/snowflake/build.gradle
index 8d9a9a46557..951af328fed 100644
--- a/sdks/java/io/snowflake/build.gradle
+++ b/sdks/java/io/snowflake/build.gradle
@@ -30,6 +30,7 @@ dependencies {
   implementation project(path: 
":sdks:java:extensions:google-cloud-platform-core")
   permitUnusedDeclared project(path: 
":sdks:java:extensions:google-cloud-platform-core")
   implementation library.java.slf4j_api
+  implementation library.java.everit_json_schema
   implementation group: 'net.snowflake', name: 'snowflake-jdbc', version: 
'4.0.2'
   implementation group: 'com.opencsv', name: 'opencsv', version: '5.12.0'
   implementation 'net.snowflake:snowflake-ingest-sdk:4.4.2'
diff --git 
a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProvider.java
 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProvider.java
new file mode 100644
index 00000000000..f914b0f7dd9
--- /dev/null
+++ 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProvider.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.beam.sdk.io.snowflake;
+
+import static 
org.apache.beam.sdk.io.snowflake.SnowflakeSchemaTransformUtils.toRow;
+
+import com.google.auto.service.AutoService;
+import com.google.auto.value.AutoValue;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.beam.sdk.coders.RowCoder;
+import org.apache.beam.sdk.schemas.AutoValueSchema;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
+import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
+import org.apache.beam.sdk.schemas.transforms.SchemaTransform;
+import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider;
+import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider;
+import org.apache.beam.sdk.schemas.utils.JsonUtils;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionRowTuple;
+import org.apache.beam.sdk.values.Row;
+
+/** A {@link SchemaTransformProvider} for reading rows from Snowflake. */
+@SuppressWarnings({
+  "nullness" // TODO(https://github.com/apache/beam/issues/20497)
+})
+@AutoService(SchemaTransformProvider.class)
+public class SnowflakeReadSchemaTransformProvider
+    extends 
TypedSchemaTransformProvider<SnowflakeReadSchemaTransformProvider.Configuration>
 {
+
+  static final String OUTPUT_TAG = "output";
+
+  public static final String IDENTIFIER = 
"beam:schematransform:org.apache.beam:snowflake_read:v1";
+
+  @Override
+  public String identifier() {
+    return IDENTIFIER;
+  }
+
+  @Override
+  public String description() {
+    return "Reads rows from a Snowflake table or query using staged CSV 
files.";
+  }
+
+  @Override
+  protected Class<Configuration> configurationClass() {
+    return Configuration.class;
+  }
+
+  @Override
+  protected SchemaTransform from(Configuration configuration) {
+    configuration.validate();
+    return new SnowflakeReadSchemaTransform(configuration);
+  }
+
+  @Override
+  public List<String> inputCollectionNames() {
+    return Collections.emptyList();
+  }
+
+  @Override
+  public List<String> outputCollectionNames() {
+    return Collections.singletonList(OUTPUT_TAG);
+  }
+
+  private static class SnowflakeReadSchemaTransform extends SchemaTransform
+      implements Serializable {
+
+    private final Configuration configuration;
+
+    private SnowflakeReadSchemaTransform(Configuration configuration) {
+      this.configuration = configuration;
+    }
+
+    @Override
+    public PCollectionRowTuple expand(PCollectionRowTuple input) {
+      Schema outputSchema = 
JsonUtils.beamSchemaFromJsonSchema(configuration.getSchema());
+
+      SnowflakeIO.DataSourceConfiguration dataSourceConfiguration =
+          SnowflakeSchemaTransformUtils.createDataSourceConfiguration(
+              configuration.getServerName(),
+              configuration.getUsername(),
+              configuration.getPassword(),
+              configuration.getOauthToken(),
+              configuration.getPrivateKey(),
+              configuration.getPrivateKeyPassphrase(),
+              configuration.getDatabase(),
+              configuration.getSnowflakeSchema(),
+              configuration.getWarehouse(),
+              configuration.getRole());
+
+      SnowflakeIO.Read<Row> read =
+          SnowflakeIO.<Row>read()
+              .withDataSourceConfiguration(dataSourceConfiguration)
+              .withStagingBucketName(configuration.getStagingBucketName())
+              
.withStorageIntegrationName(configuration.getStorageIntegrationName())
+              .withCsvMapper(parts -> toRow(parts, outputSchema))
+              .withCoder(RowCoder.of(outputSchema));
+
+      if (configuration.getTable() != null) {
+        read = read.fromTable(configuration.getTable());
+      } else {
+        read = read.fromQuery(configuration.getQuery());
+      }
+
+      if (configuration.getQuotationMark() != null) {
+        read = read.withQuotationMark(configuration.getQuotationMark());
+      }
+
+      PCollection<Row> rows =
+          input.getPipeline().apply("ReadFromSnowflake", 
read).setRowSchema(outputSchema);
+
+      return PCollectionRowTuple.of(OUTPUT_TAG, rows);
+    }
+  }
+
+  @AutoValue
+  @DefaultSchema(AutoValueSchema.class)
+  public abstract static class Configuration implements Serializable {
+
+    @SchemaFieldDescription("Snowflake server name.")
+    public abstract String getServerName();
+
+    @SchemaFieldDescription(
+        "Snowflake username. Required for password and private key 
authentication.")
+    @Nullable
+    public abstract String getUsername();
+
+    @SchemaFieldDescription(
+        "Snowflake password. Mutually exclusive with OAuth token and private 
key.")
+    @Nullable
+    public abstract String getPassword();
+
+    @SchemaFieldDescription(
+        "Snowflake OAuth token. Mutually exclusive with password and private 
key.")
+    @Nullable
+    public abstract String getOauthToken();
+
+    @SchemaFieldDescription(
+        "Raw Snowflake private key. Mutually exclusive with password and OAuth 
token.")
+    @Nullable
+    public abstract String getPrivateKey();
+
+    @SchemaFieldDescription("Passphrase for the Snowflake private key.")
+    @Nullable
+    public abstract String getPrivateKeyPassphrase();
+
+    @SchemaFieldDescription("Snowflake database name.")
+    public abstract String getDatabase();
+
+    @SchemaFieldDescription("Snowflake schema name.")
+    public abstract String getSnowflakeSchema();
+
+    @SchemaFieldDescription("Snowflake warehouse name.")
+    @Nullable
+    public abstract String getWarehouse();
+
+    @SchemaFieldDescription("Snowflake role.")
+    @Nullable
+    public abstract String getRole();
+
+    @SchemaFieldDescription("Snowflake table to read from.")
+    @Nullable
+    public abstract String getTable();
+
+    @SchemaFieldDescription("Snowflake query to read from.")
+    @Nullable
+    public abstract String getQuery();
+
+    @SchemaFieldDescription("GCS path used to stage CSV files. The path must 
end with '/'.")
+    public abstract String getStagingBucketName();
+
+    @SchemaFieldDescription("Snowflake storage integration name.")
+    public abstract String getStorageIntegrationName();
+
+    @SchemaFieldDescription("Output schema encoded using JSON Schema syntax.")
+    public abstract String getSchema();
+
+    @SchemaFieldDescription("Quotation mark used when parsing staged CSV 
files.")
+    @Nullable
+    public abstract String getQuotationMark();
+
+    public static Builder builder() {
+      return new 
AutoValue_SnowflakeReadSchemaTransformProvider_Configuration.Builder();
+    }
+
+    public abstract Builder toBuilder();
+
+    void validate() {
+      requireNonEmpty(getServerName(), "serverName");
+      requireNonEmpty(getDatabase(), "database");
+      requireNonEmpty(getSnowflakeSchema(), "snowflakeSchema");
+      requireNonEmpty(getStagingBucketName(), "stagingBucketName");
+      requireNonEmpty(getStorageIntegrationName(), "storageIntegrationName");
+      requireNonEmpty(getSchema(), "schema");
+
+      SnowflakeSchemaTransformUtils.validateAuthentication(
+          getUsername(),
+          getPassword(),
+          getOauthToken(),
+          getPrivateKey(),
+          getPrivateKeyPassphrase());
+
+      boolean tablePresent = getTable() != null && !getTable().isEmpty();
+      boolean queryPresent = getQuery() != null && !getQuery().isEmpty();
+
+      if (!tablePresent && !queryPresent) {
+        throw new IllegalArgumentException("Either table or query must be 
specified.");
+      }
+
+      if (tablePresent && queryPresent) {
+        throw new IllegalArgumentException("table and query are mutually 
exclusive.");
+      }
+
+      if (!getStagingBucketName().endsWith("/")) {
+        throw new IllegalArgumentException("stagingBucketName must end with 
'/'");
+      }
+
+      // Validate JSON schema early.
+      JsonUtils.beamSchemaFromJsonSchema(getSchema());
+    }
+
+    private static void requireNonEmpty(String value, String name) {
+      if (value == null || value.isEmpty()) {
+        throw new IllegalArgumentException(name + " cannot be empty");
+      }
+    }
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+      public abstract Builder setServerName(String value);
+
+      public abstract Builder setUsername(@Nullable String value);
+
+      public abstract Builder setPassword(@Nullable String value);
+
+      public abstract Builder setOauthToken(@Nullable String value);
+
+      public abstract Builder setPrivateKey(@Nullable String value);
+
+      public abstract Builder setPrivateKeyPassphrase(@Nullable String value);
+
+      public abstract Builder setDatabase(String value);
+
+      public abstract Builder setSnowflakeSchema(String value);
+
+      public abstract Builder setWarehouse(@Nullable String value);
+
+      public abstract Builder setRole(@Nullable String value);
+
+      public abstract Builder setTable(@Nullable String value);
+
+      public abstract Builder setQuery(@Nullable String value);
+
+      public abstract Builder setStagingBucketName(String value);
+
+      public abstract Builder setStorageIntegrationName(String value);
+
+      public abstract Builder setSchema(String value);
+
+      public abstract Builder setQuotationMark(@Nullable String value);
+
+      public abstract Configuration build();
+    }
+  }
+}
diff --git 
a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeSchemaTransformUtils.java
 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeSchemaTransformUtils.java
new file mode 100644
index 00000000000..84d451552c3
--- /dev/null
+++ 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeSchemaTransformUtils.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.beam.sdk.io.snowflake;
+
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nullable;
+import org.apache.beam.sdk.io.snowflake.data.SnowflakeColumn;
+import org.apache.beam.sdk.io.snowflake.data.SnowflakeDataType;
+import org.apache.beam.sdk.io.snowflake.data.SnowflakeTableSchema;
+import org.apache.beam.sdk.io.snowflake.data.datetime.SnowflakeTimestamp;
+import org.apache.beam.sdk.io.snowflake.data.logical.SnowflakeBoolean;
+import org.apache.beam.sdk.io.snowflake.data.numeric.SnowflakeDouble;
+import org.apache.beam.sdk.io.snowflake.data.numeric.SnowflakeNumber;
+import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeBinary;
+import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeVarchar;
+import org.apache.beam.sdk.io.snowflake.enums.CreateDisposition;
+import org.apache.beam.sdk.io.snowflake.enums.StreamingLogLevel;
+import org.apache.beam.sdk.io.snowflake.enums.WriteDisposition;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.values.Row;
+import org.joda.time.Instant;
+
+/** Utilities shared by Snowflake schema transform providers. */
+@SuppressWarnings({
+  "nullness" // TODO(https://github.com/apache/beam/issues/20497)
+})
+public class SnowflakeSchemaTransformUtils {
+
+  public static SnowflakeIO.DataSourceConfiguration 
createDataSourceConfiguration(
+      String serverName,
+      @Nullable String username,
+      @Nullable String password,
+      @Nullable String oauthToken,
+      @Nullable String privateKey,
+      @Nullable String privateKeyPassphrase,
+      String database,
+      String snowflakeSchema,
+      @Nullable String warehouse,
+      @Nullable String role) {
+
+    SnowflakeIO.DataSourceConfiguration configuration =
+        SnowflakeIO.DataSourceConfiguration.create();
+
+    if (isNotEmpty(password)) {
+      configuration = configuration.withUsernamePasswordAuth(username, 
password);
+    } else if (isNotEmpty(oauthToken)) {
+      configuration = configuration.withOAuth(oauthToken);
+    } else if (isNotEmpty(privateKey)) {
+      if (isNotEmpty(privateKeyPassphrase)) {
+        configuration =
+            configuration.withKeyPairRawAuth(username, privateKey, 
privateKeyPassphrase);
+      } else {
+        configuration = configuration.withKeyPairRawAuth(username, privateKey);
+      }
+    }
+
+    configuration =
+        
configuration.withServerName(serverName).withDatabase(database).withSchema(snowflakeSchema);
+
+    if (isNotEmpty(warehouse)) {
+      configuration = configuration.withWarehouse(warehouse);
+    }
+
+    if (isNotEmpty(role)) {
+      configuration = configuration.withRole(role);
+    }
+
+    return configuration;
+  }
+
+  public static void validateAuthentication(
+      @Nullable String username,
+      @Nullable String password,
+      @Nullable String oauthToken,
+      @Nullable String privateKey,
+      @Nullable String privateKeyPassphrase) {
+
+    int authenticationMethods = 0;
+
+    if (isNotEmpty(password)) {
+      authenticationMethods++;
+    }
+
+    if (isNotEmpty(oauthToken)) {
+      authenticationMethods++;
+    }
+
+    if (isNotEmpty(privateKey)) {
+      authenticationMethods++;
+    }
+
+    if (authenticationMethods != 1) {
+      throw new IllegalArgumentException(
+          "Exactly one authentication method must be configured: "
+              + "password, oauthToken, or privateKey.");
+    }
+
+    if ((isNotEmpty(password) || isNotEmpty(privateKey)) && 
!isNotEmpty(username)) {
+      throw new IllegalArgumentException(
+          "username is required for password and private key authentication.");
+    }
+
+    if (isNotEmpty(privateKeyPassphrase) && !isNotEmpty(privateKey)) {
+      throw new IllegalArgumentException("privateKeyPassphrase requires 
privateKey.");
+    }
+  }
+
+  public static boolean isNotEmpty(@Nullable String value) {
+    return value != null && !value.isEmpty();
+  }
+
+  public static StreamingLogLevel parseStreamingLogLevel(String value) {
+    try {
+      return StreamingLogLevel.valueOf(value);
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException(
+          "Unsupported debugMode '" + value + "'. Supported values are ERROR 
and INFO.", e);
+    }
+  }
+
+  public static CreateDisposition parseCreateDisposition(String value) {
+    try {
+      return CreateDisposition.valueOf(value);
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException(
+          "Unsupported createDisposition '"
+              + value
+              + "'. Supported values are CREATE_IF_NEEDED and CREATE_NEVER.",
+          e);
+    }
+  }
+
+  public static WriteDisposition parseWriteDisposition(String value) {
+    try {
+      return WriteDisposition.valueOf(value);
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException(
+          "Unsupported writeDisposition '"
+              + value
+              + "'. Supported values are APPEND, TRUNCATE, and EMPTY.",
+          e);
+    }
+  }
+
+  public static SnowflakeTableSchema toSnowflakeTableSchema(Schema schema) {
+    SnowflakeColumn[] columns =
+        schema.getFields().stream()
+            .map(SnowflakeSchemaTransformUtils::toSnowflakeColumn)
+            .toArray(SnowflakeColumn[]::new);
+
+    return SnowflakeTableSchema.of(columns);
+  }
+
+  public static SnowflakeColumn toSnowflakeColumn(Schema.Field field) {
+    SnowflakeDataType snowflakeType = toSnowflakeDataType(field);
+
+    return SnowflakeColumn.of(field.getName(), snowflakeType, 
field.getType().getNullable());
+  }
+
+  public static Row toRow(String[] parts, Schema schema) {
+    if (parts.length != schema.getFieldCount()) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Snowflake row contains %d values, but the configured schema 
contains %d fields.",
+              parts.length, schema.getFieldCount()));
+    }
+
+    Row.Builder builder = Row.withSchema(schema);
+
+    for (int i = 0; i < schema.getFieldCount(); i++) {
+      Schema.Field field = schema.getField(i);
+      builder.addValue(toBeamValue(parts[i], field));
+    }
+
+    return builder.build();
+  }
+
+  public static Object toBeamValue(String value, Schema.Field field) {
+    if (value == null || value.isEmpty()) {
+      if (field.getType().getNullable()) {
+        return null;
+      }
+
+      /*
+       * Snowflake COPY encodes NULL as an empty CSV value. Therefore an empty
+       * value cannot be represented for a required non-string type.
+       *
+       * For STRING, preserve the empty string.
+       */
+      if (field.getType().getTypeName() == Schema.TypeName.STRING) {
+        return "";
+      }
+
+      throw new IllegalArgumentException(
+          String.format(
+              "Received an empty value for non-nullable Snowflake field 
'%s'.", field.getName()));
+    }
+
+    try {
+      switch (field.getType().getTypeName()) {
+        case BYTE:
+          return Byte.valueOf(value);
+
+        case INT16:
+          return Short.valueOf(value);
+
+        case INT32:
+          return Integer.valueOf(value);
+
+        case INT64:
+          return Long.valueOf(value);
+
+        case FLOAT:
+          return Float.valueOf(value);
+
+        case DOUBLE:
+          return Double.valueOf(value);
+
+        case STRING:
+          return value;
+
+        case BOOLEAN:
+          if ("true".equalsIgnoreCase(value)) {
+            return true;
+          }
+
+          if ("false".equalsIgnoreCase(value)) {
+            return false;
+          }
+
+          throw new IllegalArgumentException(String.format("Invalid boolean 
value '%s'.", value));
+
+        case BYTES:
+          return value.getBytes(StandardCharsets.UTF_8);
+
+        case DATETIME:
+          return Instant.parse(value);
+
+        case DECIMAL:
+        case ARRAY:
+        case ITERABLE:
+        case MAP:
+        case ROW:
+        case LOGICAL_TYPE:
+        default:
+          throw unsupportedFieldType(field, null);
+      }
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Unable to parse value '%s' as %s for Snowflake field '%s'.",
+              value, field.getType().getTypeName(), field.getName()),
+          e);
+    }
+  }
+
+  public static SnowflakeDataType toSnowflakeDataType(Schema.Field field) {
+    switch (field.getType().getTypeName()) {
+      case BYTE:
+      case INT16:
+      case INT32:
+      case INT64:
+        return SnowflakeNumber.of();
+
+      case FLOAT:
+      case DOUBLE:
+        return SnowflakeDouble.of();
+
+      case STRING:
+        return SnowflakeVarchar.of();
+
+      case BOOLEAN:
+        return SnowflakeBoolean.of();
+
+      case BYTES:
+        return SnowflakeBinary.of();
+
+      case DATETIME:
+        return SnowflakeTimestamp.of();
+
+      case DECIMAL:
+        throw unsupportedFieldType(
+            field, "Beam DECIMAL does not include Snowflake precision and 
scale information.");
+
+      case ARRAY:
+      case ITERABLE:
+      case MAP:
+      case ROW:
+      case LOGICAL_TYPE:
+      default:
+        throw unsupportedFieldType(field, null);
+    }
+  }
+
+  public static IllegalArgumentException unsupportedFieldType(
+      Schema.Field field, @Nullable String details) {
+    String message =
+        String.format(
+            "Unsupported Beam field type %s for Snowflake column '%s'.",
+            field.getType().getTypeName(), field.getName());
+
+    if (details != null) {
+      message += " " + details;
+    }
+
+    return new IllegalArgumentException(message);
+  }
+}
diff --git 
a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteConfiguration.java
 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteConfiguration.java
new file mode 100644
index 00000000000..43fe52b1245
--- /dev/null
+++ 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteConfiguration.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.beam.sdk.io.snowflake;
+
+import static org.apache.beam.sdk.io.snowflake.SnowflakeSchemaTransformUtils.*;
+
+import com.google.auto.value.AutoValue;
+import java.io.Serializable;
+import javax.annotation.Nullable;
+import org.apache.beam.sdk.schemas.AutoValueSchema;
+import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
+import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
+
+@AutoValue
+@DefaultSchema(AutoValueSchema.class)
+public abstract class SnowflakeWriteConfiguration implements Serializable {
+
+  @SchemaFieldDescription("Snowflake server name.")
+  public abstract String getServerName();
+
+  @SchemaFieldDescription(
+      "Snowflake username. Required for password and private key 
authentication.")
+  @Nullable
+  public abstract String getUsername();
+
+  @SchemaFieldDescription(
+      "Snowflake password. Mutually exclusive with OAuth token and private 
key.")
+  @Nullable
+  public abstract String getPassword();
+
+  @SchemaFieldDescription(
+      "Snowflake OAuth token. Mutually exclusive with password and private 
key.")
+  @Nullable
+  public abstract String getOauthToken();
+
+  @SchemaFieldDescription(
+      "Raw Snowflake private key. Mutually exclusive with password and OAuth 
token.")
+  @Nullable
+  public abstract String getPrivateKey();
+
+  @SchemaFieldDescription("Passphrase for the Snowflake private key.")
+  @Nullable
+  public abstract String getPrivateKeyPassphrase();
+
+  @SchemaFieldDescription("Snowflake database name.")
+  public abstract String getDatabase();
+
+  @SchemaFieldDescription("Snowflake schema name.")
+  public abstract String getSchema();
+
+  @SchemaFieldDescription("Snowflake warehouse name.")
+  @Nullable
+  public abstract String getWarehouse();
+
+  @SchemaFieldDescription("Snowflake role.")
+  @Nullable
+  public abstract String getRole();
+
+  @SchemaFieldDescription("Destination Snowflake table. Required for batch 
writes.")
+  @Nullable
+  public abstract String getTable();
+
+  @SchemaFieldDescription("Snowflake Snowpipe name. Required for streaming 
writes.")
+  @Nullable
+  public abstract String getSnowPipe();
+
+  @SchemaFieldDescription("GCS path used to stage CSV files. The path must end 
with '/'.")
+  public abstract String getStagingBucketName();
+
+  @SchemaFieldDescription("Snowflake storage integration name.")
+  public abstract String getStorageIntegrationName();
+
+  @SchemaFieldDescription(
+      "Table creation behavior for batch writes. "
+          + "Supported values are CREATE_IF_NEEDED and CREATE_NEVER.")
+  @Nullable
+  public abstract String getCreateDisposition();
+
+  @SchemaFieldDescription(
+      "Write behavior for batch writes. " + "Supported values are APPEND, 
TRUNCATE, and EMPTY.")
+  @Nullable
+  public abstract String getWriteDisposition();
+
+  @SchemaFieldDescription("Quotation mark used when writing values to staged 
CSV files.")
+  @Nullable
+  public abstract String getQuotationMark();
+
+  @SchemaFieldDescription("Maximum number of rows to stage before flushing in 
streaming mode.")
+  @Nullable
+  public abstract Integer getFlushRowLimit();
+
+  @SchemaFieldDescription(
+      "Maximum time in milliseconds before flushing staged rows in streaming 
mode.")
+  @Nullable
+  public abstract Long getFlushTimeLimitMillis();
+
+  @SchemaFieldDescription("Number of output shards used when staging files.")
+  @Nullable
+  public abstract Integer getShardsNumber();
+
+  @SchemaFieldDescription("Streaming log level. Supported values are ERROR and 
INFO.")
+  @Nullable
+  public abstract String getDebugMode();
+
+  public static Builder builder() {
+    return new AutoValue_SnowflakeWriteConfiguration.Builder();
+  }
+
+  public abstract Builder toBuilder();
+
+  void validate() {
+    requireNonEmpty(getServerName(), "serverName");
+    requireNonEmpty(getDatabase(), "database");
+    requireNonEmpty(getSchema(), "schema");
+    requireNonEmpty(getStagingBucketName(), "stagingBucketName");
+    requireNonEmpty(getStorageIntegrationName(), "storageIntegrationName");
+
+    SnowflakeSchemaTransformUtils.validateAuthentication(
+        getUsername(), getPassword(), getOauthToken(), getPrivateKey(), 
getPrivateKeyPassphrase());
+
+    if (!getStagingBucketName().endsWith("/")) {
+      throw new IllegalArgumentException("stagingBucketName must end with 
'/'");
+    }
+
+    String createDisposition = getCreateDisposition();
+    if (createDisposition != null) {
+      parseCreateDisposition(createDisposition);
+    }
+
+    String writeDisposition = getWriteDisposition();
+    if (writeDisposition != null) {
+      parseWriteDisposition(writeDisposition);
+    }
+
+    String debugMode = getDebugMode();
+    if (debugMode != null) {
+      parseStreamingLogLevel(debugMode);
+    }
+
+    Integer flushRowLimit = getFlushRowLimit();
+    if (flushRowLimit != null && flushRowLimit <= 0) {
+      throw new IllegalArgumentException("flushRowLimit must be greater than 
0.");
+    }
+
+    Long flushTimeLimitMillis = getFlushTimeLimitMillis();
+    if (flushTimeLimitMillis != null && flushTimeLimitMillis <= 0) {
+      throw new IllegalArgumentException("flushTimeLimitMillis must be greater 
than 0.");
+    }
+
+    Integer shardsNumber = getShardsNumber();
+    if (shardsNumber != null && shardsNumber <= 0) {
+      throw new IllegalArgumentException("shardsNumber must be greater than 
0.");
+    }
+  }
+
+  private static void requireNonEmpty(String value, String name) {
+    if (value == null || value.isEmpty()) {
+      throw new IllegalArgumentException(name + " cannot be empty");
+    }
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setServerName(String value);
+
+    public abstract Builder setUsername(@Nullable String value);
+
+    public abstract Builder setPassword(@Nullable String value);
+
+    public abstract Builder setOauthToken(@Nullable String value);
+
+    public abstract Builder setPrivateKey(@Nullable String value);
+
+    public abstract Builder setPrivateKeyPassphrase(@Nullable String value);
+
+    public abstract Builder setDatabase(String value);
+
+    public abstract Builder setSchema(String value);
+
+    public abstract Builder setWarehouse(@Nullable String value);
+
+    public abstract Builder setRole(@Nullable String value);
+
+    public abstract Builder setTable(@Nullable String value);
+
+    public abstract Builder setSnowPipe(@Nullable String value);
+
+    public abstract Builder setStagingBucketName(String value);
+
+    public abstract Builder setStorageIntegrationName(String value);
+
+    public abstract Builder setCreateDisposition(@Nullable String value);
+
+    public abstract Builder setWriteDisposition(@Nullable String value);
+
+    public abstract Builder setQuotationMark(@Nullable String value);
+
+    public abstract Builder setFlushRowLimit(@Nullable Integer value);
+
+    public abstract Builder setFlushTimeLimitMillis(@Nullable Long value);
+
+    public abstract Builder setShardsNumber(@Nullable Integer value);
+
+    public abstract Builder setDebugMode(@Nullable String value);
+
+    public abstract SnowflakeWriteConfiguration build();
+  }
+}
diff --git 
a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProvider.java
 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProvider.java
index 623bb134f58..3010d88b010 100644
--- 
a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProvider.java
+++ 
b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProvider.java
@@ -17,33 +17,20 @@
  */
 package org.apache.beam.sdk.io.snowflake;
 
+import static org.apache.beam.sdk.io.snowflake.SnowflakeSchemaTransformUtils.*;
+
 import com.google.auto.service.AutoService;
-import com.google.auto.value.AutoValue;
 import java.io.Serializable;
 import java.util.Collections;
 import java.util.List;
-import javax.annotation.Nullable;
-import org.apache.beam.sdk.io.snowflake.data.SnowflakeColumn;
-import org.apache.beam.sdk.io.snowflake.data.SnowflakeDataType;
-import org.apache.beam.sdk.io.snowflake.data.SnowflakeTableSchema;
-import org.apache.beam.sdk.io.snowflake.data.datetime.SnowflakeTimestamp;
-import org.apache.beam.sdk.io.snowflake.data.logical.SnowflakeBoolean;
-import org.apache.beam.sdk.io.snowflake.data.numeric.SnowflakeDouble;
-import org.apache.beam.sdk.io.snowflake.data.numeric.SnowflakeNumber;
-import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeBinary;
-import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeVarchar;
 import org.apache.beam.sdk.io.snowflake.enums.CreateDisposition;
-import org.apache.beam.sdk.io.snowflake.enums.WriteDisposition;
-import org.apache.beam.sdk.schemas.AutoValueSchema;
-import org.apache.beam.sdk.schemas.Schema;
-import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
-import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
 import org.apache.beam.sdk.schemas.transforms.SchemaTransform;
 import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider;
 import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider;
 import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.PCollectionRowTuple;
 import org.apache.beam.sdk.values.Row;
+import org.joda.time.Duration;
 
 /** A {@link SchemaTransformProvider} for writing Beam rows to Snowflake. */
 @SuppressWarnings({
@@ -51,7 +38,7 @@ import org.apache.beam.sdk.values.Row;
 })
 @AutoService(SchemaTransformProvider.class)
 public class SnowflakeWriteSchemaTransformProvider
-    extends 
TypedSchemaTransformProvider<SnowflakeWriteSchemaTransformProvider.Configuration>
 {
+    extends TypedSchemaTransformProvider<SnowflakeWriteConfiguration> {
 
   static final String INPUT_TAG = "input";
 
@@ -64,16 +51,16 @@ public class SnowflakeWriteSchemaTransformProvider
 
   @Override
   public String description() {
-    return "Writes Beam Rows to a Snowflake table using staged CSV files.";
+    return "Writes Beam Rows to Snowflake using batch COPY or streaming 
Snowpipe.";
   }
 
   @Override
-  protected Class<Configuration> configurationClass() {
-    return Configuration.class;
+  protected Class<SnowflakeWriteConfiguration> configurationClass() {
+    return SnowflakeWriteConfiguration.class;
   }
 
   @Override
-  protected SchemaTransform from(Configuration configuration) {
+  protected SchemaTransform from(SnowflakeWriteConfiguration configuration) {
     configuration.validate();
     return new SnowflakeWriteSchemaTransform(configuration);
   }
@@ -88,13 +75,12 @@ public class SnowflakeWriteSchemaTransformProvider
     return Collections.emptyList();
   }
 
-  /** Schema transform that configures and applies {@link SnowflakeIO.Write}. 
*/
   private static class SnowflakeWriteSchemaTransform extends SchemaTransform
       implements Serializable {
 
-    private final Configuration configuration;
+    private final SnowflakeWriteConfiguration configuration;
 
-    private SnowflakeWriteSchemaTransform(Configuration configuration) {
+    private SnowflakeWriteSchemaTransform(SnowflakeWriteConfiguration 
configuration) {
       this.configuration = configuration;
     }
 
@@ -103,260 +89,91 @@ public class SnowflakeWriteSchemaTransformProvider
       PCollection<Row> rows = input.get(INPUT_TAG);
 
       SnowflakeIO.DataSourceConfiguration dataSourceConfiguration =
-          SnowflakeIO.DataSourceConfiguration.create()
-              .withUsernamePasswordAuth(configuration.getUsername(), 
configuration.getPassword())
-              .withServerName(configuration.getServerName())
-              .withDatabase(configuration.getDatabase())
-              .withSchema(configuration.getSchema());
-
-      if (configuration.getWarehouse() != null) {
-        dataSourceConfiguration =
-            
dataSourceConfiguration.withWarehouse(configuration.getWarehouse());
-      }
-
-      if (configuration.getRole() != null) {
-        dataSourceConfiguration = 
dataSourceConfiguration.withRole(configuration.getRole());
-      }
+          SnowflakeSchemaTransformUtils.createDataSourceConfiguration(
+              configuration.getServerName(),
+              configuration.getUsername(),
+              configuration.getPassword(),
+              configuration.getOauthToken(),
+              configuration.getPrivateKey(),
+              configuration.getPrivateKeyPassphrase(),
+              configuration.getDatabase(),
+              configuration.getSchema(),
+              configuration.getWarehouse(),
+              configuration.getRole());
 
       SnowflakeIO.Write<Row> write =
           SnowflakeIO.<Row>write()
               .withDataSourceConfiguration(dataSourceConfiguration)
               .withStagingBucketName(configuration.getStagingBucketName())
               
.withStorageIntegrationName(configuration.getStorageIntegrationName())
-              .withUserDataMapper(row -> row.getValues().toArray())
-              .to(configuration.getTable());
+              .withUserDataMapper(row -> row.getValues().toArray());
 
-      if (configuration.getCreateDisposition() != null) {
-        CreateDisposition createDisposition =
-            parseCreateDisposition(configuration.getCreateDisposition());
+      boolean streaming = rows.isBounded() == PCollection.IsBounded.UNBOUNDED;
 
-        write = write.withCreateDisposition(createDisposition);
+      if (streaming) {
+        String snowPipe = configuration.getSnowPipe();
 
-        if (createDisposition == CreateDisposition.CREATE_IF_NEEDED) {
-          write = 
write.withTableSchema(toSnowflakeTableSchema(rows.getSchema()));
+        if (snowPipe == null || snowPipe.isEmpty()) {
+          throw new IllegalArgumentException("snowPipe is required for 
streaming writes.");
         }
-      }
-
-      if (configuration.getWriteDisposition() != null) {
-        write =
-            
write.withWriteDisposition(parseWriteDisposition(configuration.getWriteDisposition()));
-      }
-
-      if (configuration.getQuotationMark() != null) {
-        write = write.withQuotationMark(configuration.getQuotationMark());
-      }
-
-      rows.apply("WriteToSnowflake", write);
-
-      return PCollectionRowTuple.empty(input.getPipeline());
-    }
-  }
-
-  @AutoValue
-  @DefaultSchema(AutoValueSchema.class)
-  public abstract static class Configuration implements Serializable {
-
-    @SchemaFieldDescription("Snowflake server name.")
-    public abstract String getServerName();
-
-    @SchemaFieldDescription("Snowflake username.")
-    public abstract String getUsername();
 
-    @SchemaFieldDescription("Snowflake password.")
-    public abstract String getPassword();
+        write = write.withSnowPipe(snowPipe);
 
-    @SchemaFieldDescription("Snowflake database name.")
-    public abstract String getDatabase();
-
-    @SchemaFieldDescription("Snowflake schema name.")
-    public abstract String getSchema();
-
-    @SchemaFieldDescription("Snowflake warehouse name.")
-    @Nullable
-    public abstract String getWarehouse();
-
-    @SchemaFieldDescription("Snowflake role.")
-    @Nullable
-    public abstract String getRole();
+        Integer flushRowLimit = configuration.getFlushRowLimit();
+        if (flushRowLimit != null) {
+          write = write.withFlushRowLimit(flushRowLimit);
+        }
 
-    @SchemaFieldDescription("Destination Snowflake table.")
-    public abstract String getTable();
+        Long flushTimeLimitMillis = configuration.getFlushTimeLimitMillis();
+        if (flushTimeLimitMillis != null) {
+          write = 
write.withFlushTimeLimit(Duration.millis(flushTimeLimitMillis));
+        }
 
-    @SchemaFieldDescription("GCS path used to stage CSV files. The path must 
end with '/'.")
-    public abstract String getStagingBucketName();
+        Integer shardsNumber = configuration.getShardsNumber();
+        if (shardsNumber != null) {
+          write = write.withShardsNumber(shardsNumber);
+        }
 
-    @SchemaFieldDescription("Snowflake storage integration name.")
-    public abstract String getStorageIntegrationName();
+        String debugMode = configuration.getDebugMode();
+        if (debugMode != null) {
+          write = write.withDebugMode(parseStreamingLogLevel(debugMode));
+        }
+      } else {
+        String table = configuration.getTable();
 
-    @SchemaFieldDescription(
-        "Table creation behavior. Supported values are CREATE_IF_NEEDED and 
CREATE_NEVER.")
-    @Nullable
-    public abstract String getCreateDisposition();
+        if (table == null || table.isEmpty()) {
+          throw new IllegalArgumentException("table is required for batch 
writes.");
+        }
 
-    @SchemaFieldDescription("Write behavior. Supported values are APPEND, 
TRUNCATE, and EMPTY.")
-    @Nullable
-    public abstract String getWriteDisposition();
+        write = write.to(table);
 
-    @SchemaFieldDescription("Quotation mark used when writing values to staged 
CSV files.")
-    @Nullable
-    public abstract String getQuotationMark();
+        String createDispositionValue = configuration.getCreateDisposition();
 
-    public static Builder builder() {
-      return new 
AutoValue_SnowflakeWriteSchemaTransformProvider_Configuration.Builder();
-    }
+        if (createDispositionValue != null) {
+          CreateDisposition createDisposition = 
parseCreateDisposition(createDispositionValue);
 
-    public abstract Builder toBuilder();
+          write = write.withCreateDisposition(createDisposition);
 
-    void validate() {
-      requireNonEmpty(getServerName(), "serverName");
-      requireNonEmpty(getUsername(), "username");
-      requireNonEmpty(getPassword(), "password");
-      requireNonEmpty(getDatabase(), "database");
-      requireNonEmpty(getSchema(), "schema");
-      requireNonEmpty(getTable(), "table");
-      requireNonEmpty(getStagingBucketName(), "stagingBucketName");
-      requireNonEmpty(getStorageIntegrationName(), "storageIntegrationName");
-
-      if (!getStagingBucketName().endsWith("/")) {
-        throw new IllegalArgumentException("stagingBucketName must end with 
'/'");
-      }
+          if (createDisposition == CreateDisposition.CREATE_IF_NEEDED) {
+            write = 
write.withTableSchema(toSnowflakeTableSchema(rows.getSchema()));
+          }
+        }
 
-      if (getCreateDisposition() != null) {
-        parseCreateDisposition(getCreateDisposition());
-      }
+        String writeDispositionValue = configuration.getWriteDisposition();
 
-      if (getWriteDisposition() != null) {
-        parseWriteDisposition(getWriteDisposition());
+        if (writeDispositionValue != null) {
+          write = 
write.withWriteDisposition(parseWriteDisposition(writeDispositionValue));
+        }
       }
-    }
 
-    private static void requireNonEmpty(String value, String name) {
-      if (value == null || value.isEmpty()) {
-        throw new IllegalArgumentException(name + " cannot be empty");
+      String quotationMark = configuration.getQuotationMark();
+      if (quotationMark != null) {
+        write = write.withQuotationMark(quotationMark);
       }
-    }
-
-    @AutoValue.Builder
-    public abstract static class Builder {
 
-      public abstract Builder setServerName(String value);
-
-      public abstract Builder setUsername(String value);
-
-      public abstract Builder setPassword(String value);
-
-      public abstract Builder setDatabase(String value);
-
-      public abstract Builder setSchema(String value);
-
-      public abstract Builder setWarehouse(String value);
-
-      public abstract Builder setRole(String value);
-
-      public abstract Builder setTable(String value);
-
-      public abstract Builder setStagingBucketName(String value);
-
-      public abstract Builder setStorageIntegrationName(String value);
-
-      public abstract Builder setCreateDisposition(String value);
-
-      public abstract Builder setWriteDisposition(String value);
-
-      public abstract Builder setQuotationMark(String value);
-
-      public abstract Configuration build();
-    }
-  }
-
-  private static CreateDisposition parseCreateDisposition(String value) {
-    try {
-      return CreateDisposition.valueOf(value);
-    } catch (IllegalArgumentException e) {
-      throw new IllegalArgumentException(
-          "Unsupported createDisposition '"
-              + value
-              + "'. Supported values are CREATE_IF_NEEDED and CREATE_NEVER.",
-          e);
-    }
-  }
-
-  private static WriteDisposition parseWriteDisposition(String value) {
-    try {
-      return WriteDisposition.valueOf(value);
-    } catch (IllegalArgumentException e) {
-      throw new IllegalArgumentException(
-          "Unsupported writeDisposition '"
-              + value
-              + "'. Supported values are APPEND, TRUNCATE, and EMPTY.",
-          e);
-    }
-  }
-
-  static SnowflakeTableSchema toSnowflakeTableSchema(Schema schema) {
-    SnowflakeColumn[] columns =
-        schema.getFields().stream()
-            .map(SnowflakeWriteSchemaTransformProvider::toSnowflakeColumn)
-            .toArray(SnowflakeColumn[]::new);
-
-    return SnowflakeTableSchema.of(columns);
-  }
-
-  private static SnowflakeColumn toSnowflakeColumn(Schema.Field field) {
-    SnowflakeDataType snowflakeType = toSnowflakeDataType(field);
-
-    return SnowflakeColumn.of(field.getName(), snowflakeType, 
field.getType().getNullable());
-  }
-
-  private static SnowflakeDataType toSnowflakeDataType(Schema.Field field) {
-    switch (field.getType().getTypeName()) {
-      case BYTE:
-      case INT16:
-      case INT32:
-      case INT64:
-        return SnowflakeNumber.of();
-
-      case FLOAT:
-      case DOUBLE:
-        return SnowflakeDouble.of();
-
-      case STRING:
-        return SnowflakeVarchar.of();
-
-      case BOOLEAN:
-        return SnowflakeBoolean.of();
-
-      case BYTES:
-        return SnowflakeBinary.of();
-
-      case DATETIME:
-        return SnowflakeTimestamp.of();
-
-      case DECIMAL:
-        throw unsupportedFieldType(
-            field, "Beam DECIMAL does not include Snowflake precision and 
scale information.");
-
-      case ARRAY:
-      case ITERABLE:
-      case MAP:
-      case ROW:
-      case LOGICAL_TYPE:
-      default:
-        throw unsupportedFieldType(field, null);
-    }
-  }
-
-  private static IllegalArgumentException unsupportedFieldType(
-      Schema.Field field, @Nullable String details) {
-    String message =
-        String.format(
-            "Unsupported Beam field type %s for Snowflake column '%s'.",
-            field.getType().getTypeName(), field.getName());
+      rows.apply("WriteToSnowflake", write);
 
-    if (details != null) {
-      message += " " + details;
+      return PCollectionRowTuple.empty(input.getPipeline());
     }
-
-    return new IllegalArgumentException(message);
   }
 }
diff --git 
a/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProviderTest.java
 
b/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProviderTest.java
new file mode 100644
index 00000000000..02f8e2e7b66
--- /dev/null
+++ 
b/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeReadSchemaTransformProviderTest.java
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.beam.sdk.io.snowflake;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import 
org.apache.beam.sdk.io.snowflake.SnowflakeReadSchemaTransformProvider.Configuration;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.values.Row;
+import org.joda.time.Instant;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SnowflakeReadSchemaTransformProviderTest {
+
+  private static final String SIMPLE_SCHEMA =
+      "{"
+          + "\"type\":\"object\","
+          + "\"properties\":{"
+          + "\"id\":{\"type\":\"integer\"},"
+          + "\"name\":{\"type\":\"string\"}"
+          + "},"
+          + "\"required\":[\"id\",\"name\"]"
+          + "}";
+
+  private final SnowflakeReadSchemaTransformProvider provider =
+      new SnowflakeReadSchemaTransformProvider();
+
+  @Test
+  public void testIdentifier() {
+    assertThat(
+        provider.identifier(), 
equalTo("beam:schematransform:org.apache.beam:snowflake_read:v1"));
+  }
+
+  @Test
+  public void testInputCollectionNames() {
+    assertThat(provider.inputCollectionNames(), empty());
+  }
+
+  @Test
+  public void testOutputCollectionNames() {
+    assertThat(provider.outputCollectionNames(), contains("output"));
+  }
+
+  @Test
+  public void testValidTableConfiguration() {
+    provider.from(validConfiguration().setTable("table").build());
+  }
+
+  @Test
+  public void testValidQueryConfiguration() {
+    provider.from(validConfiguration().setQuery("SELECT * FROM 
table").build());
+  }
+
+  @Test
+  public void testTableAndQueryAreMutuallyExclusive() {
+    Configuration configuration =
+        validConfiguration().setTable("table").setQuery("SELECT * FROM 
table").build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("table and query are mutually 
exclusive."));
+  }
+
+  @Test
+  public void testTableOrQueryIsRequired() {
+    Configuration configuration = validConfiguration().build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("Either table or query must be 
specified."));
+  }
+
+  @Test
+  public void testStagingBucketMustEndWithSlash() {
+    Configuration configuration =
+        
validConfiguration().setTable("table").setStagingBucketName("gs://bucket/staging").build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("stagingBucketName must end 
with '/'"));
+  }
+
+  @Test
+  public void testConvertsCsvValuesToBeamRow() {
+    Schema schema =
+        Schema.builder()
+            .addByteField("byte_value")
+            .addInt16Field("short_value")
+            .addInt32Field("int_value")
+            .addInt64Field("long_value")
+            .addFloatField("float_value")
+            .addDoubleField("double_value")
+            .addStringField("string_value")
+            .addBooleanField("boolean_value")
+            .addByteArrayField("bytes_value")
+            .addDateTimeField("datetime_value")
+            .build();
+
+    String[] values = {
+      "1", "2", "3", "4", "5.5", "6.5", "hello", "true", "abc", 
"2026-08-13T09:00:00.000Z"
+    };
+
+    Row row = SnowflakeSchemaTransformUtils.toRow(values, schema);
+
+    assertThat(row.getByte("byte_value"), equalTo((byte) 1));
+    assertThat(row.getInt16("short_value"), equalTo((short) 2));
+    assertThat(row.getInt32("int_value"), equalTo(3));
+    assertThat(row.getInt64("long_value"), equalTo(4L));
+    assertThat(row.getFloat("float_value"), equalTo(5.5F));
+    assertThat(row.getDouble("double_value"), equalTo(6.5D));
+    assertThat(row.getString("string_value"), equalTo("hello"));
+    assertThat(row.getBoolean("boolean_value"), equalTo(true));
+    assertArrayEquals("abc".getBytes(StandardCharsets.UTF_8), 
row.getBytes("bytes_value"));
+    assertThat(
+        row.getDateTime("datetime_value"), 
equalTo(Instant.parse("2026-08-13T09:00:00.000Z")));
+  }
+
+  @Test
+  public void testNullableEmptyValueBecomesNull() {
+    Schema schema = Schema.builder().addNullableField("value", 
Schema.FieldType.INT64).build();
+
+    Row row = SnowflakeSchemaTransformUtils.toRow(new String[] {""}, schema);
+
+    assertThat(row.getValue("value"), equalTo(null));
+  }
+
+  @Test
+  public void testEmptyRequiredStringIsPreserved() {
+    Schema schema = Schema.builder().addStringField("value").build();
+
+    Row row = SnowflakeSchemaTransformUtils.toRow(new String[] {""}, schema);
+
+    assertThat(row.getString("value"), equalTo(""));
+  }
+
+  @Test
+  public void testEmptyRequiredNonStringIsRejected() {
+    Schema schema = Schema.builder().addInt64Field("value").build();
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> SnowflakeSchemaTransformUtils.toRow(new String[] {""}, 
schema));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("Received an empty value for non-nullable Snowflake field 
'value'."));
+  }
+
+  @Test
+  public void testWrongNumberOfFieldsIsRejected() {
+    Schema schema = 
Schema.builder().addInt64Field("id").addStringField("name").build();
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> SnowflakeSchemaTransformUtils.toRow(new String[] {"1"}, 
schema));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("Snowflake row contains 1 values, but the configured schema 
contains 2 fields."));
+  }
+
+  @Test
+  public void testArrayIsRejected() {
+    Schema schema = Schema.builder().addArrayField("values", 
Schema.FieldType.STRING).build();
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> SnowflakeSchemaTransformUtils.toRow(new String[] {"value"}, 
schema));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("Unable to parse value 'value' as ARRAY for Snowflake field 
'values'."));
+  }
+
+  @Test
+  public void testOauthAuthenticationIsValid() {
+    provider.from(
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setOauthToken("token")
+            .setTable("table")
+            .build());
+  }
+
+  @Test
+  public void testPrivateKeyAuthenticationIsValid() {
+    provider.from(
+        validConfiguration()
+            .setPassword(null)
+            .setPrivateKey("private-key")
+            .setPrivateKeyPassphrase("passphrase")
+            .setTable("table")
+            .build());
+  }
+
+  @Test
+  public void testAuthenticationMethodIsRequired() {
+    Configuration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setOauthToken(null)
+            .setPrivateKey(null)
+            .setTable("table")
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo(
+            "Exactly one authentication method must be configured: "
+                + "password, oauthToken, or privateKey."));
+  }
+
+  @Test
+  public void testMultipleAuthenticationMethodsAreRejected() {
+    Configuration configuration =
+        validConfiguration().setOauthToken("token").setTable("table").build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo(
+            "Exactly one authentication method must be configured: "
+                + "password, oauthToken, or privateKey."));
+  }
+
+  @Test
+  public void testUsernameIsRequiredForPrivateKeyAuthentication() {
+    Configuration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setPrivateKey("private-key")
+            .setTable("table")
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("username is required for password and private key 
authentication."));
+  }
+
+  @Test
+  public void testPrivateKeyPassphraseRequiresPrivateKey() {
+    Configuration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setOauthToken("token")
+            .setPrivateKeyPassphrase("passphrase")
+            .setTable("table")
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("privateKeyPassphrase requires 
privateKey."));
+  }
+
+  private static Configuration.Builder validConfiguration() {
+    return Configuration.builder()
+        .setServerName("account.snowflakecomputing.com")
+        .setUsername("username")
+        .setPassword("password")
+        .setDatabase("database")
+        .setSnowflakeSchema("public")
+        .setWarehouse("warehouse")
+        .setRole("role")
+        .setStagingBucketName("gs://bucket/staging/")
+        .setStorageIntegrationName("storage_integration")
+        .setSchema(SIMPLE_SCHEMA);
+  }
+}
diff --git 
a/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProviderTest.java
 
b/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProviderTest.java
index 336d7f01f03..bdd5aae0e75 100644
--- 
a/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProviderTest.java
+++ 
b/sdks/java/io/snowflake/src/test/java/org/apache/beam/sdk/io/snowflake/SnowflakeWriteSchemaTransformProviderTest.java
@@ -24,7 +24,8 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.Assert.assertThrows;
 
-import 
org.apache.beam.sdk.io.snowflake.SnowflakeWriteSchemaTransformProvider.Configuration;
+import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.coders.RowCoder;
 import org.apache.beam.sdk.io.snowflake.data.SnowflakeColumn;
 import org.apache.beam.sdk.io.snowflake.data.SnowflakeTableSchema;
 import org.apache.beam.sdk.io.snowflake.data.datetime.SnowflakeTimestamp;
@@ -34,6 +35,12 @@ import 
org.apache.beam.sdk.io.snowflake.data.numeric.SnowflakeNumber;
 import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeBinary;
 import org.apache.beam.sdk.io.snowflake.data.text.SnowflakeVarchar;
 import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.transforms.SchemaTransform;
+import org.apache.beam.sdk.testing.TestStream;
+import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionRowTuple;
+import org.apache.beam.sdk.values.Row;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -41,6 +48,8 @@ import org.junit.runners.JUnit4;
 @RunWith(JUnit4.class)
 public class SnowflakeWriteSchemaTransformProviderTest {
 
+  private final transient Pipeline pipeline = Pipeline.create();
+
   private final SnowflakeWriteSchemaTransformProvider provider =
       new SnowflakeWriteSchemaTransformProvider();
 
@@ -62,31 +71,17 @@ public class SnowflakeWriteSchemaTransformProviderTest {
 
   @Test
   public void testValidConfiguration() {
-    Configuration configuration = validConfiguration().build();
-
-    provider.from(configuration);
+    provider.from(validConfiguration().build());
   }
 
   @Test
   public void testBlankQuotationMarkIsAllowed() {
-    Configuration configuration = 
validConfiguration().setQuotationMark("").build();
-
-    provider.from(configuration);
+    provider.from(validConfiguration().setQuotationMark("").build());
   }
 
   @Test
-  public void testMissingServerName() {
-    Configuration configuration =
-        Configuration.builder()
-            .setUsername("username")
-            .setPassword("password")
-            .setDatabase("database")
-            .setSchema("schema")
-            .setTable("table")
-            .setServerName("")
-            .setStagingBucketName("gs://bucket/staging/")
-            .setStorageIntegrationName("storage_integration")
-            .build();
+  public void testMissingServerNameIsRejected() {
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setServerName("").build();
 
     IllegalArgumentException exception =
         assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
@@ -95,28 +90,14 @@ public class SnowflakeWriteSchemaTransformProviderTest {
   }
 
   @Test
-  public void testMissingTable() {
-    Configuration configuration =
-        Configuration.builder()
-            .setServerName("account.snowflakecomputing.com")
-            .setUsername("username")
-            .setPassword("password")
-            .setDatabase("database")
-            .setSchema("schema")
-            .setTable("")
-            .setStagingBucketName("gs://bucket/staging/")
-            .setStorageIntegrationName("storage_integration")
-            .build();
-
-    IllegalArgumentException exception =
-        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
-
-    assertThat(exception.getMessage(), equalTo("table cannot be empty"));
+  public void testMissingTableIsAllowedAtConfigurationTime() {
+    // Whether table is required depends on whether the input is bounded.
+    provider.from(validConfiguration().setTable(null).build());
   }
 
   @Test
   public void testStagingBucketMustEndWithSlash() {
-    Configuration configuration =
+    SnowflakeWriteConfiguration configuration =
         
validConfiguration().setStagingBucketName("gs://bucket/staging").build();
 
     IllegalArgumentException exception =
@@ -126,8 +107,9 @@ public class SnowflakeWriteSchemaTransformProviderTest {
   }
 
   @Test
-  public void testInvalidCreateDisposition() {
-    Configuration configuration = 
validConfiguration().setCreateDisposition("INVALID").build();
+  public void testInvalidCreateDispositionIsRejected() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration().setCreateDisposition("INVALID").build();
 
     IllegalArgumentException exception =
         assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
@@ -135,13 +117,24 @@ public class SnowflakeWriteSchemaTransformProviderTest {
     assertThat(
         exception.getMessage(),
         equalTo(
-            "Unsupported createDisposition 'INVALID'. Supported values are "
-                + "CREATE_IF_NEEDED and CREATE_NEVER."));
+            "Unsupported createDisposition 'INVALID'. "
+                + "Supported values are CREATE_IF_NEEDED and CREATE_NEVER."));
   }
 
   @Test
-  public void testInvalidWriteDisposition() {
-    Configuration configuration = 
validConfiguration().setWriteDisposition("INVALID").build();
+  public void testCreateIfNeededIsAccepted() {
+    
provider.from(validConfiguration().setCreateDisposition("CREATE_IF_NEEDED").build());
+  }
+
+  @Test
+  public void testCreateNeverIsAccepted() {
+    
provider.from(validConfiguration().setCreateDisposition("CREATE_NEVER").build());
+  }
+
+  @Test
+  public void testInvalidWriteDispositionIsRejected() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration().setWriteDisposition("INVALID").build();
 
     IllegalArgumentException exception =
         assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
@@ -149,46 +142,249 @@ public class SnowflakeWriteSchemaTransformProviderTest {
     assertThat(
         exception.getMessage(),
         equalTo(
-            "Unsupported writeDisposition 'INVALID'. Supported values are "
-                + "APPEND, TRUNCATE, and EMPTY."));
+            "Unsupported writeDisposition 'INVALID'. "
+                + "Supported values are APPEND, TRUNCATE, and EMPTY."));
   }
 
   @Test
   public void testSupportedWriteDispositions() {
     provider.from(validConfiguration().setWriteDisposition("APPEND").build());
+
     
provider.from(validConfiguration().setWriteDisposition("TRUNCATE").build());
+
     provider.from(validConfiguration().setWriteDisposition("EMPTY").build());
   }
 
   @Test
-  public void testCreateNeverIsSupported() {
-    Configuration configuration = 
validConfiguration().setCreateDisposition("CREATE_NEVER").build();
+  public void testOauthAuthenticationIsValid() {
+    provider.from(
+        
validConfiguration().setUsername(null).setPassword(null).setOauthToken("token").build());
+  }
 
-    provider.from(configuration);
+  @Test
+  public void testPrivateKeyAuthenticationIsValid() {
+    provider.from(
+        validConfiguration()
+            .setPassword(null)
+            .setPrivateKey("private-key")
+            .setPrivateKeyPassphrase("passphrase")
+            .build());
   }
 
-  private static Configuration.Builder validConfiguration() {
-    return Configuration.builder()
-        .setServerName("account.snowflakecomputing.com")
-        .setUsername("username")
-        .setPassword("password")
-        .setDatabase("database")
-        .setSchema("schema")
-        .setWarehouse("warehouse")
-        .setRole("role")
-        .setTable("table")
-        .setStagingBucketName("gs://bucket/staging/")
-        .setStorageIntegrationName("storage_integration");
+  @Test
+  public void testPrivateKeyWithoutPassphraseIsValid() {
+    
provider.from(validConfiguration().setPassword(null).setPrivateKey("private-key").build());
   }
 
   @Test
-  public void testConvertsBeamSchemaToSnowflakeSchema() {
+  public void testAuthenticationMethodIsRequired() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setOauthToken(null)
+            .setPrivateKey(null)
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo(
+            "Exactly one authentication method must be configured: "
+                + "password, oauthToken, or privateKey."));
+  }
+
+  @Test
+  public void testMultipleAuthenticationMethodsAreRejected() {
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setOauthToken("token").build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo(
+            "Exactly one authentication method must be configured: "
+                + "password, oauthToken, or privateKey."));
+  }
+
+  @Test
+  public void testUsernameIsRequiredForPasswordAuthentication() {
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setUsername(null).build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("username is required for password and private key 
authentication."));
+  }
+
+  @Test
+  public void testUsernameIsRequiredForPrivateKeyAuthentication() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setPrivateKey("private-key")
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("username is required for password and private key 
authentication."));
+  }
+
+  @Test
+  public void testPrivateKeyPassphraseRequiresPrivateKey() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration()
+            .setUsername(null)
+            .setPassword(null)
+            .setOauthToken("token")
+            .setPrivateKeyPassphrase("passphrase")
+            .build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("privateKeyPassphrase requires 
privateKey."));
+  }
+
+  @Test
+  public void testInvalidDebugModeIsRejected() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration().setDebugMode("INVALID").build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(
+        exception.getMessage(),
+        equalTo("Unsupported debugMode 'INVALID'. " + "Supported values are 
ERROR and INFO."));
+  }
+
+  @Test
+  public void testSupportedDebugModes() {
+    provider.from(validConfiguration().setDebugMode("ERROR").build());
+
+    provider.from(validConfiguration().setDebugMode("INFO").build());
+  }
+
+  @Test
+  public void testFlushRowLimitMustBePositive() {
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setFlushRowLimit(0).build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("flushRowLimit must be greater 
than 0."));
+  }
+
+  @Test
+  public void testFlushTimeLimitMustBePositive() {
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration().setFlushTimeLimitMillis(0L).build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("flushTimeLimitMillis must be 
greater than 0."));
+  }
+
+  @Test
+  public void testShardsNumberMustBePositive() {
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setShardsNumber(0).build();
+
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> 
provider.from(configuration));
+
+    assertThat(exception.getMessage(), equalTo("shardsNumber must be greater 
than 0."));
+  }
+
+  @Test
+  public void testBatchWriteRequiresTable() {
+    Schema schema = 
Schema.builder().addInt64Field("id").addStringField("name").build();
+
+    Row row = Row.withSchema(schema).addValues(1L, "Alice").build();
+
+    PCollection<Row> rows =
+        
pipeline.apply(Create.of(row).withCoder(RowCoder.of(schema))).setRowSchema(schema);
+
+    SnowflakeWriteConfiguration configuration = 
validConfiguration().setTable(null).build();
+
+    SchemaTransform transform = provider.from(configuration);
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> transform.expand(PCollectionRowTuple.of("input", rows)));
+
+    assertThat(exception.getMessage(), equalTo("table is required for batch 
writes."));
+  }
+
+  @Test
+  public void testStreamingWriteRequiresSnowPipe() {
+    Schema schema = 
Schema.builder().addInt64Field("id").addStringField("name").build();
+
+    Row row = Row.withSchema(schema).addValues(1L, "Alice").build();
+
+    TestStream<Row> stream =
+        
TestStream.create(RowCoder.of(schema)).addElements(row).advanceWatermarkToInfinity();
+
+    PCollection<Row> rows = pipeline.apply(stream).setRowSchema(schema);
+
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration().setTable(null).setSnowPipe(null).build();
+
+    SchemaTransform transform = provider.from(configuration);
+
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> transform.expand(PCollectionRowTuple.of("input", rows)));
+
+    assertThat(exception.getMessage(), equalTo("snowPipe is required for 
streaming writes."));
+  }
+
+  @Test
+  public void testStreamingConfigurationIsAccepted() {
+    Schema schema = 
Schema.builder().addInt64Field("id").addStringField("name").build();
+
+    Row row = Row.withSchema(schema).addValues(1L, "Alice").build();
+
+    TestStream<Row> stream =
+        
TestStream.create(RowCoder.of(schema)).addElements(row).advanceWatermarkToInfinity();
+
+    PCollection<Row> rows = pipeline.apply(stream).setRowSchema(schema);
+
+    SnowflakeWriteConfiguration configuration =
+        validConfiguration()
+            .setTable(null)
+            .setSnowPipe("MY_PIPE")
+            .setFlushRowLimit(50000)
+            .setFlushTimeLimitMillis(18000L)
+            .setShardsNumber(1)
+            .setDebugMode("ERROR")
+            .build();
+
+    SchemaTransform transform = provider.from(configuration);
+
+    transform.expand(PCollectionRowTuple.of("input", rows));
+  }
+
+  @Test
+  public void testScalarSchemaMapping() {
     Schema schema =
         Schema.builder()
             .addByteField("byte_value")
-            .addInt16Field("short_value")
-            .addInt32Field("int_value")
-            .addInt64Field("long_value")
+            .addInt16Field("int16_value")
+            .addInt32Field("int32_value")
+            .addInt64Field("int64_value")
             .addFloatField("float_value")
             .addDoubleField("double_value")
             .addStringField("string_value")
@@ -198,20 +394,16 @@ public class SnowflakeWriteSchemaTransformProviderTest {
             .build();
 
     SnowflakeTableSchema snowflakeSchema =
-        SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema);
+        SnowflakeSchemaTransformUtils.toSnowflakeTableSchema(schema);
 
     SnowflakeColumn[] columns = snowflakeSchema.getColumns();
 
-    assertThat(columns.length, equalTo(10));
-
     assertThat(columns[0].getDataType(), instanceOf(SnowflakeNumber.class));
     assertThat(columns[1].getDataType(), instanceOf(SnowflakeNumber.class));
     assertThat(columns[2].getDataType(), instanceOf(SnowflakeNumber.class));
     assertThat(columns[3].getDataType(), instanceOf(SnowflakeNumber.class));
-
     assertThat(columns[4].getDataType(), instanceOf(SnowflakeDouble.class));
     assertThat(columns[5].getDataType(), instanceOf(SnowflakeDouble.class));
-
     assertThat(columns[6].getDataType(), instanceOf(SnowflakeVarchar.class));
     assertThat(columns[7].getDataType(), instanceOf(SnowflakeBoolean.class));
     assertThat(columns[8].getDataType(), instanceOf(SnowflakeBinary.class));
@@ -219,31 +411,23 @@ public class SnowflakeWriteSchemaTransformProviderTest {
   }
 
   @Test
-  public void testPreservesColumnNamesAndNullability() {
+  public void testSchemaMappingPreservesNameAndNullability() {
     Schema schema =
         Schema.builder()
-            .addStringField("required_value")
-            .addNullableField("optional_value", Schema.FieldType.INT64)
+            .addInt64Field("id")
+            .addNullableField("name", Schema.FieldType.STRING)
             .build();
 
     SnowflakeTableSchema snowflakeSchema =
-        SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema);
+        SnowflakeSchemaTransformUtils.toSnowflakeTableSchema(schema);
 
     SnowflakeColumn[] columns = snowflakeSchema.getColumns();
 
-    assertThat(columns[0].getName(), equalTo("required_value"));
+    assertThat(columns[0].getName(), equalTo("id"));
     assertThat(columns[0].isNullable(), equalTo(false));
 
-    assertThat(columns[1].getName(), equalTo("optional_value"));
+    assertThat(columns[1].getName(), equalTo("name"));
     assertThat(columns[1].isNullable(), equalTo(true));
-  }
-
-  @Test
-  public void testSnowflakeSchemaSql() {
-    Schema schema = 
Schema.builder().addInt64Field("id").addNullableStringField("name").build();
-
-    SnowflakeTableSchema snowflakeSchema =
-        SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema);
 
     assertThat(snowflakeSchema.sql(), equalTo("id NUMBER(38,0), name VARCHAR 
NULL"));
   }
@@ -255,7 +439,7 @@ public class SnowflakeWriteSchemaTransformProviderTest {
     IllegalArgumentException exception =
         assertThrows(
             IllegalArgumentException.class,
-            () -> 
SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema));
+            () -> 
SnowflakeSchemaTransformUtils.toSnowflakeTableSchema(schema));
 
     assertThat(
         exception.getMessage(),
@@ -271,7 +455,7 @@ public class SnowflakeWriteSchemaTransformProviderTest {
     IllegalArgumentException exception =
         assertThrows(
             IllegalArgumentException.class,
-            () -> 
SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema));
+            () -> 
SnowflakeSchemaTransformUtils.toSnowflakeTableSchema(schema));
 
     assertThat(
         exception.getMessage(),
@@ -287,18 +471,24 @@ public class SnowflakeWriteSchemaTransformProviderTest {
     IllegalArgumentException exception =
         assertThrows(
             IllegalArgumentException.class,
-            () -> 
SnowflakeWriteSchemaTransformProvider.toSnowflakeTableSchema(schema));
+            () -> 
SnowflakeSchemaTransformUtils.toSnowflakeTableSchema(schema));
 
     assertThat(
         exception.getMessage(),
         equalTo("Unsupported Beam field type ROW for Snowflake column 
'nested'."));
   }
 
-  @Test
-  public void testCreateIfNeededIsSupported() {
-    Configuration configuration =
-        validConfiguration().setCreateDisposition("CREATE_IF_NEEDED").build();
-
-    provider.from(configuration);
+  private static SnowflakeWriteConfiguration.Builder validConfiguration() {
+    return SnowflakeWriteConfiguration.builder()
+        .setServerName("account.snowflakecomputing.com")
+        .setUsername("username")
+        .setPassword("password")
+        .setDatabase("database")
+        .setSchema("public")
+        .setWarehouse("warehouse")
+        .setRole("role")
+        .setTable("table")
+        .setStagingBucketName("gs://bucket/staging/")
+        .setStorageIntegrationName("storage_integration");
   }
 }
diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml 
b/sdks/python/apache_beam/yaml/standard_io.yaml
index 9fc25b49fc6..4b5cb82b22f 100644
--- a/sdks/python/apache_beam/yaml/standard_io.yaml
+++ b/sdks/python/apache_beam/yaml/standard_io.yaml
@@ -208,27 +208,56 @@
 # Snowflake
 - type: renaming
   transforms:
+    'ReadFromSnowflake': 'ReadFromSnowflake'
     'WriteToSnowflake': 'WriteToSnowflake'
   config:
     mappings:
+      'ReadFromSnowflake':
+        server_name: 'server_name'
+        username: 'username'
+        password: 'password'
+        oauth_token: 'oauth_token'
+        private_key: 'private_key'
+        private_key_passphrase: 'private_key_passphrase'
+        database: 'database'
+        snowflake_schema: 'snowflake_schema'
+        warehouse: 'warehouse'
+        role: 'role'
+        table: 'table'
+        query: 'query'
+        staging_bucket_name: 'staging_bucket_name'
+        storage_integration_name: 'storage_integration_name'
+        schema: 'schema'
+        quotation_mark: 'quotation_mark'
       'WriteToSnowflake':
         server_name: 'server_name'
         username: 'username'
         password: 'password'
+        oauth_token: 'oauth_token'
+        private_key: 'private_key'
+        private_key_passphrase: 'private_key_passphrase'
         database: 'database'
         schema: 'schema'
         warehouse: 'warehouse'
         role: 'role'
         table: 'table'
+        snow_pipe: 'snow_pipe'
         staging_bucket_name: 'staging_bucket_name'
         storage_integration_name: 'storage_integration_name'
         create_disposition: 'create_disposition'
         write_disposition: 'write_disposition'
         quotation_mark: 'quotation_mark'
+        flush_row_limit: 'flush_row_limit'
+        flush_time_limit_millis: 'flush_time_limit_millis'
+        shards_number: 'shards_number'
+        debug_mode: 'debug_mode'
     underlying_provider:
       type: beamJar
       transforms:
-        'WriteToSnowflake': 
'beam:schematransform:org.apache.beam:snowflake_write:v1'
+        'ReadFromSnowflake':
+          'beam:schematransform:org.apache.beam:snowflake_read:v1'
+        'WriteToSnowflake':
+          'beam:schematransform:org.apache.beam:snowflake_write:v1'
       config:
         gradle_target: 'sdks:java:io:snowflake:expansion-service:shadowJar'
 

Reply via email to