3.9 Upload a file & register it as a table¶
Concept¶
A very common real task: someone hands you a file, you drop it in the data lake (RustFS), then
make it a catalog table so everyone can query it in SQL and Spark — governed, shared, and
addressable as my.<namespace>.<table>.
Three moves: upload → read → register. Then it's a first-class table in the my catalog —
your own lakehouse — visible to your notebooks (SQL and Spark) and the Polaris Console.
This is the ad-hoc "someone handed me a file" version. Ingesting ShopFlow's own sources (Postgres + Parquet) into Bronze systematically — the pipeline version — is 4.2.
Lab¶
1 · Upload the file to the lake (RustFS)¶
Open the RustFS console at http://localhost:9001/rustfs/console/ (sign in with your lab account):
- Go to your own bucket,
<username>-lake— the only one members see in the console (managers also see the shareddemo-bucket). - Create/enter a folder, e.g.
files/source/uploads/. - Upload your file — say
customers.csv.
The object is now at s3a://demouser-lake/files/source/uploads/customers.csv. (📁 My files on the
landing page uploads to the same place. Any S3 client works too — RustFS is S3-compatible, reached
inside the stack at http://storage:9000 — but the console needs no tooling.)
The examples use the default lab account's bucket, demouser-lake — signed in with your own
account? Use your bucket instead (shown at the top of the landing page, e.g. ravi-lake).
2 · Read it in the notebook¶
spark is already there (3.7). Point it at the path:
path = "s3a://demouser-lake/files/source/uploads/customers.csv" # your own bucket
df = (spark.read
.option("header", True) # first row is column names
.option("inferSchema", True) # detect types (int/double/…)
.csv(path))
df.printSchema()
df.show(5)
For Parquet it's just spark.read.parquet("s3a://demouser-lake/files/source/uploads/…") (schema is built in — no
options needed).
3 · Register it as a catalog table¶
Write the DataFrame into the governed my catalog. Pick (or create) a namespace, then
create the table:
spark.sql("CREATE NAMESPACE IF NOT EXISTS my.bronze")
df.writeTo("my.bronze.customers_upload").createOrReplace()
writeTo("my.bronze.customers_upload")— target table inmy→ namespacebronze..createOrReplace()— create it (or replace if re-running). The data is written as Iceberg files in RustFS and the table is registered in Polaris.
SQL-only alternative — and getting the column names right¶
You can register the file without the DataFrame step, but there's a trap. The obvious one-liner does not work well for CSVs with a header:
%%sql
-- DON'T: the csv.`path` shorthand accepts NO options, so it can't set header=true
CREATE TABLE my.bronze.customers_upload USING iceberg AS
SELECT * FROM csv.`s3a://demouser-lake/files/source/uploads/customers.csv`
_c0, _c1, _c2 and the header line (id,name,country) lands as a data row:
| _c0 | _c1 | _c2 |
|---|---|---|
| id | name | country |
| 1 | Ada | UK |
The fix: register a temp view with USING csv OPTIONS (...) first, then CREATE TABLE … AS.
The options are where header/inferSchema live (%%sql is one statement per cell, so this is two
cells):
%%sql
CREATE TEMPORARY VIEW customers_raw USING csv
OPTIONS (path 's3a://demouser-lake/files/source/uploads/customers.csv', header 'true', inferSchema 'true')
id, name, country) with inferred types —
no stray header row.
header 'true'— treat row 1 as column names (and skip it from the data).inferSchema 'true'— detect types (int/double/…); omit it and every column isstring.- For Parquet there's no header problem — a Parquet file already carries names and types built in,
so the plain
parquet.<path>shorthand works fine (no options needed).
Define the columns yourself (explicit names + types)
Don't want inferred names/types? Declare the schema on the view and keep header 'true' so
the first row is still skipped:
%%sql
CREATE TEMPORARY VIEW customers_raw (id INT, name STRING, country STRING) USING csv
OPTIONS (path 's3a://demouser-lake/files/source/uploads/customers.csv', header 'true')
.option("header", True) / .schema(...) — the
header/options spark.read sets in Python become the OPTIONS (...) clause in SQL.
4 · Access it — SQL and Spark¶
It's now a normal catalog table. Query it any way:
The same table is now visible in the Polaris Console (your lakehouse <username>_lake) and to
every Spark session that signs in as you — one governed copy.
From SQLPad (Trino)¶
SQLPad reaches the same table by your lakehouse's real name, <username>_lake. my
is a short name only Jupyter knows — in SQLPad (and Superset) iceberg is the course's shared
lake (0.3). The real name works in Jupyter too, so a query written with it
runs in both:
| Where | The table is |
|---|---|
Jupyter (%%sql, spark.table) |
my.bronze.customers_upload or demouser_lake.bronze.customers_upload |
| SQLPad, Superset | demouser_lake.bronze.customers_upload |
Open SQLPad, pick the connection Lakehouse (Trino) — demouser (your own username), and run:
ORDER BY id— sort byid, so the rows come back in the same order every time.iceberg.bronze.customers_uploadhere fails with Table 'iceberg.bronze.customers_upload' does not exist: that name looks in the shared lake.
SQLPad also changes the table — it's your table, so UPDATE, INSERT and DELETE work:
UPDATE … SET name = 'Surendra'— set the columnnameto a new value…WHERE id = 2— …only in the rows that match (leave outWHEREand every row changes).
Run the SELECT again (in Jupyter too) and row 2 now says Surendra — one table, read and changed
from both tools.
Text values take 'single quotes'
In SQL, 'Surendra' is a text value, but "Surendra" (double quotes) is a column name.
SET name = "Surendra" fails with Column 'surendra' cannot be resolved: Trino looks for a
column called surendra. Double quotes are only for column or table names with spaces or
capitals, like "Order Date".
Just exploring? Use a temp view instead¶
If you only need to poke at a file for this session (no permanent table), skip the catalog and register a temp view — no write, no namespace:
The view disappears when the kernel stops; a catalog table persists and is shared.Next: the data you just registered is rarely clean — 4.2.1 Data cleaning takes two messy Bronze tables to clean Silver.
🎯 This runs unchanged on Azure, Databricks, Snowflake & Fabric¶
Upload to ADLS / S3, then CREATE TABLE … AS SELECT or writeTo(...).createOrReplace() into
the Databricks catalog / Snowflake / Fabric — the exact same read-then-register
pattern; only the storage URL and catalog name change.
You can now…¶
- Upload a file to the data lake via the RustFS console
- Read a CSV/Parquet from
s3a://…in a notebook - Register it as a governed
mycatalog table (writeTo().createOrReplace()orCREATE TABLE AS) - In SQL-only mode, get column names right with
CREATE TEMPORARY VIEW … USING csv OPTIONS (header 'true', …)— or declare an explicit schema — instead of the header-losingcsv.<path>shorthand - Query it from SQL and Spark, and from SQLPad as
<username>_lake.bronze.…(select and update) — and know when a temp view is enough instead