> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.ampdb.io/api/mcp` to find what you need.

# Snowflake Loader

The Snowflake loader writes query results into tables in [Snowflake](https://www.snowflake.com), a cloud data
warehouse. Choose it to analyze Amp's data alongside the rest of your company's data in Snowflake, or to share it with
teams that already work there.

It creates each table from the query's columns, and loads rows one of four ways: through a stage (the default), with
`INSERT` statements, with pandas, or with Snowpipe Streaming, Snowflake's API for continuous, low-latency loading. A
command-line script in the SDK's repository loads large block ranges in parallel (see
[Load in parallel from the command line](/sdks/python/loaders/snowflake#load-in-parallel-from-the-command-line)).

[Loaders](/sdks/python/loaders) explains what every loader has in common: named connections, load options, and the
`LoadResult` each load returns.

## Prerequisites

* **Amp running, with the Quickstart's dataset.** The examples query `"acme/eth_mainnet"`, which the
  [Quickstart](/get-started/quickstart) deploys, through solo mode's query server at `grpc://localhost:1602`.

* **The Python SDK**, installed as [Installation](/sdks/python#installation) shows.

* **The Snowflake connector**, which the SDK doesn't install. Add `pandas` for the `pandas` loading method, and
  `snowpipe-streaming` for the `snowpipe_streaming` method:

  :::code-group
  ```bash [pip]
  pip install snowflake-connector-python
  # Only for loading_method='pandas'
  pip install pandas
  # Only for loading_method='snowpipe_streaming'
  pip install snowpipe-streaming
  ```

  ```bash [uv]
  uv add snowflake-connector-python
  # Only for loading_method='pandas'
  uv add pandas
  # Only for loading_method='snowpipe_streaming'
  uv add snowpipe-streaming
  ```
  :::

* **A Snowflake account**, with a warehouse to run the loads, a database to load into, and a user whose role can use
  the warehouse and create tables and stages in the database's schema (`PUBLIC`, unless you set `schema`). Set your
  account's identifier, like `xy12345.us-east-1` or `myorg-myaccount`, and the rest in environment variables:

  ```bash
  export SNOWFLAKE_ACCOUNT=…
  export SNOWFLAKE_USER=…
  export SNOWFLAKE_PRIVATE_KEY="$(cat rsa_key.p8)"
  export SNOWFLAKE_WAREHOUSE=…
  export SNOWFLAKE_DATABASE=…
  ```

  The user signs in with a key pair. [Authentication](/sdks/python/loaders/snowflake#authentication) shows how to create one.

## Load a table

This script loads the Quickstart's 100 blocks into a Snowflake table named `eth_blocks`:

```python [load_snowflake.py]
import os

from amp import Client

client = Client('grpc://localhost:1602')
client.configure_connection(
    'my_snowflake',
    'snowflake',
    {
        'account': os.environ['SNOWFLAKE_ACCOUNT'],
        'user': os.environ['SNOWFLAKE_USER'],
        'private_key': os.environ['SNOWFLAKE_PRIVATE_KEY'],
        'warehouse': os.environ['SNOWFLAKE_WAREHOUSE'],
        'database': os.environ['SNOWFLAKE_DATABASE'],
        'connection_params': {'timezone': 'UTC'},
    },
)

result = client.sql('SELECT block_num, timestamp, hash, gas_used FROM "acme/eth_mainnet".blocks').load(
    connection='my_snowflake', destination='eth_blocks', read_all=True
)
print(result)
```

```bash
python load_snowflake.py
```

When it succeeds, it prints `✅ Loaded 100 rows to eth_blocks`, with the time the load took. When it fails, it prints
`❌ Failed to load to eth_blocks:` and the error: a load doesn't raise exceptions, it returns them in its `LoadResult`.

* `configure_connection()` names a connection, `my_snowflake`, and says which loader it uses, `snowflake`. Every
  [option](/sdks/python/loaders/snowflake#configuration) goes in its dictionary.
* `'timezone': 'UTC'` makes Snowflake read timestamps as UTC. The `stage` and `insert` methods send timestamps
  without their time zone, so Snowflake reads them in the session's time zone, which is the account's default unless
  you set one.
* `read_all=True` loads the whole result at once. Without it, the loader loads it batch by batch.

:::warning
`configure_connection()` saves the connection's configuration, key or password included, in plain text in
`~/.flight_sql_connections.json`, so other scripts can use it by name. Set the `FLIGHT_SQL_CONNECTIONS` environment
variable to another path to keep it elsewhere.
:::

In Snowflake, the loader creates the table with an unquoted name, which Snowflake stores in uppercase, `ETH_BLOCKS`,
and quoted column names, which keep their lowercase. Quote the columns when you query them:

```sql
SELECT "block_num", "timestamp", "gas_used" FROM eth_blocks ORDER BY "block_num" DESC LIMIT 5;
```

The `pandas` method is different: see [Loading methods](/sdks/python/loaders/snowflake#loading-methods).

## Configuration

`account`, `user`, `warehouse`, and `database` are required. Any key not listed here fails the load (see
[Troubleshooting](/sdks/python/loaders/snowflake#troubleshooting)). The `state` and `resilience` keys, which every loader takes, are on
[Loaders](/sdks/python/loaders).

### Connection

| Option                         | Type              | Default    | Description                                                                                                                        |
| ------------------------------ | ----------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `account`                      | string            | required   | Your account's identifier, like `'xy12345.us-east-1'` or `'myorg-myaccount'`.                                                      |
| `user`                         | string            | required   | The user to sign in as, like `'AMP_LOADER'`.                                                                                       |
| `warehouse`                    | string            | required   | The warehouse that runs the loads, like `'COMPUTE_WH'`.                                                                            |
| `database`                     | string            | required   | The database to load into, like `'AMP'`.                                                                                           |
| `schema`                       | string            | `'PUBLIC'` | The schema to create tables in, like `'ETHEREUM'`.                                                                                 |
| `role`                         | string            | none       | The role to use, like `'LOADER'`. Without it, Snowflake uses the user's default role.                                              |
| `use_connection_pool`          | boolean           | `True`     | Whether to reuse connections across loads in the same process (see [Connection pool](/sdks/python/loaders/snowflake#connection-pool)).                           |
| `pool_size`                    | integer           | `5`        | How many connections the pool opens at most, like `10`.                                                                            |
| `connection_params`            | dictionary        | `{}`       | Other parameters for the Snowflake connector, like `{'timezone': 'UTC'}`. They override the loader's own.                          |
| `login_timeout`                | integer (seconds) | `60`       | How long to wait for sign-in. Only without the connection pool; with it, set `login_timeout` in `connection_params`.               |
| `network_timeout`              | integer (seconds) | `300`      | How long to wait for a request. Only without the connection pool, which uses `600`; set it in `connection_params` instead.         |
| `socket_timeout`               | integer (seconds) | `300`      | How long to wait on the network socket. Only without the connection pool, which uses `600`; set it in `connection_params` instead. |
| `timezone`                     | string            | none       | The session's time zone, like `'UTC'`. Only without the connection pool; with it, set `timezone` in `connection_params`.           |
| `ocsp_response_cache_filename` | string            | none       | Where the connector caches certificate checks, like `'/tmp/ocsp_cache'`. Only without the connection pool.                         |
| `validate_default_parameters`  | boolean           | `True`     | Whether the connector checks `database`, `schema`, and `warehouse` when it connects. Only without the connection pool.             |
| `paramstyle`                   | string            | `'qmark'`  | The connector's placeholder style. Leave it as `'qmark'`: the loader's statements use `?`.                                         |

### Authentication

Set one way to sign in. The loader uses the first that applies: `authenticator`, then `private_key`, then `password`.

| Option                   | Type   | Default | Description                                                                                                                                  |
| ------------------------ | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `private_key`            | string | none    | A private key, as the text of a PKCS#8 PEM file, like `open('rsa_key.p8').read()`. Snowpipe Streaming requires it.                           |
| `private_key_passphrase` | string | none    | The passphrase of an encrypted `private_key`.                                                                                                |
| `password`               | string | none    | The user's password, if your account still allows it (see below).                                                                            |
| `authenticator`          | string | none    | Another way to sign in: `'oauth'` (with `token`), `'externalbrowser'` (single sign-on in a browser), or `'okta'` (with `okta_account_name`). |
| `token`                  | string | none    | The OAuth access token, with `authenticator='oauth'`.                                                                                        |
| `okta_account_name`      | string | none    | Your Okta account's name, with `authenticator='okta'`, like `'acme'` for `https://acme.okta.com`.                                            |

Snowflake is [phasing out sign-in with a password alone](https://docs.snowflake.com/en/user-guide/security-mfa-rollout),
on a schedule that varies by account and excludes trial and reader accounts. Once the rollout reaches an account, a new
user can't be `TYPE = LEGACY_SERVICE`, and a person's password needs a second factor, which a script can't provide. In
any account, a user of `TYPE = SERVICE` can't have a password. Use a key pair. Generate one, then give Snowflake the
public key:

```bash
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt
openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub
```

In Snowflake, set the contents of `rsa_key.pub`, without its `-----BEGIN` and `-----END` lines, as the user's public key
(`ALTER USER … SET RSA_PUBLIC_KEY='…'`). Snowflake's [key-pair authentication
guide](https://docs.snowflake.com/en/user-guide/key-pair-auth) has the details. Then put the private key in an
environment variable, as [Prerequisites](/sdks/python/loaders/snowflake#prerequisites) does, and pass it as `private_key`, as [Load a
table](/sdks/python/loaders/snowflake#load-a-table) does.

### Loading

| Option                            | Type       | Default                                  | Description                                                                                                                                                                                      |
| --------------------------------- | ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `loading_method`                  | string     | `'stage'`                                | How rows get into Snowflake: `'stage'`, `'insert'`, `'pandas'`, or `'snowpipe_streaming'` (see [Loading methods](/sdks/python/loaders/snowflake#loading-methods)).                                                             |
| `pandas_compression`              | string     | `'gzip'`                                 | With `'pandas'`: how to compress the files it uploads, `'gzip'`, `'snappy'`, or `'none'`.                                                                                                        |
| `pandas_parallel_threads`         | integer    | `4`                                      | With `'pandas'`: how many files it uploads at once, like `8`.                                                                                                                                    |
| `streaming_channel_prefix`        | string     | `'amp'`                                  | With `'snowpipe_streaming'`: the start of the pipe and channel names it creates, like `'eth'`.                                                                                                   |
| `streaming_max_retries`           | integer    | `3`                                      | With `'snowpipe_streaming'`: how many times to retry sending rows after a network error, a timeout, or throttling.                                                                               |
| `streaming_buffer_flush_interval` | integer    | `1`                                      | Accepted, but has no effect.                                                                                                                                                                     |
| `preserve_reorg_history`          | boolean    | `False`                                  | Whether a [reorg](/sdks/python/loaders/snowflake#streaming-and-reorgs) marks the rows it replaces instead of deleting them.                                                                                                    |
| `state`                           | dictionary | `{'enabled': True, 'storage': 'memory'}` | Where the loader records the block ranges it loaded. `'storage': 'snowflake'` keeps them in Snowflake (see [Stream state](/sdks/python/loaders/snowflake#stream-state)). [Loaders](/sdks/python/loaders) lists the other keys. |

### Connection pool

By default, loads in the same Python process share a pool of connections for each account, user, database, and schema,
so parallel loads don't each sign in. The pool opens up to `pool_size` connections, replaces connections that sat unused
for over an hour or stop responding, and makes a load wait up to 30 seconds for a free one. For a
[parallel load](/sdks/python/parallel-streaming), set `pool_size` above the number of workers.

With the pool, the loader connects with its own timeouts (`login_timeout` of 60 seconds, `network_timeout` and
`socket_timeout` of 600) and ignores `timezone`: set those in `connection_params` instead. Set
`use_connection_pool` to `False` to give each load its own connection, which uses every option above.

## What the loader creates

When it connects, the loader creates what its loading method needs, if it doesn't exist:

* **The stage** `AMP_STAGE`, with the `stage` method: an internal stage the loader uploads files to. Its name and
  compression (gzip) are fixed.
* **The table `amp_stream_state`**, with `'state': {'storage': 'snowflake'}` (see [Stream state](/sdks/python/loaders/snowflake#stream-state)).

Before the first batch, it creates the destination table, if it doesn't exist, in the configured `schema` (or the one
in a qualified name, like `ethereum.blocks`). Table names may only contain letters, digits, `_`, and `$`, and start
with a letter or `_`. Each column gets the closest Snowflake type:

| Amp (Arrow) type                      | Snowflake type                  |
| ------------------------------------- | ------------------------------- |
| Integers, signed or unsigned          | `TINYINT` to `BIGINT`           |
| `Float32`, `Float64`                  | `FLOAT`, `DOUBLE`               |
| `Decimal128(p, s)`                    | `NUMBER(p,s)`                   |
| `Utf8`                                | `VARCHAR`                       |
| `Binary`, `FixedSizeBinary(n)`        | `BINARY`, `BINARY(n)`           |
| `Boolean`                             | `BOOLEAN`                       |
| `Timestamp` with a time zone, without | `TIMESTAMP_TZ`, `TIMESTAMP_NTZ` |
| `Date32`, `Time64`                    | `DATE`, `TIME`                  |
| Lists                                 | `VARIANT`                       |
| Structs, maps                         | `OBJECT`                        |
| Anything else                         | `VARCHAR`                       |

Columns that can't be null get `NOT NULL`. The loader also adds:

* `_amp_batch_id` (`VARCHAR`), which identifies the block range each row came from, so a reorg can remove the rows of
  the blocks it replaces. It's empty for rows loaded without streaming.
* `_amp_is_current` (`BOOLEAN NOT NULL`) and `_amp_reorg_batch_id` (`VARCHAR(16)`), with `preserve_reorg_history`.
  Only streaming and parallel loads fill them, so use the option only for those. A stream also creates two views:
  `ETH_BLOCKS_CURRENT`, with only the rows no reorg replaced, and `ETH_BLOCKS_HISTORY`, with every row.

## Loading methods

| Method               | How it loads each batch                                                                                                                                                    | Choose it for                                                                                                                        |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `stage`              | Writes it as a compressed CSV file, uploads it to `AMP_STAGE`, and copies it into the table with `COPY INTO`, which then deletes the file.                                 | Most loads, including large and parallel ones.                                                                                       |
| `insert`             | Sends its rows as `INSERT` statements.                                                                                                                                     | Small loads and tests. It's the slowest.                                                                                             |
| `pandas`             | Turns it into a pandas DataFrame, and loads it with the connector's `write_pandas()`, retrying up to three times after a network error, a timeout, or expired credentials. | Loads where you already use pandas. Needs `pandas`.                                                                                  |
| `snowpipe_streaming` | Sends its rows through [Snowpipe Streaming](https://docs.snowflake.com/en/user-guide/snowpipe-streaming/data-load-snowpipe-streaming-overview), in chunks of up to 50,000. | Continuous [streams](/sdks/python/streaming), with the lowest latency. Needs `snowpipe-streaming` and a [key pair](/sdks/python/loaders/snowflake#authentication). |

The `pandas` method doesn't write to the table the others do. The loader still creates `ETH_BLOCKS` for `eth_blocks`,
and leaves it empty, while `write_pandas()` creates a second table, with a quoted lowercase name, and puts the rows
there. Its columns can be null, its timestamps are `TIMESTAMP_LTZ`, and it has no `_amp_batch_id`. Query it with the
name in quotes: `SELECT COUNT(*) FROM "eth_blocks"`. Timestamps stay correct without the `timezone` setting, as they do
with `snowpipe_streaming`.

Every method but `snowpipe_streaming` commits each batch before loading the next. Snowpipe Streaming works differently:

* For each table, the loader creates a pipe, `amp_eth_blocks_pipe` for `eth_blocks` (`streaming_channel_prefix`, the
  table's name, and `pipe`), then opens a channel on it, `amp_eth_blocks_default`. Each worker of a parallel load opens
  its own channel, `amp_eth_blocks_partition_0` and so on.
* The rows a batch reports as loaded are the rows it sent. Snowflake ingests them on its own, so they become queryable
  shortly after.
* The loader's role needs privileges to create pipes in the schema, as well as tables.

## Load modes and options

The Snowflake loader only appends: `mode='append'`, the default, is the only [load
mode](/sdks/python/loaders#load-modes) it supports. To replace a table's rows, delete them, or drop the table, in
Snowflake first.

With `snowpipe_streaming`, `load()` also takes `channel_suffix`, the end of the channel's name (default `'default'`),
to load into the same table from several processes at once:

```python
client.sql(query).load(connection='my_snowflake', destination='eth_blocks', channel_suffix='worker_1')
```

With `stream=True`, `load()` drops `channel_suffix`, so every process uses the `default` channel.

## Streaming and reorgs (Unstable)

With `stream=True`, the loader keeps loading new rows as Amp extracts blocks (see [Streaming](/sdks/python/streaming)).
Each row's `_amp_batch_id` records the block range it came from. When a [reorg](/get-started/concepts/reorgs) replaces
blocks, the loader finds the batches of those blocks in its stream state, and:

* Deletes their rows, by default.
* With `preserve_reorg_history`, keeps them, sets their `_amp_is_current` to `FALSE`, and sets their
  `_amp_reorg_batch_id` to the batch that replaced them. Query the `_CURRENT` view for the data as it stands, and the
  `_HISTORY` view for everything you loaded.

With `snowpipe_streaming`, it closes the table's channels first, and opens new ones for the next rows.

:::warning
The SDK's reorg detection has known issues, which make a stream of finished blocks reload them in a loop. See
[Known issues](/sdks/python/streaming#known-issues) before you stream into Snowflake.
:::

### Stream state

The loader records which block ranges it loaded, which is how it finds the rows of a reorg's blocks. By default, it
keeps that record in memory, for the length of the load. To keep it in Snowflake instead, set `state`:

```python
'state': {'enabled': True, 'storage': 'snowflake'},
```

The loader then creates a table named `amp_stream_state` in the configured schema, if it doesn't exist, with a row for
each range: the destination table, the network, the batch's identifier, its first and last blocks, and the hash of its
last block. If the loader's role can't create tables, create it yourself with
[`sql/snowflake_stream_state.sql`](https://github.com/edgeandnode/amp-python/blob/main/sql/snowflake_stream_state.sql),
from the SDK's repository.

:::warning
At this version, a stream with `'storage': 'snowflake'` fails as soon as the SDK reads the stored state, with
`001104 (42601): SQL compilation error: … 'AMP_STREAM_STATE.BATCH_ID' in select clause is neither an aggregate nor in the group by clause`.
Because of the [reorg issue](/sdks/python/streaming#every-block-range-is-reported-as-a-reorg), that's at the end of its
first block range. A parallel load writes the state without reading it.
:::

:::note
At this version, the stored state doesn't make a restarted load skip what it already loaded: a stream started again
loads its rows again, and so does a parallel load (see [Known issues](/sdks/python/streaming#known-issues)).
:::

## Load in parallel from the command line

The SDK's repository has a script, `apps/snowflake_parallel_loader.py`, that runs a SQL query from a file and loads its
results into Snowflake with a [parallel load](/sdks/python/parallel-streaming): it splits the block range into equal
parts, and loads them at the same time. Use it for one-off backfills of a large range, without writing Python.

### Set it up

The script runs from a clone of the repository, with [uv](https://docs.astral.sh/uv/). Clone it, and install the
Snowflake packages:

```bash
git clone https://github.com/edgeandnode/amp-python.git
cd amp-python
uv sync --group snowflake --group snowpipe_streaming --no-group notebooks
```

`--no-group notebooks` leaves out the repository's notebook tools, which can't be installed alongside
`snowpipe-streaming`. If you only use the `stage` or `insert` methods, `uv sync --group snowflake` is enough.

Run the script with `uv run --no-sync`, as below. A plain `uv run` syncs the environment again, with the default groups
instead of yours: it installs the notebook tools, and replaces the version of `msgspec` that `snowpipe-streaming` needs.
If that happens, run the `uv sync` command again.

The script signs in with a key pair only (see [Authentication](/sdks/python/loaders/snowflake#authentication)), and reads its connection from these
environment variables:

```bash
export SNOWFLAKE_ACCOUNT=…
export SNOWFLAKE_USER=…
export SNOWFLAKE_WAREHOUSE=…
export SNOWFLAKE_DATABASE=…
export SNOWFLAKE_PRIVATE_KEY="$(cat rsa_key.p8)"
```

It loads into the `PUBLIC` schema, which must exist, with the user's default role. Set `AMP_AUTH_TOKEN` too if your Amp
requires a token
(see [Authentication](/sdks/python/authentication)).

### Run it

Write the query in a file. It must select the column the script splits the range on, `block_num` by default:

```sql [logs.sql]
SELECT block_num, tx_hash, log_index, address, topic0
FROM "acme/eth_mainnet".logs
```

Then load the Quickstart's 100 blocks with four workers:

```bash
uv run --no-sync python apps/snowflake_parallel_loader.py \
  --server grpc://localhost:1602 \
  --query-file logs.sql \
  --table-name eth_logs \
  --source-table '"acme/eth_mainnet".logs' \
  --min-block 24000000 \
  --max-block 24000100 \
  --loading-method stage \
  --workers 4
```

The script adds a block filter to the query for each part, like `WHERE block_num >= 24000000 AND block_num < 24000025`.
It logs each worker's progress, then prints a summary: the block range, the rows loaded, the time it took, and how
many parts succeeded.

:::warning
Pass `--loading-method stage`, as above. With the default, `snowpipe_streaming`, each worker fails at the empty batch
that ends its part, with `Invalid request for zero_num_rows: request contains 0 rows`, and the summary says
`Rows loaded: 0` and `Successful: 0/4 batches`, even though the rows were sent and do reach the table. Running it again
loads them again.
:::

Running the same command again loads the range again. To reload a range, delete its rows first, or load into another
table.

### Options

| Flag                          | Default                    | Description                                                                                                                                               |
| ----------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--query-file PATH`           | required                   | The file with the SQL query to run, like `--query-file logs.sql`.                                                                                         |
| `--table-name NAME`           | required                   | The Snowflake table to load into, like `--table-name eth_logs`.                                                                                           |
| `--min-block N`               | none                       | The first block to load, like `--min-block 24000000`. Requires `--max-block`.                                                                             |
| `--max-block N`               | none                       | The block to stop before: the script loads up to the block before it, like `--max-block 24000100`. Requires `--min-block`.                                |
| `--blocks N`                  | none                       | Instead of `--min-block` and `--max-block`, load the `N` blocks before the last block of `--source-table`, like `--blocks 50000`.                         |
| `--source-table TABLE`        | `eth_firehose.logs`        | The table `--blocks` finds the last block of, like `--source-table '"acme/eth_mainnet".logs'`. Set it to your query's table.                              |
| `--block-column COLUMN`       | `block_num`                | The column to split the range on, like `--block-column _block_num`.                                                                                       |
| `--workers N`                 | `4`                        | How many parts to split the range into, and load at once, like `--workers 8`.                                                                             |
| `--loading-method METHOD`     | `snowpipe_streaming`       | `stage`, `insert`, or `snowpipe_streaming`, which [reports failure](/sdks/python/loaders/snowflake#run-it) (see [Loading methods](/sdks/python/loaders/snowflake#loading-methods)).                                   |
| `--pool-size N`               | `--workers` plus 2         | How many Snowflake connections to open at most, like `--pool-size 12`.                                                                                    |
| `--preserve-reorg-history`    | on                         | Add the `_amp_is_current` and `_amp_reorg_batch_id` columns (see [What the loader creates](/sdks/python/loaders/snowflake#what-the-loader-creates)).                                    |
| `--no-preserve-reorg-history` | off                        | Leave them out.                                                                                                                                           |
| `--disable-state`             | off                        | Don't record the loaded block ranges in the `amp_stream_state` table.                                                                                     |
| `--connection-name NAME`      | `snowflake_` and the table | The name of the connection the script configures, like `--connection-name eth_logs_load`.                                                                 |
| `--label-csv PATH`            | none                       | A CSV file of [labels](/sdks/python/labels) to join to the rows, like `--label-csv tokens.csv`. Requires the next three flags.                            |
| `--label-name NAME`           | none                       | The name to give the labels, like `--label-name tokens`.                                                                                                  |
| `--label-key COLUMN`          | none                       | The CSV column to join on, like `--label-key token_address`.                                                                                              |
| `--stream-key COLUMN`         | none                       | The query's column to join on, like `--stream-key token_address`. Rows without a matching label are left out.                                             |
| `--flush-interval SECONDS`    | `1.0`                      | Accepted, but has no effect.                                                                                                                              |
| `--server URL`                | `AMP_SERVER_URL`           | Your Amp's query server, like `--server grpc://localhost:1602`. Without it or `AMP_SERVER_URL`, the script uses an address built into it: always set one. |
| `--verbose`                   | off                        | Show the Snowflake libraries' detailed logs.                                                                                                              |

`uv run --no-sync python apps/snowflake_parallel_loader.py --help` lists them too. The repository's `apps/queries/` has
an example query that decodes ERC-20 token transfers.

## Troubleshooting

### `Invalid SnowflakeLoader configuration: … missing 2 required positional arguments: 'warehouse' and 'database'`

The connection's configuration lacks required options. Add `account`, `user`, `warehouse`, and `database`.

### `Invalid SnowflakeLoader configuration: … got an unexpected keyword argument`

The configuration has an option the loader doesn't know, named at the end of the message. Check its spelling against
[Configuration](/sdks/python/loaders/snowflake#configuration). `stage_name` and `compression` aren't options: the stage's name and compression are
fixed.

### `290404 (08001): 404 Not Found: post myorg-myaccount.snowflakecomputing.com:443/session/v1/login-request`

Snowflake has no account with the identifier in `account`, here `myorg-myaccount`. Check its spelling, and its form:
`orgname-accountname`, or a locator like `xy12345.us-east-1`.

### `390201 (08001): Failed to connect to DB: … The requested schema does not exist or not authorized.`

The schema in `schema`, or `PUBLIC` without it, doesn't exist, or the user's role can't use it. Create it, or set
`schema` to one the role can use.

### `390100 (28000): Failed to connect to DB: … Incorrect username or password was specified.`

Snowflake refused the login. A user of `TYPE = SERVICE` can't sign in with a password: use a key pair (see
[Authentication](/sdks/python/loaders/snowflake#authentication)).

### `Failed to parse private key: … Ensure the key is in PKCS#8 PEM format (unencrypted or with passphrase).`

`private_key` isn't the text of a PKCS#8 PEM file, which starts with `-----BEGIN PRIVATE KEY-----` (or
`-----BEGIN ENCRYPTED PRIVATE KEY-----`, with `private_key_passphrase`). Generate the key as
[Authentication](/sdks/python/loaders/snowflake#authentication) shows, and pass the file's contents, not its path.

### `Snowpipe Streaming requires private_key authentication. Password authentication is not supported.`

`loading_method='snowpipe_streaming'` needs a key pair. Set `private_key` (see [Authentication](/sdks/python/loaders/snowflake#authentication)).

### `snowpipe-streaming package required for Snowpipe Streaming.`

Install the `snowpipe-streaming` package (see [Prerequisites](/sdks/python/loaders/snowflake#prerequisites)).

### `pandas and snowflake.connector.pandas_tools are required for pandas loading.`

Install the `pandas` package (see [Prerequisites](/sdks/python/loaders/snowflake#prerequisites)).

### `Snowflake loader does not support OVERWRITE mode`, or `Unsupported mode LoadMode.OVERWRITE`

The loader only appends. Remove `mode='overwrite'`, and delete the table's rows in Snowflake before the load if you
want to replace them.

### `100072 (22000): NULL result in a non-nullable column`

`preserve_reorg_history` is on, in a load that isn't a stream or a parallel load, which don't fill its columns. Turn it
off, or stream.

### `Invalid SQL table name: 'eth-blocks'`

The destination isn't a valid unquoted Snowflake name. Use letters, digits, `_`, and `$`, like `eth_blocks`.

### `Failed to acquire connection from pool within 30.0s`

Every connection in the pool was busy for 30 seconds, which happens when more loads run at once than `pool_size`
allows. Raise `pool_size` above the number of parallel workers.

### `Loaded 100 rows`, but the table is empty

With `loading_method='pandas'`, the rows are in a second table, with a quoted lowercase name: see
[Loading methods](/sdks/python/loaders/snowflake#loading-methods).

### `invalid identifier 'BLOCK_NUM'` when you query the table

Snowflake reads unquoted names in uppercase, and the loader's columns are lowercase. Quote them: `"block_num"`.

### `Query file not found: logs.sql`, from the command-line script

The script reads the query file relative to the directory you run it from, the repository's. Pass the file's full
path, or a path relative to the repository.
