DML and bulk operations in SQLAlchemy¶
Snowflake SQLAlchemy provides custom constructs for Snowflake DML statements and for loading and unloading data. The examples on this page
assume an engine created as described in Connecting to Snowflake with SQLAlchemy.
Merge command support¶
Use the MergeInto construct to run a MERGE statement, which inserts, updates, and deletes rows in a single
statement. Add clauses with when_matched_then_update(), when_matched_then_delete(), and when_not_matched_then_insert(). Each clause
accepts an optional .where() condition, and the update and insert clauses take .values():
Merging from a VALUES list¶
The source can be any SQLAlchemy selectable. To merge from inline values without a staging table, wrap a values() construct in a
subquery:
Multi-table insert (INSERT ALL / INSERT FIRST)¶
Use the InsertMulti construct to run a multi-table INSERT statement. Build it from a source
select() and add targets.
For an unconditional INSERT ALL, add each target with .into(). Optionally, pass columns to name the target columns and values to map
source columns or expressions. When you omit values, the source columns are used in order:
For a conditional insert, add targets with .when(condition, table) and an optional .else_(table). Pass first=True to generate
INSERT FIRST, which inserts each row into the first matching target only:
Pass overwrite=True to generate INSERT OVERWRITE, which truncates the target tables before inserting.
Bulk insert optimization for ORM models¶
When you call Session.bulk_save_objects() with models that have nullable columns, SQLAlchemy groups objects by which columns are set. Objects
that leave different nullable columns unset end up in separate INSERT statements instead of a single batch.
To send one batched INSERT, use these two components together:
SnowflakeBase: A declarative base class that sets every nullable column without a default toNone, or to its scalar Python default, when you create an object. Subclass it directly. If your code must also run on SQLAlchemy 1.4, use thesnowflake_declarative_base()factory instead.SnowflakeSession: A session class whosebulk_save_objects()keeps thoseNonevalues, so all objects share the same set of columns.
The optimization applies only to bulk_save_objects(). Columns with a server_default, a callable default, or a SQL expression default
aren’t pre-populated, so objects that differ on those columns can still be split into separate batches.
Bulk array binding for large executemany inserts¶
For large batch inserts, such as connection.execute(insert(table), rows) or session.bulk_insert_mappings(), the Snowflake Connector for
Python can upload the bind values to a temporary stage and insert them from there. This bulk array binding is much faster than sending
the values inline.
The connector only uses array binding with the qmark (or numeric) parameter style. Snowflake SQLAlchemy defaults to pyformat, so set
paramstyle='qmark' on both the engine and the connector connection. If only one side is set, the placeholders and bind values don’t match
and the statement fails:
The optional CLIENT_STAGE_ARRAY_BINDING_THRESHOLD session parameter sets the total bind size, in bytes, above which the connector uses a
stage. A value of 0 disables array binding. Below the threshold, the connector sends the values inline. Either way, your code doesn’t
change. The qmark setting applies to every statement that the engine compiles.
Writing pandas DataFrames¶
To write a pandas DataFrame to Snowflake through a SQLAlchemy engine, pass the connector’s pd_writer function as the method argument to
DataFrame.to_sql(). pd_writer uploads the data to a stage and loads it with COPY INTO:
For more information, including write_pandas, see Using pandas DataFrames with the Python Connector.
CopyIntoStorage support¶
Use the CopyIntoStorage construct to unload a table or query result to a stage or cloud storage location with
COPY INTO <location>. It takes these arguments:
from_: A table orselect()construct.into: AnExternalStage(a named internal or external stage),AWSBucket,AzureContainer, orGCSBucket.formatter: Optional. ACSVFormatter,JSONFormatter, orPARQUETFormatter, configured with chained methods such as.null_if()or.field_delimiter().partition_by: Optional. An expression for thePARTITION BYclause.
This example unloads a table to a single CSV file on a named stage:
To unload directly to cloud storage, create the location with from_uri(). Use a storage integration
when you can, so that no credentials appear in the SQL:
The options available on a CopyIntoStorage object are .single(), .maxfilesize(), .files(), .pattern(), and .storage_integration().
For inline credentials and encryption, the location classes provide these methods:
AWSBucket:.credentials(aws_role=...)or.credentials(aws_key_id=..., aws_secret_key=..., aws_token=...),.encryption_aws_cse(),.encryption_aws_sse_s3(), and.encryption_aws_sse_kms().AzureContainer:.credentials(azure_sas_token)and.encryption_azure_cse().GCSBucket:.encryption_gcs_sse_kms()and.encryption_none(). Google Cloud Storage requires a storage integration.
Securing cloud credentials in logs¶
Inline credentials (from .credentials() or a client-side encryption master key) appear as literal values in the compiled SQL. If you
enable SQLAlchemy engine logging, for example with create_engine(..., echo=True), the full statement, including the secrets, is written to
the log. The connector’s secret masking doesn’t apply to SQLAlchemy’s logger.
To mask secrets in log output, pass the handler that writes the records to add_secret_redaction_filter(). Attach the filter to a handler,
not a logger, because logger filters don’t run on records propagated from child loggers:
To redact a statement string yourself, call redact_secrets(text). The repr() of the location classes (AWSBucket, AzureContainer,
GCSBucket) already masks secrets.
Creating a named file format¶
To reuse file format settings, create a named file format with CreateFileFormat and a formatter. It accepts these arguments:
format_name: The name of the file format, optionally qualified.formatter: ACSVFormatter,JSONFormatter, orPARQUETFormatter.replace_if_exists: GeneratesCREATE OR REPLACE.if_not_exists: GeneratesIF NOT EXISTS. You can’t combine it withreplace_if_exists. Setting both raises aValueError.comment: Optional comment.
To use the named file format with CopyIntoStorage, pass its name to the formatter:
In a SQL COPY INTO statement, reference the named file format with FORMAT_NAME:
To create the stage itself, see Stages and file formats.