Skip to content
Home » Blog » Testing Heterogeneous CDC? Skip the Setup Pain with One-Command Deployment

Testing Heterogeneous CDC? Skip the Setup Pain with One-Command Deployment

We’ve All Been There

It’s Friday afternoon. Your manager asks: “Can we replicate data from our Oracle database to PostgreSQL? I need to know by Monday.”

Sure, no problem. You’ve done this before. You start listing what you need:

  • Set up Oracle with CDC enabled (there goes Friday evening)
  • Install PostgreSQL (okay, that’s quick)
  • Configure the replication tool (Saturday morning?)
  • Add monitoring because you’re not flying blind (Saturday afternoon)
  • Debug the inevitable “connection refused” errors (Sunday? Please not Sunday…)
  • Actually test if the replication works (Monday morning, if you’re lucky)

By the time you’re done, you’ve burned through your weekend, and you haven’t even started testing your actual use case.

There has to be a better way.

What is SynchDB?

Before we dive into the solution, let’s talk about what SynchDB actually is.

SynchDB is a PostgreSQL extension that replicates data from other databases directly into PostgreSQL. No middleware, no Kafka clusters, no separate services to manage. It runs inside PostgreSQL itself.

Think of it this way: you install an extension (like you would install PostGIS or pg_cron), configure which tables you want to replicate, and SynchDB handles the rest. It captures changes from your source databases in real-time and writes them directly to PostgreSQL tables.

What Makes SynchDB Different?

Most replication tools follow this architecture:

Source DB → Middleware (Debezium/Kafka/etc) → Target DB

SynchDB simplifies this to:

Source DB → PostgreSQL (with SynchDB)

Here’s what it supports:

Source Databases:

  • MySQL (any reasonably recent version)
  • Microsoft SQL Server
  • Oracle (both legacy and modern versions)

Replication Features:

  • Initial snapshot (copies existing data)
  • Change Data Capture (streams ongoing changes)
  • DDL replication (schema changes propagate automatically)
  • Data type mapping and transformation
  • Multiple sources into a single PostgreSQL instance

Why developers like it:

  • No JVM to manage (well, except for the embedded Debezium engine, but you don’t need to tune it)
  • No external dependencies to deploy
  • PostgreSQL’s reliability for the entire pipeline
  • Can transform data using SQL as it arrives (because it’s PostgreSQL!)

The project is open source, actively developed, and used in production by companies migrating off Oracle or consolidating data from multiple database systems.

Now, here’s the catch: even though SynchDB itself is relatively straightforward, setting up a proper test environment with source databases, monitoring, and everything configured correctly? That’s still a pain.

Until now.

Enter ezdeploy.sh

Someone on the SynchDB team apparently got tired of setting up test environments (we’ve all been there), so they built ezdeploy.sh for the 1.2 release in September.

Here’s the idea: one script that deploys everything you need to test CDC replication. Source database, target PostgreSQL with SynchDB, monitoring tools, sample data – the whole stack.

I tried it last week, and honestly, I was skeptical. “One-command deployment” tools usually mean “one command that fails in seven different ways.” But this one actually works.

SynchDB Test: MySQL to PostgreSQL

Instead of telling you what it does, let me show you what happened when I ran it.

I wanted to test replicating a MySQL database. Here’s what I did:

git clone https://github.com/Hornetlabs/synchdb.git

./ezdeploy.sh

The script greeted me with a menu:

----------------------------------
-----> Welcome to ezdeploy! <-----
----------------------------------

please select a quick deploy option:
         1) synchdb only
         2) synchdb + mysql
         3) synchdb + sqlserver
         4) synchdb + oracle23ai
         5) synchdb + oracle19c
         6) synchdb + olr(oracle19c)
         7) synchdb + all source databases
         8) custom deployment
         9) deploy monitoring
        10) teardown deployment
enter your selection: 

I typed 2 for MySQL and hit enter.

The script started pulling Docker images. About three minutes later (depending on your internet connection), I had:

  • MySQL running with binary logging enabled
  • PostgreSQL 17 with SynchDB 1.2 installed
  • Sample tables with data already loaded

No configuration files to edit. No “wait, which port was that again?” No googling error messages.

Then I ran it again and selected option 9 to add monitoring. Opened Grafana at localhost:3000, logged in with default credentials (admin/admin) and there were already dashboards set up showing replication metrics. There were no data to show yet, because we haven’t given any data to it. Let’s run some tests here!

docker ps
CONTAINER ID   IMAGE                                COMMAND                  CREATED          STATUS          PORTS                                       NAMES
389cdcffca87   grafana/grafana:latest               "/run.sh"                9 minutes ago    Up 9 minutes    0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   grafana
43a303129832   prom/prometheus:latest               "/bin/prometheus --c…"   9 minutes ago    Up 9 minutes    0.0.0.0:9090->9090/tcp, :::9090->9090/tcp   prometheus
05c8257c0d01   quay.io/debezium/example-mysql:2.6   "docker-entrypoint.s…"   10 minutes ago   Up 10 minutes                                               mysql
211a1760ec88   hgneon/synchdbtest:pg17              "/usr/local/bin/init…"   10 minutes ago   Up 10 minutes                                               synchdb

Login to PostgreSQL and Enable SynchDB

docker exec -it synchdb bash -c "psql -d postgres"
postgres=# CREATE EXTENSION synchdb CASCADE;
NOTICE:  installing required extension "pgcrypto"
CREATE EXTENSION

Here you have it, a PostgreSQL service with SynchDB enabled. Now, we are ready to create a MySQL connector and have it synchronize with the example MySQL server.

SELECT synchdb_add_conninfo('mysqlconn',
                            'mysql',
                            3306,
                            'mysqluser',
                            'mysqlpwd',
                            'inventory',
                            'postgres',
                            'null',
                            'null',
                            'mysql');

We have also enabled prometheus and grafana monitoring options via ezdeploy, so, don’t forget to enable JMX exporter on this connector. jmx_prometheus_javaagent-1.3.0.jar and jmxexport.conf are required to be loaded to the connector to export metrics to prometheus. Luckily, the SynchDB image comes with them and keeping them default should suffice for our tests.

SELECT synchdb_add_jmx_exporter_conninfo(
                            'mysqlconn',
                            '/home/ubuntu/jmx_prometheus_javaagent-1.3.0.jar',
                            9404,
                            '/home/ubuntu/jmxexport.conf');

Get the Connector Rolling

Now, we are ready to start the MySQL connector that we have just started. All it takes is one single command to have the connector to start rolling!

SELECT synchdb_start_engine_bgw('mysqlconn');

How do I know if there is something going on? I ain’t gonna always check the repetitive logs for some activities. The SynchDB team has put up some views to show the current state of the connector, how many change events it has processed and how it converts the external table schemas to PostgreSQL

select * from synchdb_state_view;
   name    | connector_type | pid |      stage       |  state  |   err    |       last_dbz_offset
-----------+----------------+-----+------------------+---------+----------+-----------------------------
 mysqlconn | mysql          |  65 | initial snapshot | polling | no error | offset file not flushed yet
(1 row)

Well, from the state view, I can at least know the connector is polling with no error and running as pid = 65!

postgres=# select * from synchdb_stats_view;
   name    | ddls | dmls | reads | creates | updates | deletes | bad_events | total_events | batches_done | avg_batch_size | first_src_ts | first_dbz_ts | f
irst_pg_ts |  last_src_ts  |  last_dbz_ts  |  last_pg_ts
-----------+------+------+-------+---------+---------+---------+------------+--------------+--------------+----------------+--------------+--------------+--
-----------+---------------+---------------+---------------
 mysqlconn |   16 |   36 |    36 |       0 |       0 |       0 |          4 |           52 |            1 |             52 |            0 |            0 |
         0 | 1759950734000 | 1759950734652 | 1759950735065
(1 row)

and it has processed 16 DDLs and 36 DMLs events from the statistics view.

postgres=# select * from synchdb_att_view;
   name    | type  | attnum |         ext_tbname         |         pg_tbname          | ext_attname  |  pg_attname  | ext_atttypename |  pg_atttypename   |
transform
-----------+-------+--------+----------------------------+----------------------------+--------------+--------------+-----------------+-------------------+-
----------
 mysqlconn | mysql |      1 | inventory.addresses        | inventory.addresses        | id           | id           | int             | integer           |
 mysqlconn | mysql |      2 | inventory.addresses        | inventory.addresses        | customer_id  | customer_id  | int             | integer           |
 mysqlconn | mysql |      3 | inventory.addresses        | inventory.addresses        | street       | street       | varchar         | character varying |
 mysqlconn | mysql |      4 | inventory.addresses        | inventory.addresses        | city         | city         | varchar         | character varying |
 mysqlconn | mysql |      5 | inventory.addresses        | inventory.addresses        | state        | state        | varchar         | character varying |
 mysqlconn | mysql |      6 | inventory.addresses        | inventory.addresses        | zip          | zip          | varchar         | character varying |
 mysqlconn | mysql |      7 | inventory.addresses        | inventory.addresses        | type         | type         | enum            | text              |
 mysqlconn | mysql |      1 | inventory.customers        | inventory.customers        | id           | id           | int             | integer           |
 mysqlconn | mysql |      2 | inventory.customers        | inventory.customers        | first_name   | first_name   | varchar         | character varying |
 mysqlconn | mysql |      3 | inventory.customers        | inventory.customers        | last_name    | last_name    | varchar         | character varying |
 mysqlconn | mysql |      4 | inventory.customers        | inventory.customers        | email        | email        | varchar         | character varying |
 mysqlconn | mysql |      1 | inventory.geom             | inventory.geom             | id           | id           | int             | integer           |
 mysqlconn | mysql |      2 | inventory.geom             | inventory.geom             | g            | g            | geometry        | text              |
 mysqlconn | mysql |      3 | inventory.geom             | inventory.geom             | h            | h            | geometry        | text              |
 mysqlconn | mysql |      1 | inventory.orders           | inventory.orders           | order_number | order_number | int             | integer           |
 mysqlconn | mysql |      2 | inventory.orders           | inventory.orders           | order_date   | order_date   | date            | date              |
 mysqlconn | mysql |      3 | inventory.orders           | inventory.orders           | purchaser    | purchaser    | int             | integer           |
 mysqlconn | mysql |      4 | inventory.orders           | inventory.orders           | quantity     | quantity     | int             | integer           |
 mysqlconn | mysql |      5 | inventory.orders           | inventory.orders           | product_id   | product_id   | int             | integer           |
 mysqlconn | mysql |      1 | inventory.products         | inventory.products         | id           | id           | int             | integer           |
 mysqlconn | mysql |      2 | inventory.products         | inventory.products         | name         | name         | varchar         | character varying |
 mysqlconn | mysql |      3 | inventory.products         | inventory.products         | description  | description  | varchar         | character varying |
 mysqlconn | mysql |      4 | inventory.products         | inventory.products         | weight       | weight       | float           | real              |
 mysqlconn | mysql |      1 | inventory.products_on_hand | inventory.products_on_hand | product_id   | product_id   | int             | integer           |
 mysqlconn | mysql |      2 | inventory.products_on_hand | inventory.products_on_hand | quantity     | quantity     | int             | integer           |
(25 rows)

It is very nice that the attribute view provides a side-by-side view to show how foreign tables are mapped to PostgreSQL and the data type translation choices. This view is really useful for me to see what is going on with SynchDB.

Where is the Data?

We know where the data is, synchdb_att_view has already told us. the database called inventory has been mapped as a schema also called inventory in PostgreSQL. All the tables and example data have been replicated unedr it.

postgres=# select * from inventory.orders;
 order_number | order_date | purchaser | quantity | product_id
--------------+------------+-----------+----------+------------
        10001 | 2016-01-16 |      1001 |        1 |        102
        10002 | 2016-01-17 |      1002 |        2 |        105
        10003 | 2016-02-19 |      1002 |        2 |        106
        10004 | 2016-02-21 |      1003 |        1 |        107
(4 rows)

postgres=# select * from inventory.products;
 id  |        name        |                       description                       | weight
-----+--------------------+---------------------------------------------------------+--------
 101 | scooter            | Small 2-wheel scooter                                   |   3.14
 102 | car battery        | 12V car battery                                         |    8.1
 103 | 12-pack drill bits | 12-pack of drill bits with sizes ranging from #40 to #3 |    0.8
 104 | hammer             | 12oz carpenter's hammer                                 |   0.75
 105 | hammer             | 14oz carpenter's hammer                                 |  0.875
 106 | hammer             | 16oz carpenter's hammer                                 |      1
 107 | rocks              | box of assorted rocks                                   |    5.3
 108 | jacket             | water resistent black wind breaker                      |    0.1
 109 | spare tire         | 24 inch spare tire                                      |   22.2
(9 rows)

Time to Check the Dashboard

I logged in to Grafana platform with default credentials (admin/admin), navigate to MySQL dashboard template that comes preloaded (very convenient), and I can already see some replication metrics displayed here.

Oh, and don’t forget the change the instance to “synchdb:9404” as port 9404 is the port number I have selected when I configured the connector’s monitoring options via synchdb_add_jmx_exporter_conninfo.

Is Testing Done?

Not quite. In fact, we have not started any testing yet. Here’s what we have done so far:

  • deploy all the enviornment with ezdeploy
  • install SynchDB and create a MySQL connector + JMX exporter options
  • start the MySQL connector
  • observe some metrics, replicated tables, initial data, and views to confirm that the connector is doing some work.

It is now the time to simulate some INSERT, UPDATE and DELETEs or CREATE new tables on this running connector to see if it can do real-time Change Data Capature (CDC).

Test CDC Functionalities

SynchDB’s ezdeploy documentation page provides these handy docker exec commands to do INSERT, UPDATE and DELETE to a running MySQL, or other database types based on what you have deployed:

# INSERT
docker exec -i mysql mysql -D inventory -umysqluser -pmysqlpwd -e "INSERT INTO orders(order_date, purchaser, quantity, product_id) VALUES ('2025-12-12', 1002, 10000, 102)"

# UPDATE
docker exec -i mysql mysql -D inventory -umysqluser -pmysqlpwd -e "UPDATE orders SET quantity = 1000 WHERE order_number = 10002"

# DELETE
docker exec -i mysql mysql -D inventory -umysqluser -pmysqlpwd -e "DELETE FROM orders WHERE order_number = 10003"

Now, let’s go back to PostgreSQL and check this orders table. We will see that a new row with order_number 10005 is inserted, quantity for order_number 10002 updated and row with order_number 10003 is deleted correctly.

postgres=# select * from inventory.orders;
 order_number | order_date | purchaser | quantity | product_id
--------------+------------+-----------+----------+------------
        10001 | 2016-01-16 |      1001 |        1 |        102
        10004 | 2016-02-21 |      1003 |        1 |        107
        10005 | 2025-12-12 |      1002 |    10000 |        102
        10002 | 2016-01-17 |      1002 |     1000 |        105
(4 rows)

This demonstrates the basic CDC feature of SynchDB where it will stream any changes incurred on MySQL, transform and apply them to PostgreSQL!

SynchDB can also handle DDL commands!

# CREATE TABLE
docker exec -i mysql mysql -D inventory -umysqluser -pmysqlpwd -e "CREATE TABLE mytable (a INT PRIMARY KEY, b varchar(128))"

and a new table is created in PostgreSQL

postgres=# \d inventory.mytable
                            Table "inventory.mytable"
 Column |          Type          | Collation | Nullable |         Default
--------+------------------------+-----------+----------+-------------------------
 a      | integer                |           | not null |
 b      | character varying(128) |           |          | NULL::character varying
Indexes:
    "mytable_pkey" PRIMARY KEY, btree (a)

Of course, we can test more complex tables with more complex data types and operations. Treat it like a data playground for SynchDB prepared all by ezdeploy!

Other Database Type Test to postgreSQL

To test SynchDB with other databases, the procedures are very similar. Launch ezdeploy to deploy one or more database instances and create a matching connectors for each. Yes, SynchDB can support multiple connectors running at the same time and you can navigate to Grafana dashboards to see a graphical metrics view.

What Actually Gets Deployed

When you run ezdeploy.sh, it spins up Docker containers based on what you select:

Source Database Options:

  • Option 1: Just SynchDB (no source, for when you want to connect to your own database)
  • Option 2: MySQL 8.0 with binlog enabled
  • Option 3: SQL Server 2022 with CDC configured
  • Option 4: Oracle 23ai (the new long-term support release)
  • Option 5: Oracle 19c (if you’re stuck in enterprise-land like most of us)
  • Option 6: Oracle 19c + Openlog Replicator (native Oracle CDC streaming)
  • Option 7: All source databases at once (MySQL + SQL Server + Oracle)
  • Option 8: Custom deployment (lets you pick and choose)

Target Environment (always deployed):

  • PostgreSQL 17 with SynchDB 1.2 pre-compiled and ready
  • Sample schemas so you can start testing immediately

Monitoring Stack (option 9):

  • Prometheus scraping metrics from everything
  • Grafana with pre-built dashboards showing replication lag, throughput, errors, the usual stuff you actually care about

Cleanup (option 10):

  • Tears down the entire deployment when you’re done

The nice thing is that it’s all pre-configured. The databases talk to each other, the monitoring works, and you can start testing your actual scenarios right away.

What I Learned Using It

The Interactive Menu is Actually Nice

At first, I thought “why not just use command-line flags?” But having the menu is actually better when you’re learning. You can see all the options at once. You don’t need to remember the syntax. And if you’re showing this to someone else, the menu is self-documenting.

Deploy Once, Add Components Later

You don’t have to deploy everything at once. I usually start with just the source database I need, test my connectors, and then add monitoring when I need to dig into performance. The script is smart enough to not redeploy existing containers.

It’s Not Magic (But It’s Close)

The script uses Docker Compose under the hood. If something breaks, you can look at the generated docker-compose files and figure out what’s going on. It’s not a black box.

Resource Usage Matters

Running Oracle in Docker is heavy. If you’re on a laptop, maybe think twice before selecting option 7 (all sources). I learned this the hard way when my MacBook fan sounded like it was trying to achieve liftoff.

The Sample Data is Actually Useful

The script loads sample data into the source databases. At first, I thought “I’ll just use my own data.” But the sample data is actually helpful for quick tests. It has the right structure to demonstrate different replication scenarios (foreign keys, different data types, etc).

Always Deploy Monitoring

When I first tried it, I skipped the monitoring to save some resources. Bad idea. When something went wrong (connector stopped, replication lag increased), I had no visibility into what was happening. Always run the script again and add monitoring (option 9).

Try It Yourself

Here’s what I’d recommend if you want to give it a shot:

  1. Make sure you have Docker installed. That’s the only real prerequisite. Docker Desktop works fine on Mac and Windows.
  2. Clone the repo:
  3. git clone https://github.com/Hornetlabs/synchdb.git
  4. cd synchdb/tools
  5. Run the script:
  6. ./ezdeploy.sh
  1. Start with option 2 (MySQL – it’s the quickest to deploy).
  2. Run the script again and select option 9 to add monitoring.
  3. Open Grafana at http://localhost:3000 (admin/admin) and watch the dashboards while you make changes to the MySQL database. It’s satisfying to see the data flow in real-time.
  4. Read the SynchDB docs to understand what connectors can do: https://docs.synchdb.com/getting-started/quick_start/

If you run into issues, the SynchDB community is pretty helpful. There’s a GitHub discussions page, and the maintainers are responsive.

Final Thoughts

I’ve set up a lot of database replication environments over the years. Some took hours, some took days. One particularly memorable Oracle RAC setup took a week (we don’t talk about that project anymore).

Having a tool that just handles the boring setup stuff so you can focus on actual testing? That’s valuable. The interactive menu makes it accessible even if you’re not a shell script wizard. And being able to tear down and redeploy different configurations quickly means you can actually experiment.

Is ezdeploy.sh perfect? No. It’s a v1.2 feature, so I’m sure there are edge cases and improvements coming. But for quickly spinning up test environments for CDC replication, it’s genuinely useful.

If you’re working with heterogeneous database replication – whether you’re planning a migration, building a data warehouse, or just evaluating SynchDB – give it a try. Worst case, you waste 10 minutes. Best case, you save yourself a weekend.

About SynchDB

SynchDB is an open-source PostgreSQL extension for real-time data replication from MySQL, SQL Server, and Oracle. It’s developed by Hornetlabs and used in production for database migrations and data consolidation projects.

Links:

Leave a Reply

Your email address will not be published. Required fields are marked *