feat: support a stage/PUT/COPY/MERGE bulk-load pipeline in server mode - #400
arnaldojvg wants to merge 9 commits into
Conversation
|
thanks for the contribution @arnaldojvg - fyi this is a large PR and will take me some time to review each commit, if you wanted to progress some of these more quickly then I recommend raising individual PRs |
|
Thanks @tekumara, let me know of anything 🙏 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stores named file formats in the info schema so they can be referenced by name, eg: from COPY INTO FILE_FORMAT = (FORMAT_NAME = ...). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FILE_FORMAT = (FORMAT_NAME = ...) resolves named file formats - SKIP_HEADER > 1, NULL_IF, EMPTY_FIELD_AS_NULL, ESCAPE_UNENCLOSED_FIELD and COMPRESSION are mapped onto duckdb's read_csv - ON_ERROR accepts a string literal - a stage reference can include a path suffix, eg: @stage1/file.csv.gz Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sqlglot only parses PUT as exp.Put when the source is a quoted literal, so an unquoted url, as sent by eg: snowflake-sqlalchemy, fell through to duckdb as a raw command. Quote it before parsing. Honour AUTO_COMPRESS, OVERWRITE, PARALLEL and SOURCE_COMPRESSION, and don't recompress an already gzipped source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snowflake tracks load metadata per table, so the same file loaded into a different table is not skipped, and recreating a table resets its load history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eg: PUT file://data.csv @db1.schema1.%mytable and COPY INTO t FROM @db1.schema1.%mytable/data.csv.gz Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs the full pipeline twice on one server to prove CREATE OR REPLACE STAGE/FILE FORMAT/TABLE work on rerun. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
duckdb's COUNT_IF returns NULL over an empty merge_candidates table, which crashed clients that read the counts, eg: the snowflake python connector calls int() on them. Snowflake always returns integers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The server returned a LOCAL_FS stage location, so the connector wrote the file to the client's own filesystem. When the client doesn't share the server's filesystem (eg: the server runs in a container) a subsequent COPY INTO silently found no files. Return a GCS-style stage info with a presigned url pointing back at the fakesnow server instead, so the connector uploads the file over http into the stage's backing storage. A PUT with a ? placeholder target keeps LOCAL_FS because the connector re-requests the presigned url by executing the command without bindings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ca7d9b4 to
8cd4caf
Compare
|
Thanks for the review and the suggestion — agreed this is a lot to review at once. I've split it into four issues and PRs, all cherry-picked verbatim from this branch, each independently green (full suite + hooks):
#410, #411 and #412 are independent and can be reviewed in any order; #413 is based on #412's branch and carries #411's commit until they merge, so it is green standalone and its diff shrinks to its own changes as the others land. This PR remains as the integrated reference — happy for it to be closed in favour of the splits whenever you prefer. 🤖 Generated with Claude Code |
duckdb's `COUNT_IF` returns NULL over an empty `merge_candidates` table, which crashed clients that read the counts, eg: snowflake-connector-python calls `int()` on them. Snowflake always returns integers, so wrap each count in `COALESCE(..., 0)`. Regression tests cover both local mode and the server/connector path (where the `int()` crash happened). Fixes #406. Split out of #400 per maintainer request. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: arnaldo-cai <arnaldo.varela@clarity.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Oliver Mannion <125105+tekumara@users.noreply.github.com>
Snowflake tracks load metadata per table, so the same file loaded into a different table is not skipped, and recreating a table resets its load history. The dedup check now matches on file name, table name and schema, and `CREATE TABLE` clears the table's load-history rows. Fixes #407. Split out of #400 per maintainer request. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: arnaldo-cai <arnaldo.varela@clarity.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
I'll close this since we are managing this on the smaller PRs |
Implements the Snowflake features needed to run acceptance tests of a stage/PUT/COPY/MERGE bulk-load pipeline (Postgres → Snowflake data-sync job using snowflake-sqlalchemy) against fakesnow in server mode.
Fixes #403
Features
_fs_global._fs_information_schema._fs_file_formats). Previously this crashed the server with a 500 (AssertionError: Unexpected parent kind: FILE FORMATin checks.py).read_csv:SKIP_HEADER=<n>,NULL_IF,EMPTY_FIELD_AS_NULL,ESCAPE_UNENCLOSED_FIELD='NONE',COMPRESSION(AUTO/GZIP/NONE).ON_ERRORalso accepts a string literal (ON_ERROR = 'ABORT_STATEMENT').exp.Putwhen the source is a quoted literal, so the statement fell through to duckdb raw and failed withParser Error: syntax error at or near "PUT". The source is now quoted before parsing (both embedded and server mode).AUTO_COMPRESS,OVERWRITE,PARALLEL,SOURCE_COMPRESSIONare honoured (and returned to the client in server mode, so the connector's file transfer agent respects them). An already gzipped source is no longer recompressed.COPY INTO t FROM @stage/file.csv.gz(prefix match, like Snowflake).PUT file://data.csv @db.schema.%mytableandCOPY INTO t FROM @db.schema.%mytable/data.csv.gz.CREATE OR REPLACE TEMPORARY TABLE staging) resets its load history, matching Snowflake's per-table load metadata.An end-to-end server-mode test runs the full pipeline (CREATE OR REPLACE STAGE / FILE FORMAT / TEMPORARY TABLE, PUT AUTO_COMPRESS=FALSE, COPY with FORMAT_NAME, MERGE) twice on one server to prove reruns work.
Out of scope
LIST @%table_stage— sqlglot cannot parse LIST with a table stage reference; table stages are supported for PUT and COPY only.ESCAPE_UNENCLOSED_FIELDvalues other than'NONE', andEMPTY_FIELD_AS_NULL=FALSEcombined withNULL_IF=(''), raise NotImplementedError as they have no faithful duckdb mapping.SHOW FILE FORMATS/DROP FILE FORMAT.🤖 Generated with Claude Code