Skip to content

feat: PUT over http in server mode, unquoted PUT urls, PUT options, and table stages - #413

Closed
arnaldojvg wants to merge 9 commits into
tekumara:mainfrom
arnaldojvg:feat-put-server-mode-and-table-stages
Closed

arnaldojvg wants to merge 9 commits into
tekumara:mainfrom
arnaldojvg:feat-put-server-mode-and-table-stages

Conversation

@arnaldojvg

@arnaldojvg arnaldojvg commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Depends on #412 (based on its branch) — contains its commits until it merges. Please merge #412 first; I'll rebase this PR right after so only its own 4 commits remain. (If this one is merged first instead, #412 can simply be closed as already included.)

  • Unquoted PUT urls: 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; the source is quoted before parsing. AUTO_COMPRESS, OVERWRITE, PARALLEL and SOURCE_COMPRESSION are honoured, and an already gzipped source is no longer recompressed.
  • PUT over http in server mode: 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. The server now returns a GCS-style stage info with a presigned url pointing back at the fakesnow server, so the connector uploads the file over http into the stage's backing storage (GCS is the only location type the connector uploads to via a plain http presigned url). A PUT with a ? placeholder target keeps LOCAL_FS because the connector re-requests the presigned url by executing the command without bindings.
  • Table stages: a stage name starting with % is a table stage, which exists implicitly for every table: existence is checked against the table catalog rather than the stage registry, and its files live in the internal-stage backing storage. Out of scope: LIST @%table, which sqlglot cannot parse.
  • Includes an end-to-end server-mode test running the full stage/PUT/COPY/MERGE pipeline twice on one server (the rerun needs fix: scope load history to the target table #411's per-table load history, hence the carried commit).

Fixes #409. Split out of #400 per maintainer request.

🤖 Generated with Claude Code

arnaldo-cai and others added 9 commits September 4, 2026 09:51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A replaced stage starts empty, as in Snowflake.

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>
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>
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>
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>
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>
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>
@arnaldojvg

Copy link
Copy Markdown
Contributor Author

Superseded by #420, #422 and #424 (split per the discussion in #412); closing.

@arnaldojvg arnaldojvg closed this Sep 10, 2026
arnaldojvg pushed a commit to arnaldojvg/fakesnow that referenced this pull request Sep 12, 2026
Accept PUT commands as the connector and most snowflake clients issue
them: an unquoted file:// source, which sqlglot otherwise parses as an
opaque command, and options such as AUTO_COMPRESS, OVERWRITE,
SOURCE_COMPRESSION and PARALLEL. AUTO_COMPRESS = FALSE and already
gzipped sources are stored as-is rather than being gzipped again, and
the upload result reports the real compression of each side.

Part of tekumara#409, umbrella tekumara#403. Supersedes part of tekumara#413.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arnaldojvg pushed a commit to arnaldojvg/fakesnow that referenced this pull request Sep 12, 2026
In server mode the PUT response told the connector to write the file to
a local path, which only works when the client shares the server's
filesystem. Return a GCS-style stage info with a presigned url instead,
so the connector uploads the file over http to a new /fs_bucket route
that stores it in the stage's backing directory. GCS is the only
location type the connector uploads to via a plain http url, and it
re-requests the url with the destination file name, so PUT now accepts
a bare file://name.gz source. A PUT with a ? placeholder target keeps
LOCAL_FS because that re-request runs without bindings.

The existing server-mode PUT test (test_copy_internal_stage_server)
now exercises the http upload.

Part of tekumara#409, umbrella tekumara#403. Supersedes part of tekumara#413.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arnaldojvg pushed a commit to arnaldojvg/fakesnow that referenced this pull request Sep 12, 2026
A table stage (@%table or @db.schema.%table) exists implicitly for
every table, so PUT, LIST and COPY INTO resolve it against the table
catalog rather than the stages table. A stage reference can also carry
a path suffix (@stage/dir/file.csv.gz) which COPY INTO treats as a
prefix match within the stage, as Snowflake does.

Internal stage globs now use the plain path instead of a file:// uri,
because duckdb does not decode percent-encoded uris.

Adds end-to-end server-mode tests for the stage/PUT/COPY/MERGE
bulk-load pipeline and for loading a gzipped csv from a table stage.

Part of tekumara#409, umbrella tekumara#403. Supersedes part of tekumara#413.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arnaldojvg pushed a commit to arnaldojvg/fakesnow that referenced this pull request Sep 21, 2026
In server mode the PUT response told the connector to write the file to
a local path, which only works when the client shares the server's
filesystem. Return a GCS-style stage info with a presigned url instead,
so the connector uploads the file over http to a new /fs_bucket route
that stores it in the stage's backing directory. GCS is the only
location type the connector uploads to via a plain http url, and it
re-requests the url with the destination file name, so PUT now accepts
a bare file://name.gz source. A PUT with a bound ? target keeps
LOCAL_FS because that re-request runs without bindings.

Part of tekumara#409, umbrella tekumara#403. Supersedes part of tekumara#413.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
arnaldojvg pushed a commit to arnaldojvg/fakesnow that referenced this pull request Sep 21, 2026
A table stage (@%table or @db.schema.%table) exists implicitly for
every table, so PUT, LIST and COPY INTO resolve it against the table
catalog rather than the stages table. A stage reference can also carry
a path suffix (@stage/dir/file.csv.gz) which COPY INTO treats as a
prefix match within the stage, as Snowflake does.

Internal stage globs now use the plain path instead of a file:// uri,
because duckdb does not decode percent-encoded uris.

Adds end-to-end server-mode tests for the stage/PUT/COPY/MERGE
bulk-load pipeline and for loading a gzipped csv from a table stage.

Part of tekumara#409, umbrella tekumara#403. Supersedes part of tekumara#413.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server-mode PUT is unusable: unquoted urls fail to parse, files land on the client, table stages unsupported

2 participants