<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shubham Baldava</title>
    <description>The latest articles on DEV Community by Shubham Baldava (@shubham_olake).</description>
    <link>https://dev.to/shubham_olake</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4212879%2Fd265c69e-976b-4b2b-a4ba-0d6c44484c21.png</url>
      <title>DEV Community: Shubham Baldava</title>
      <link>https://dev.to/shubham_olake</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shubham_olake"/>
    <language>en</language>
    <item>
      <title>Postgres to Snowflake with CDC: Open Source, No Kafka, No Stage</title>
      <dc:creator>Shubham Baldava</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:09:52 +0000</pubDate>
      <link>https://dev.to/shubham_olake/postgres-to-snowflake-with-cdc-open-source-no-kafka-no-stage-11a4</link>
      <guid>https://dev.to/shubham_olake/postgres-to-snowflake-with-cdc-open-source-no-kafka-no-stage-11a4</guid>
      <description>&lt;p&gt;Most Postgres to Snowflake pipelines are a cron job: dump a table every hour, push the file to a stage, &lt;code&gt;COPY INTO&lt;/code&gt; a Snowflake table. That is fine for a daily report. It breaks as soon as you need deletes, the latest version of a row within minutes, or the same data in a second engine without paying to copy it again.&lt;/p&gt;

&lt;p&gt;There are four common ways to move Postgres data into Snowflake:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual export:&lt;/strong&gt; &lt;code&gt;COPY&lt;/code&gt; to S3, then &lt;code&gt;COPY INTO&lt;/code&gt;. Cheap, but full reloads only, and deletes are lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debezium and Kafka with the Snowflake Kafka connector:&lt;/strong&gt; seconds of latency, but you run Kafka, Kafka Connect and a schema registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed ELT (Fivetran, Estuary, Airbyte Cloud):&lt;/strong&gt; nothing to run, but you pay per row or GB to land a second copy inside Snowflake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake Openflow:&lt;/strong&gt; Snowflake's own CDC connector, a good fit when Snowflake is the only consumer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This walkthrough builds a fifth option. Change data capture (CDC) reads the Postgres write-ahead log with &lt;a href="https://github.com/datazip-inc/olake" rel="noopener noreferrer"&gt;OLake&lt;/a&gt;, an open source (Apache 2.0) replication tool, and writes the rows as Iceberg tables straight into Snowflake's own catalog. Snowflake queries them as native managed tables, with no stage, no Snowpipe and no refresh job. The same tables stay readable from DuckDB or Spark.&lt;/p&gt;

&lt;p&gt;I ran every step below against a real Snowflake account. The numbers are from that run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; Postgres 16 in Docker with logical replication on, and two small tables (&lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline:&lt;/strong&gt; OLake running locally with Docker Compose. It takes a full snapshot first, then replays the write-ahead log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination:&lt;/strong&gt; Snowflake, with the tables landing as Snowflake-managed tables you query in Snowsight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You need:&lt;/strong&gt; Docker, a Snowflake account where you can use &lt;code&gt;ACCOUNTADMIN&lt;/code&gt; (a trial works), and about 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Postgres with logical replication
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;docker-compose.yml&lt;/code&gt; in an empty folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_cdc&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wal_level=logical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"max_replication_slots=10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"max_wal_senders=10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wal_sender_timeout=0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create the tables, a replication user, a publication and a slot. Run this with &lt;code&gt;psql -h localhost -U postgres -d shop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nf"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="kt"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Aarav Mehta'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'aarav@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mumbai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'gold'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Priya Sharma'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'priya@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Bengaluru'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'silver'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Liam Carter'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'liam@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Austin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Sofia Rossi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sofia@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Milan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'gold'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Kenji Tanaka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'kenji@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Tokyo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'silver'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Emma Schulz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'emma@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Berlin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Noah Kim'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'noah@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Seoul'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'gold'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Zara Ali'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'zara@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Dubai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'silver'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Laptop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Headphones'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Monitor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Keyboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mouse'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Webcam'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7919&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'delivered'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nf"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The user OLake connects as&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;olake&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="no"&gt;LOGIN&lt;/span&gt; &lt;span class="no"&gt;REPLICATION&lt;/span&gt; &lt;span class="no"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'olake_password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="no"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;olake&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="no"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;olake&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="no"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="no"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;olake&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- What OLake reads changes from&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;PUBLICATION&lt;/span&gt; &lt;span class="n"&gt;olake_pub&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nf"&gt;pg_create_logical_replication_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'olake_slot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pgoutput'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 One slot and one publication per pipeline. Two jobs sharing a slot each read part of the other's changes, and both lose data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few things that matter on a real database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres 10 or later.&lt;/strong&gt; Logical replication with the built-in &lt;code&gt;pgoutput&lt;/code&gt; plugin arrived in 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables without a primary key&lt;/strong&gt; need &lt;code&gt;ALTER TABLE &amp;lt;table&amp;gt; REPLICA IDENTITY FULL;&lt;/code&gt;, or their updates and deletes don't carry the old row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The slot holds WAL until it's read.&lt;/strong&gt; Pause the pipeline for a week and Postgres keeps a week of WAL on disk. Set &lt;code&gt;max_slot_wal_keep_size&lt;/code&gt; so a stalled slot is dropped before the disk fills, and alert on &lt;code&gt;pg_replication_slots.wal_status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wal_sender_timeout=0&lt;/code&gt;&lt;/strong&gt; stops a long first snapshot from being cut off. It's in the compose file above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On RDS, Aurora, Cloud SQL or Azure you set &lt;code&gt;wal_level&lt;/code&gt; through the parameter group instead of the command line. Everything else is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Prepare Snowflake
&lt;/h2&gt;

&lt;p&gt;Run this once in a Snowsight worksheet as &lt;code&gt;ACCOUNTADMIN&lt;/code&gt;. It creates the database the tables land in, a role and a service user for OLake, and the token OLake logs in with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="no"&gt;USE&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;ACCOUNTADMIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="no"&gt;OLAKE_WH&lt;/span&gt; &lt;span class="no"&gt;WAREHOUSE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;XSMALL&lt;/span&gt; &lt;span class="no"&gt;AUTO_SUSPEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="no"&gt;AUTO_RESUME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="no"&gt;CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SNOWFLAKE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="no"&gt;EXTERNAL_VOLUME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SNOWFLAKE_MANAGED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="no"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="no"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="no"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_WH&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;SYSADMIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;USER&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="no"&gt;OLAKE_SVC&lt;/span&gt; &lt;span class="no"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SERVICE&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="no"&gt;ROLE&lt;/span&gt; &lt;span class="no"&gt;OLAKE_ROLE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="no"&gt;USER&lt;/span&gt; &lt;span class="no"&gt;OLAKE_SVC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- A token for a service user needs a network policy. Allow the IP OLake connects from.&lt;/span&gt;
&lt;span class="no"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PUBLIC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;NETWORK&lt;/span&gt; &lt;span class="no"&gt;RULE&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;olake_ip&lt;/span&gt; &lt;span class="no"&gt;MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INGRESS&lt;/span&gt; &lt;span class="no"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IPV4&lt;/span&gt; &lt;span class="no"&gt;VALUE_LIST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;your public IP&amp;gt;/32'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;NETWORK&lt;/span&gt; &lt;span class="no"&gt;POLICY&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;olake_network_policy&lt;/span&gt; &lt;span class="no"&gt;ALLOWED_NETWORK_RULE_LIST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OLAKE_DEMO.PUBLIC.olake_ip'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="no"&gt;USER&lt;/span&gt; &lt;span class="no"&gt;OLAKE_SVC&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="no"&gt;NETWORK_POLICY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;olake_network_policy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="no"&gt;CURRENT_ORGANIZATION_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;CURRENT_ACCOUNT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;account_identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CURRENT_REGION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="no"&gt;USER&lt;/span&gt; &lt;span class="no"&gt;OLAKE_SVC&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="no"&gt;PROGRAMMATIC&lt;/span&gt; &lt;span class="no"&gt;ACCESS&lt;/span&gt; &lt;span class="no"&gt;TOKEN&lt;/span&gt; &lt;span class="no"&gt;OLAKE_PAT&lt;/span&gt; &lt;span class="no"&gt;ROLE_RESTRICTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OLAKE_ROLE'&lt;/span&gt; &lt;span class="no"&gt;DAYS_TO_EXPIRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;curl -s https://checkip.amazonaws.com&lt;/code&gt; prints your public IP. Keep the &lt;code&gt;account_identifier&lt;/code&gt;, the &lt;code&gt;region&lt;/code&gt; and the token from the last statement. Snowflake shows the token only once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Skip the network policy and the last line fails with &lt;code&gt;Network Policy is required when creating a programmatic access token for user OLAKE_SVC&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two &lt;code&gt;ALTER DATABASE&lt;/code&gt; lines do the important part. They make every table OLake creates a Snowflake-managed table on Snowflake's own storage. That is why there is no stage, no external volume and no &lt;code&gt;CREATE TABLE&lt;/code&gt; later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run OLake
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/datazip-inc/olake-ui/master/docker-compose-v1.yml &lt;span class="o"&gt;|&lt;/span&gt; docker compose -f - up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8000&lt;/code&gt; and sign in with &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;password&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source.&lt;/strong&gt; Go to Sources, then Create Source, then &lt;strong&gt;Postgres&lt;/strong&gt;. Leave &lt;strong&gt;Enable CDC&lt;/strong&gt; on and enter &lt;code&gt;olake_slot&lt;/code&gt; as the &lt;strong&gt;Replication Slot&lt;/strong&gt; and &lt;code&gt;olake_pub&lt;/code&gt; as the &lt;strong&gt;Publication&lt;/strong&gt;. For the connection, use host &lt;code&gt;host.docker.internal&lt;/code&gt; (the containers reach your machine through it), port &lt;code&gt;5432&lt;/code&gt;, database &lt;code&gt;shop&lt;/code&gt;, user &lt;code&gt;olake&lt;/code&gt; and password &lt;code&gt;olake_password&lt;/code&gt;. Click Create. OLake checks &lt;code&gt;wal_level&lt;/code&gt;, the replication privilege, the slot and the publication, and tells you which one is missing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmz4lufr3qjyxt46luxv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmz4lufr3qjyxt46luxv.webp" alt="OLake Create Source form for Postgres with Enable CDC on, replication slot olake_slot and publication olake_pub" width="800" height="722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqtmeii4bpg9iho5wr3n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqtmeii4bpg9iho5wr3n.webp" alt="OLake source test: connection successful, 4 of 4 required CDC checks passed" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destination.&lt;/strong&gt; Go to Destinations, then Create Destination, then &lt;strong&gt;Apache Iceberg&lt;/strong&gt;, then catalog type &lt;strong&gt;Horizon&lt;/strong&gt;, then &lt;strong&gt;Programmatic Access Token (PAT)&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Horizon Iceberg REST Catalog URI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://&amp;lt;account_identifier&amp;gt;.snowflakecomputing.com/polaris/api/catalog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Database&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OLAKE_DEMO&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Programmatic Access Token&lt;/td&gt;
&lt;td&gt;the token from step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Role Scope&lt;/td&gt;
&lt;td&gt;&lt;code&gt;session:role:OLAKE_ROLE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage Region&lt;/td&gt;
&lt;td&gt;the region from step 2, for example &lt;code&gt;AWS_AP_SOUTH_1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv5i30xbcdw3jj64vb4z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv5i30xbcdw3jj64vb4z.webp" alt="OLake destination form for the Snowflake Horizon catalog with programmatic access token fields" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job.&lt;/strong&gt; Go to Jobs, then Create Job. Name it &lt;code&gt;pg_to_snowflake&lt;/code&gt;, pick the source and destination, and under Advanced Settings set &lt;strong&gt;Target Query Engines&lt;/strong&gt; to Snowflake, plus DuckDB and Apache Spark if they'll read the data too. That setting matters: OLake picks the format for updates and deletes that every listed engine can read. Leave it empty and Snowflake won't see your updates. On the Streams page tick both tables and keep &lt;strong&gt;Full Refresh + CDC&lt;/strong&gt;. Then click Create Job and Sync Now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlr7kaijsxpzn09uc3kh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlr7kaijsxpzn09uc3kh.webp" alt="OLake Create Job page with target query engines set" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5q1zc6uvpj5xnvvo60hc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5q1zc6uvpj5xnvvo60hc.webp" alt="OLake Streams page with customers and orders selected, Full Refresh + CDC and Upsert" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Query it in Snowflake
&lt;/h2&gt;

&lt;p&gt;In my run the first sync copied 8 customers and 40 orders in &lt;strong&gt;41 seconds&lt;/strong&gt;, container start included. The tables show up right away:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1nwbam3j90hhm5ibp3r8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1nwbam3j90hhm5ibp3r8.webp" alt="OLake job history: first sync 41 seconds, second sync 23 seconds" width="800" height="203"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="no"&gt;SHOW&lt;/span&gt; &lt;span class="no"&gt;ICEBERG&lt;/span&gt; &lt;span class="no"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"pg_to_snowflake_shop_public"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_op_type"&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="no"&gt;OLAKE_DEMO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"pg_to_snowflake_shop_public"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"customers"&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SHOW ICEBERG TABLES&lt;/code&gt; reports &lt;code&gt;iceberg_table_type = MANAGED&lt;/code&gt;. Snowflake owns these tables, and there is nothing to refresh.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fraqzwhj4fhmnf87240hb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fraqzwhj4fhmnf87240hb.webp" alt="Snowsight SHOW ICEBERG TABLES listing customers and orders as MANAGED tables on SNOWFLAKE_MANAGED storage" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb51cr6p5mcdu38rno2w5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb51cr6p5mcdu38rno2w5.webp" alt="Snowsight query on the replicated customers table" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Quote the names. OLake keeps the lowercase Postgres names, and Snowflake upper-cases anything unquoted. That turns into &lt;code&gt;Object 'OLAKE_DEMO.PG_TO_SNOWFLAKE_SHOP_PUBLIC.CUSTOMERS' does not exist or not authorized.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Change rows and sync again
&lt;/h2&gt;

&lt;p&gt;This is the part a cron job gets wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Maya Patel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'maya@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Pune'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'silver'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Lucas Silva'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'lucas@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'São Paulo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Laptop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1299.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mouse'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;25.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Monitor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;349.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Keyboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;89.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Webcam'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;79.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'gold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Liam Carter'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Abu Dhabi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Zara Ali'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'delivered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run Sync Now again. The second sync read &lt;strong&gt;12 change events in 23 seconds&lt;/strong&gt; from the replication slot. For &lt;code&gt;customers&lt;/code&gt; it wrote one data file with 4 rows (the 2 inserts and the new versions of the 2 updated rows), plus one small delete file that retires the 2 old versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmckbx88rqwowu8s3ln41.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmckbx88rqwowu8s3ln41.webp" alt="OLake sync log commit report: addedDataFiles 1, addedRecords 4, addedPositionalDeletes 2" width="800" height="251"&gt;&lt;/a&gt;&lt;br&gt;
 Re-run the &lt;code&gt;SELECT&lt;/code&gt;: Liam is &lt;code&gt;gold&lt;/code&gt;, Zara is in Abu Dhabi, and &lt;code&gt;_op_type&lt;/code&gt; shows &lt;code&gt;c&lt;/code&gt; for the inserts and &lt;code&gt;u&lt;/code&gt; for the updates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8l2zremn41c96phiaq9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8l2zremn41c96phiaq9.webp" alt="Snowsight customers after the change sync: u for the updated rows, c for the new rows" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deletes work the same way. &lt;code&gt;DELETE FROM customers WHERE id = 6;&lt;/code&gt; and a sync leave a row with &lt;code&gt;_op_type = 'd'&lt;/code&gt;, so filter with &lt;code&gt;WHERE "_op_type" &amp;lt;&amp;gt; 'd'&lt;/code&gt; for live rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema changes.&lt;/strong&gt; When you add a column in Postgres, OLake adds it to the table on the next sync and Snowflake shows it. Widening changes (&lt;code&gt;int&lt;/code&gt; to &lt;code&gt;bigint&lt;/code&gt;) are applied. Narrowing or incompatible changes fail the sync instead of corrupting the table.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the same tables from DuckDB
&lt;/h2&gt;

&lt;p&gt;The tables are open Apache Iceberg tables in Snowflake's catalog, so another engine can read the same files without an export or a second copy. DuckDB wants a short-lived bearer token instead of the raw access token, so exchange it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s -X POST &lt;span class="s2"&gt;"https://&amp;lt;account_identifier&amp;gt;.snowflakecomputing.com/polaris/api/catalog/v1/oauth/tokens"&lt;/span&gt; \
  -H &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; \
  --data-urlencode &lt;span class="nv"&gt;grant_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client_credentials \
  --data-urlencode &lt;span class="nv"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;session:role:OLAKE_ROLE \
  --data-urlencode &lt;span class="nv"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;your token&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="no"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="no"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="no"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="no"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;SECRET&lt;/span&gt; &lt;span class="n"&gt;horizon_secret&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TOKEN&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;access_token from the curl call&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="no"&gt;ATTACH&lt;/span&gt; &lt;span class="s1"&gt;'OLAKE_DEMO'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;horizon&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="no"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;ENDPOINT&lt;/span&gt; &lt;span class="s1"&gt;'https://&amp;lt;account_identifier&amp;gt;.snowflakecomputing.com/polaris/api/catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;SECRET&lt;/span&gt; &lt;span class="n"&gt;horizon_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;ACCESS_DELEGATION_MODE&lt;/span&gt; &lt;span class="s1"&gt;'vended_credentials'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;horizon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_to_snowflake_shop_public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxas51h15lt5p82x3o2i9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxas51h15lt5p82x3o2i9.webp" alt="DuckDB 1.5.5 reading the Snowflake-managed customers table and returning the same 10 rows" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DuckDB 1.5.5 returned the same 10 rows as Snowflake, and Spark 3.5.6 did too. An &lt;code&gt;orders&lt;/code&gt; roll-up by status matched Postgres to the cent in all three engines. The DuckDB path needs a Snowflake account on AWS or GCP. The token is valid for about an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data types to know about
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;Snowflake shows&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;serial&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NUMBER(10,0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NUMBER(19,0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;numeric&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FLOAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stored as doubles; round when you need exact cents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;timestamptz&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TIMESTAMP_LTZ(6)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;jsonb&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, arrays&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VARCHAR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;use &lt;code&gt;PARSE_JSON()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In my run the &lt;code&gt;orders.amount&lt;/code&gt; totals still matched Postgres to the cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors you might hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FATAL: must be superuser or replication role to start walsender&lt;/code&gt;.&lt;/strong&gt; The OLake user can't replicate. Run &lt;code&gt;ALTER ROLE olake WITH REPLICATION;&lt;/code&gt;. On RDS and Aurora, grant &lt;code&gt;rds_replication&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Object '...CUSTOMERS' does not exist or not authorized&lt;/code&gt;.&lt;/strong&gt; The name wasn't quoted; see step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The destination test fails.&lt;/strong&gt; The token exchange with Snowflake is failing. Check, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The service user has the network policy, and OLake's IP is in the rule.&lt;/li&gt;
&lt;li&gt;The scope is &lt;code&gt;session:role:OLAKE_ROLE&lt;/code&gt;, matching the token's &lt;code&gt;ROLE_RESTRICTION&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The token hasn't expired. An expired token fails with &lt;code&gt;Programmatic access token is expired.&lt;/code&gt;; add a new one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running the &lt;code&gt;curl&lt;/code&gt; exchange above shows the same error without the UI in the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snowflake shows stale rows after updates.&lt;/strong&gt; The job was created without a target query engine, so it wrote a delete format Snowflake can't read. Set Snowflake as a target engine on the job, and the next sync rewrites the delete files.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Deletes&lt;/th&gt;
&lt;th&gt;Where the data lands&lt;/th&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;COPY&lt;/code&gt; to S3 plus &lt;code&gt;COPY INTO&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;hours to daily&lt;/td&gt;
&lt;td&gt;no, full reloads&lt;/td&gt;
&lt;td&gt;Snowflake tables&lt;/td&gt;
&lt;td&gt;a script and a scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debezium, Kafka and the Snowflake connector&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Kafka, then Snowflake tables&lt;/td&gt;
&lt;td&gt;Kafka, Connect, Debezium, schema registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fivetran, Estuary, Airbyte Cloud&lt;/td&gt;
&lt;td&gt;minutes, by plan&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Snowflake tables (a second copy)&lt;/td&gt;
&lt;td&gt;nothing; per-row or per-GB pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Openflow&lt;/td&gt;
&lt;td&gt;CDC&lt;/td&gt;
&lt;td&gt;yes, as soft deletes&lt;/td&gt;
&lt;td&gt;native Snowflake tables&lt;/td&gt;
&lt;td&gt;an Openflow runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OLake to Snowflake's catalog (this post)&lt;/td&gt;
&lt;td&gt;CDC, schedule down to 1 minute&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Snowflake-managed tables that DuckDB and Spark can read too&lt;/td&gt;
&lt;td&gt;one Docker Compose stack, open source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Snowflake is the only thing that will ever read this data, Openflow or a managed tool is the shorter path. The approach above pays off when a second engine needs the same rows, or you want your data in an open format instead of paying to copy it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Point the source at your real database. RDS, Aurora, Cloud SQL and Azure only differ in how you set &lt;code&gt;wal_level&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add tables with &lt;code&gt;ALTER PUBLICATION olake_pub ADD TABLE ...&lt;/code&gt;, then tick them in the job.&lt;/li&gt;
&lt;li&gt;Want the files in your own S3 bucket instead of Snowflake storage? Use AWS Glue as the catalog. Snowflake then reads the tables through a catalog integration, and Athena and EMR can read them too.&lt;/li&gt;
&lt;li&gt;The full guide, with the Glue setup and the Spark connection, is on &lt;a href="https://olake.io/docs/integrations/postgres-to-snowflake/" rel="noopener noreferrer"&gt;olake.io&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a step doesn't match what you see, say so in the comments and I'll update the post.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>snowflake</category>
      <category>dataengineering</category>
      <category>cdc</category>
    </item>
  </channel>
</rss>
