How to Replicate Databricks Lakebase to Snowflake with Sling

Slinger avatar
Slinger
Cover for How to Replicate Databricks Lakebase to Snowflake with Sling

How to Replicate Databricks Lakebase to Snowflake with Sling

Last updated: July 2026

Databricks Lakebase is a serverless Postgres database, built on the Neon engine Databricks acquired in 2025 and generally available since February 2026. It holds operational, transactional data: the reads and writes an application or an AI agent makes in real time, kept separate from the analytical tables in your lakehouse or warehouse. The idea is to keep the OLTP store fast and small and move data out to a warehouse when you need to run analytics across it.

Snowflake is a common destination for that analytics half, which raises an obvious question. How do you get data out of Lakebase and into Snowflake without standing up a separate ETL platform to do it?

Here’s the thing that makes it easy: Lakebase is Postgres. Not Postgres-like, actual Postgres, running version 17 and speaking the standard wire protocol on port 5432. Anything that connects to Postgres connects to Lakebase. That includes Sling, which treats Lakebase as an ordinary postgres source and moves it to Snowflake with a few lines of YAML.

This guide walks through that move end to end. The row counts, timings, and type mappings below come from a real replication run: a Postgres source loaded with a demo e-commerce schema, replicated into a live Snowflake warehouse. Because Lakebase presents as standard Postgres, the mechanics don’t change whether the source is a self-hosted Postgres or a Lakebase endpoint. Only the host and the credentials change.

Installation

Sling is a single binary with no runtime dependencies. Install it however suits your setup:

# macOS / Linux
curl -fsSL https://slingdata.io/install.sh | bash

# Windows
irm https://slingdata.io/install.ps1 | iex

# Python
pip install sling

Confirm it’s on your path:

sling --version

Connecting Sling to Lakebase

A Lakebase instance exposes a standard Postgres connection: a hostname (Databricks generates one that looks like ep-<id>.databricks.com), port 5432, and a default database named databricks_postgres. SSL is required — Lakebase rejects unencrypted connections — so the connection string carries sslmode=require.

The one decision worth making up front is authentication. Lakebase supports two methods, and they behave very differently for a data-movement tool. You can pass an OAuth token as the password, which is convenient for poking around interactively, but the token expires after an hour, so a scheduled replication that runs overnight will fail when it goes stale. Or you can use a native Postgres role and password, which doesn’t expire and works with connection pooling. Use the second one for Sling.

Create a Postgres role in Lakebase, give it read access to the tables you’re replicating, and use that role’s credentials. Then register the connection with Sling. Since Lakebase is Postgres, the connection type is postgres:

sling conns set lakebase type=postgres \
  host=ep-your-instance.databricks.com port=5432 \
  user=your_role password=your_password \
  database=databricks_postgres sslmode=require

Or as a connection string:

sling conns set lakebase \
  url="postgres://your_role:[email protected]:5432/databricks_postgres?sslmode=require"

Connecting Sling to Snowflake

Snowflake authenticates with an account identifier, a user, and either a password or a key pair. The Sling user needs USAGE on the warehouse, database, and target schema, plus CREATE TABLE on the schema so Sling can create the destination tables.

sling conns set snowflake type=snowflake \
  account=your-account user=your-user password=your-password \
  database=analytics warehouse=compute_wh role=your-role

Test both connections

sling conns test lakebase
sling conns test snowflake
8:41AM INF success!

If the Lakebase test hangs before failing, the instance may have scaled its compute to zero while idle — Lakebase spins compute down when nothing is connected and back up on demand. Give it a moment and test again. If it fails outright, check that sslmode=require is present and that your role has login rights.

The source tables

The demo source is a small e-commerce schema, the kind of transactional data that lives in an OLTP store: 15,000 customers, 3,000 products, and 90,000 orders. The orders table carries an ordered_at timestamp, which matters for incremental loads later, plus a nullable promo_code column so the type-handling examples have something to point at.

-- on the Lakebase source
select 'customers' as t, count(*) as c from demo_lakebase_snowflake.customers
union all select 'products', count(*) from demo_lakebase_snowflake.products
union all select 'orders',   count(*) from demo_lakebase_snowflake.orders;
t          c
customers  15000
products   3000
orders     90000

Full refresh: the first load

A replication is one YAML file. The defaults block sets the mode and how targets are named; streams lists what to move. The {stream_table} token maps each source table to a Snowflake table of the same name.

# replication.yaml
source: lakebase
target: snowflake

defaults:
  mode: full-refresh
  object: demo_lakebase_snowflake.{stream_table}

streams:
  demo_lakebase_snowflake.customers:
  demo_lakebase_snowflake.products:
  demo_lakebase_snowflake.orders:

Run it:

sling run -r replication.yaml
INF Sling Replication [3 streams] | lakebase -> snowflake

INF [1 / 3] running stream demo_lakebase_snowflake.customers
INF writing to target database [mode: full-refresh]
INF created table "DEMO_LAKEBASE_SNOWFLAKE"."CUSTOMERS_TMP"
INF created table "DEMO_LAKEBASE_SNOWFLAKE"."CUSTOMERS"
INF inserted 15000 rows into "DEMO_LAKEBASE_SNOWFLAKE"."CUSTOMERS" in 15 secs [993 r/s] [1.3 MB]
...
INF [3 / 3] running stream demo_lakebase_snowflake.orders
INF created table "DEMO_LAKEBASE_SNOWFLAKE"."ORDERS_TMP"
INF inserted 90000 rows into "DEMO_LAKEBASE_SNOWFLAKE"."ORDERS" in 13 secs [6,814 r/s] [5.5 MB]
INF execution succeeded

INF Sling Replication Completed in 40s | lakebase -> snowflake | 3 Successes | 0 Failures

Three tables, 108,000 rows, 40 seconds. Two details in that log are worth noticing. First, Sling writes to a _TMP table and swaps it in once the load is clean, so a failed run leaves the existing table untouched rather than half-loaded. Second, Sling stages the data and loads it into Snowflake in bulk with COPY INTO rather than inserting row by row — that’s why 90,000 rows land in 13 seconds.

You don’t have to pre-create the tables. Sling creates them on first run and adjusts them on later runs when the schema changes.

A note on branching

Lakebase inherits Neon’s copy-on-write branching: you can create an instant, zero-copy clone of your database. That’s a useful trick here. Instead of pointing the replication at your production instance and adding read load to the same compute your application uses, create a branch and replicate from that. The branch is a consistent snapshot, the read traffic hits separate compute, and you tear the branch down when the load finishes. Nothing about the Sling side changes — you just point the host at the branch’s endpoint.

Verification

Trust the load, but check it. Run a count and a min/max against the target:

sling conns exec snowflake \
  "select count(*) as orders, min(ordered_at) as first_order, max(ordered_at) as last_order from demo_lakebase_snowflake.orders"
ORDERS  FIRST_ORDER                    LAST_ORDER
90000   2025-01-01 00:02:00 +0000 UTC  2025-03-04 12:01:00 +0000 UTC

And a sample:

sling conns exec snowflake \
  "select order_id, customer_id, quantity, amount, promo_code, ordered_at from demo_lakebase_snowflake.orders order by order_id limit 5"
ORDER_ID  CUSTOMER_ID  QUANTITY  AMOUNT     PROMO_CODE  ORDERED_AT
1         2            2         10.740000  PROMO1      2025-01-01 00:02:00 +0000 UTC
2         3            3         17.220000  PROMO2      2025-01-01 00:03:00 +0000 UTC
3         4            4         24.440000  PROMO3      2025-01-01 00:04:00 +0000 UTC
4         5            5         32.400000              2025-01-01 00:05:00 +0000 UTC
5         6            1         6.850000   PROMO5      2025-01-01 00:06:00 +0000 UTC

Row count matches the source, the timestamps survived the trip, and the decimal amounts kept their precision. Notice row 4: the promo_code is empty because it was null in the source, and that nullability carried across intact.

Type mapping

Postgres and Snowflake don’t share a type system, so Sling maps between them. Read the target schema to see the result:

sling conns exec snowflake \
  "select column_name, data_type from information_schema.columns
   where table_schema='DEMO_LAKEBASE_SNOWFLAKE' and table_name='ORDERS'
   order by ordinal_position"
COLUMN_NAME  DATA_TYPE
ORDER_ID     NUMBER
CUSTOMER_ID  NUMBER
PRODUCT_ID   NUMBER
QUANTITY     NUMBER
AMOUNT       NUMBER
PROMO_CODE   TEXT
ORDERED_AT   TIMESTAMP_NTZ

Postgres integers become Snowflake NUMBER, and numeric(10,2) also becomes NUMBER with its precision preserved, so the order amounts keep full decimal precision instead of collapsing into a float. varchar and text become TEXT, and timestamp becomes TIMESTAMP_NTZ. When a clean mapping isn’t available, Sling renders the value as a string rather than dropping precision silently, and you can override any column with a columns: block in the stream if you need a specific Snowflake type.

Incremental loads: only what changed

Full refresh is fine for a first load or a small dimension table. For anything that grows, you want to move only the new rows. Switch the mode to incremental and tell Sling which column tracks change and which column identifies a row:

# replication-incremental.yaml
source: lakebase
target: snowflake

defaults:
  mode: incremental
  object: demo_lakebase_snowflake.{stream_table}
  primary_key: [order_id]
  update_key: ordered_at

streams:
  demo_lakebase_snowflake.orders:

Say 2,500 new orders land in Lakebase. Re-run with the incremental file:

sling run -r replication-incremental.yaml
INF Sling Replication | lakebase -> snowflake | demo_lakebase_snowflake.orders
INF getting checkpoint value (ordered_at)
INF created table "DEMO_LAKEBASE_SNOWFLAKE"."ORDERS_TMP"
INF inserted 2500 rows into "DEMO_LAKEBASE_SNOWFLAKE"."ORDERS" in 10 secs [245 r/s] [152 kB]
INF execution succeeded

The line that does the work is getting checkpoint value (ordered_at). Sling reads the maximum ordered_at already in the Snowflake target, then pulls only source rows newer than that. There’s no separate state file to manage — the target table is the checkpoint. The primary_key lets Sling merge, so a row that was updated rather than inserted is upserted instead of duplicated.

Run it again with nothing new on the source and you get a clean no-op:

INF getting checkpoint value (ordered_at)
WRN no data or records found in stream. Nothing to do.
INF inserted 0 rows into "DEMO_LAKEBASE_SNOWFLAKE"."ORDERS" in 7 secs [0 r/s]
INF execution succeeded

Zero rows, no error. The warning is Sling telling you there was nothing past the checkpoint, which is exactly what you want from a scheduled job that fires on a cron whether or not there’s anything to move.

A final count confirms the math:

sling conns exec snowflake "select count(*) as orders, max(ordered_at) as last_order from demo_lakebase_snowflake.orders"
ORDERS  LAST_ORDER
92500   2025-03-06 05:41:00 +0000 UTC

90,000 from the first load plus 2,500 from the incremental run, with the high-water mark advanced to the newest order.

Replicating many tables at once

Listing every table by hand doesn’t scale past a handful. Use a wildcard in streams and the {stream_table} token in the target object to move a whole schema:

source: lakebase
target: snowflake

defaults:
  mode: incremental
  object: demo_lakebase_snowflake.{stream_table}
  primary_key: [id]
  update_key: updated_at

streams:
  demo_lakebase_snowflake.*:

Every table in the schema gets replicated, each into a Snowflake table of the same name. You can still override a single table by listing it explicitly below the wildcard — the explicit entry wins. This is the pattern to reach for when you’re mirroring a whole Lakebase database into the warehouse.

Scheduling

Once the YAML works, scheduling is a cron entry:

0 * * * * cd /path/to/configs && sling run -r replication-incremental.yaml >> /var/log/sling.log 2>&1

Because incremental mode tracks its checkpoint against the target, each run is idempotent — if one fails or is skipped, the next picks up exactly where the last left off. One thing to keep in mind on a schedule: use a native Postgres password for the Lakebase role, not an OAuth token. Tokens expire after an hour, and a job that runs overnight will hit a stale one.

If you’d rather not babysit cron, the Sling Platform handles scheduling, alerting, and run history, while keeping the data movement itself on your own infrastructure — nothing routes through a vendor control plane.

Conclusion

Because Lakebase is real Postgres, moving data out of it is not a special case. You register it as a postgres connection, point Sling at Snowflake, and write two YAML files: full-refresh for the first load, incremental for everything after. Sling loads into Snowflake in bulk, maps the types sensibly by default, and keeps the checkpoint in the target table so there’s no separate state store to lose.

The numbers here are from a real run: 108,000 rows on the first load in 40 seconds, 2,500 rows on the incremental in 10, a clean no-op when there’s nothing to move. Point Sling at your own Lakebase endpoint, ideally a branch, and you’ll get the same shape.

The Lakebase-to-Snowflake path is the standard-Postgres-to-Snowflake path, so these companion walkthroughs apply directly:

If you’re weighing Sling against a managed connector for this job, the Sling vs Fivetran comparison covers the tradeoffs. For the reasoning behind Sling’s single-binary, no-control-plane design, see the Sling blog.

Frequently asked questions

Does Sling need a special Lakebase connector?

No. Lakebase speaks the standard Postgres wire protocol on port 5432, so Sling connects to it with the ordinary postgres connection type. There’s nothing Lakebase-specific to configure beyond the hostname, credentials, and sslmode=require.

Should I use an OAuth token or a password for the Lakebase connection?

Use a native Postgres role and password. Lakebase’s OAuth tokens expire after an hour, which breaks scheduled or long-running replications. A native password doesn’t expire and works with connection pooling, so it’s the right fit for a data-movement tool.

Will replicating add load to my production Lakebase instance?

It can, since the read query runs against your instance’s compute. To avoid competing with application traffic, use Lakebase’s copy-on-write branching to create a zero-copy clone and replicate from the branch. The read load hits separate compute, and you delete the branch when the run finishes.

How are Postgres numeric, jsonb, and uuid columns handled in Snowflake?

numeric maps to Snowflake NUMBER with precision preserved, so decimal amounts don’t get flattened to floats. jsonb lands in a VARIANT column and stays queryable with : and FLATTEN(). uuid maps to TEXT by default; override it with a columns: block if you want a different type.

Can I do incremental Lakebase → Snowflake with deletes?

Sling’s incremental mode propagates inserts and updates via the update_key and merges on the primary_key, but it does not detect physical deletes in the source. For delete-aware syncs, soft-delete with a deleted_at column and filter in queries, run mode: full-refresh on small enough tables, or use mode: snapshot to keep historical versions.

Does the source have to be Lakebase specifically?

No — this exact configuration works against any Postgres 16 or 17 instance. Lakebase is just Postgres with serverless compute and branching, so if you later move the transactional store somewhere else, the replication YAML doesn’t change; only the connection host does.