Skip to content

Commit 4135acc

Browse files
authored
Feat/implement phase e (#4)
1 parent 4099cee commit 4135acc

19 files changed

Lines changed: 808 additions & 71 deletions

File tree

.github/workflows/ci.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,28 @@ jobs:
3838
cache: true
3939
- name: go vet
4040
run: go vet ./...
41+
# `./...` skips underscore-prefixed dirs, so name the _mysqlcommon package
42+
# explicitly by import path to run its unit tests. Use the import path (not
43+
# a `_*` shell glob) so it works on PowerShell (Windows) as well as bash.
4144
- name: go test
42-
run: go test ./...
45+
run: go test ./... github.com/nixrajput/siphon/internal/driver/_mysqlcommon
46+
# Integration tests use testcontainers, which needs a Docker daemon.
47+
# Only ubuntu-latest runners have Docker available by default, so the
48+
# DB client install and integration suite are scoped to Ubuntu.
49+
# macOS/Windows run unit tests only (the integration-tagged files that
50+
# shell out to the client binaries are tagged out of the unit build).
51+
- name: Install DB client tools (integration, Ubuntu only)
52+
if: matrix.os == 'ubuntu-latest'
53+
run: |
54+
sudo apt-get update
55+
# postgresql-client -> pg_dump/pg_restore ; mysql-client -> mysqldump/mysql
56+
sudo apt-get install -y postgresql-client mysql-client
57+
# Ubuntu's mariadb-client Conflicts with mysql-client-core, so it can't
58+
# co-install. Pull mariadb-dump/mariadb from MariaDB's official APT repo
59+
# (the repo's mariadb-client is built to coexist with MySQL's client).
60+
curl -fsSL https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash
61+
sudo apt-get update
62+
sudo apt-get install -y mariadb-client
63+
- name: Integration tests (Ubuntu only — needs Docker)
64+
if: matrix.os == 'ubuntu-latest'
65+
run: go test -tags=integration ./... github.com/nixrajput/siphon/internal/driver/_mysqlcommon

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Capability gating helper: `app.RequireCapability` resolves a profile to its driver and rejects unsupported affordances with a `CodeUser` error. The helper and the `Capabilities` flags are in place; verbs are wired to it only where the gated feature is implemented. Streaming (`NativeStream`) and parallel backup (`Parallel`) are deferred to Phase F, so `sync --stream` / `backup --jobs` are not yet gated — the wiring lands with those features.
1818
- Connection-probe retry on Postgres `Connect` (3 attempts, exponential backoff) per spec §4.3; the generic `Retry` helper moved to `internal/jobs` to keep the driver layer free of an app-layer import.
1919
- `docs/DRIVERS.md` contributor guide documenting the driver contract, registration, the test harness, capability flags, and error mapping.
20+
- Phase E MySQL + MariaDB drivers:
21+
- Shared `internal/driver/_mysqlcommon` package: DSN builder, `mysqldump`/`mariadb-dump` arg builder, fork detection, and a shared `Conn` implementing the full `driver.Conn` contract (Inspect via `information_schema`, Backup by shelling to the dump tool, Restore by piping SQL into the client, sha256 Verify, bounded connect-probe retry). Backup/Restore pass the password via `MYSQL_PWD` and stream through stdout/stdin.
22+
- `mysql` and `mariadb` drivers as thin wrappers that inject the fork-specific binary names and declare capabilities honestly (`Parallel: false` — the dump tools are single-threaded). Registered via side-effect import in `internal/app/drivers.go`.
23+
- Integration suites for both engines run on the Phase D `RunDriverSuite` harness via testcontainers (`mysql:8.0`, `mariadb:11`).
24+
- CI installs the Postgres/MySQL/MariaDB client tools and runs the full integration suite on the Ubuntu runner (where Docker is available); unit tests continue to run on Linux/macOS/Windows.

Makefile

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@ help: ## Show this help
99
lint: ## Run golangci-lint
1010
golangci-lint run
1111

12+
# NOTE: `go test ./...` skips directories whose names start with `_` (Go tool
13+
# convention), so underscore-prefixed packages with real tests (e.g.
14+
# internal/driver/_mysqlcommon) are silently excluded. Name them explicitly by
15+
# import path so their unit tests run. Use the import path (not a `_*` shell
16+
# glob) so this is identical to what CI runs and works regardless of shell.
17+
UNDERSCORE_PKGS := github.com/nixrajput/siphon/internal/driver/_mysqlcommon
18+
1219
test: ## Run unit tests
13-
go test ./...
20+
go test ./... $(UNDERSCORE_PKGS)
1421

1522
test-verbose: ## Run unit tests verbosely
16-
go test -v ./...
23+
go test -v ./... $(UNDERSCORE_PKGS)
1724

1825
test-integration: ## Run integration tests (build tag: integration)
19-
go test -tags=integration ./...
26+
go test -tags=integration ./... $(UNDERSCORE_PKGS)
2027

2128
build: ## Build the siphon binary into ./bin
2229
@mkdir -p bin

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
1717
---
1818

1919
> [!WARNING]
20-
> **Pre-1.0 — active development.** Postgres backup/restore/sync/verify/inspect work end-to-end today (Phase B), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D), so MySQL/MariaDB can land mechanically next. MySQL/MariaDB, incremental backups, and ops features are on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a`, `phase-b`, `phase-c`, `phase-d`, …).
20+
> **Pre-1.0 — active development.** Postgres, MySQL, and MariaDB backup/restore/sync/verify/inspect work end-to-end today (Phases B + E), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D). Incremental backups, cross-engine sync, and ops features are on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a`, `phase-b`, `phase-c`, `phase-d`, `phase-e`, …).
2121
2222
## Table of contents
2323

@@ -38,8 +38,8 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
3838

3939
## Why siphon
4040

41-
- **One CLI, many databases.** Postgres works today; MySQL and MariaDB land in v1.0 (sharing a common backend). The driver interface is engine-agnostic, so SQLite, MongoDB, SQL Server, and ClickHouse can follow.
42-
- **Native, not reimplemented.** siphon shells out to `pg_dump`/`pg_restore` (and `mysqldump`/`mariadb-dump` in v1.0) for the actual data movement — you inherit 20+ years of correctness from the official tools, wrapped in a consistent UX.
41+
- **One CLI, many databases.** Postgres, MySQL, and MariaDB all work today (MySQL and MariaDB share a common `_mysqlcommon` backend). The driver interface is engine-agnostic, so SQLite, MongoDB, SQL Server, and ClickHouse can follow.
42+
- **Native, not reimplemented.** siphon shells out to `pg_dump`/`pg_restore`, `mysqldump`/`mysql`, and `mariadb-dump`/`mariadb` for the actual data movement — you inherit 20+ years of correctness from the official tools, wrapped in a consistent UX.
4343
- **Integrity by default.** Every dump is checksummed (SHA-256) and recorded in a sidecar metadata file. `siphon verify` re-hashes the dump and flags corruption or tampering — and fails with a distinct exit code so CI can catch it.
4444
- **Built for scripts and humans.** A Cobra command tree with predictable flags and POSIX exit codes for automation; an interactive Bubble Tea dashboard when you invoke `siphon` bare.
4545
- **Named profiles + secret refs.** Store connection details once; reference secrets as `env:VAR` today, with OS keychain / Vault / 1Password / AWS Secrets Manager backends on the roadmap. Plaintext passwords never have to live in your config.
@@ -53,22 +53,25 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
5353
| **B** — Postgres walking skeleton | `backup`, `restore`, `sync`, `verify`, `inspect`, `dumps`, `config`, `profile` working end-to-end against PostgreSQL | ✅ Complete |
5454
| **C** — TUI dashboard | Multi-panel Bubble Tea dashboard (profiles · dumps · jobs) with live job progress, backup/restore modal forms, and snapshot tests | ✅ Complete |
5555
| **D** — Driver hardening | Shared cross-driver test harness (`RunDriverSuite`), capability-gating helper (`RequireCapability`), Postgres connection-probe retry, and a `docs/DRIVERS.md` contributor guide | ✅ Complete |
56-
| **E** — MySQL + MariaDB | Both drivers via a shared `_mysqlcommon` package | ⏳ Planned |
56+
| **E** — MySQL + MariaDB | Both drivers via a shared `_mysqlcommon` package (shared `Conn`, `mysqldump`/`mariadb-dump` backup, client-pipe restore), exercised by the Phase D `RunDriverSuite` harness | ✅ Complete |
5757
| **F** — Advanced transfer | Incremental backups, bounded-buffer streaming, cross-engine sync, CDC | ⏳ Planned |
5858
| **G** — Ops features | Cloud storage, secret backends, profile groups + 2FA, team mode, audit log, retention, telemetry | ⏳ Planned |
5959
| **H** — Distribution | GoReleaser, Homebrew tap, Scoop bucket, install script, docs site | ⏳ Planned |
6060

6161
## Requirements
6262

6363
- **[Go](https://go.dev/dl/) 1.26 or newer** — to build from source (the only install method until Phase H).
64-
- **PostgreSQL client tools**`pg_dump`, `pg_restore`, and `psql` must be on your `PATH`. siphon shells out to them; it does not embed a Postgres client.
65-
66-
| Platform | Install |
67-
| ------------- | --------------------------------------------------------------------------------------------------------------- |
68-
| macOS | `brew install postgresql@16` |
69-
| Debian/Ubuntu | `sudo apt install postgresql-client` |
70-
| Fedora/RHEL | `sudo dnf install postgresql` |
71-
| Windows | Install the [EDB PostgreSQL](https://www.postgresql.org/download/windows/) package and add its `bin/` to `PATH` |
64+
- **Database client tools** — siphon shells out to the native dump/restore tools; it does not embed a client. You only need the tools for the engines you actually use:
65+
- **PostgreSQL** profiles need `pg_dump`, `pg_restore`, `psql`.
66+
- **MySQL** profiles need `mysqldump`, `mysql`.
67+
- **MariaDB** profiles need `mariadb-dump`, `mariadb` (the renamed binaries shipped by MariaDB 10.5+; older installs that only ship `mysqldump`/`mysql` are not yet supported).
68+
69+
| Platform | PostgreSQL | MySQL / MariaDB |
70+
| ------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
71+
| macOS | `brew install postgresql@16` | `brew install mysql-client` / `brew install mariadb` |
72+
| Debian/Ubuntu | `sudo apt install postgresql-client` | `sudo apt install mysql-client mariadb-client` |
73+
| Fedora/RHEL | `sudo dnf install postgresql` | `sudo dnf install mysql mariadb` |
74+
| Windows | Install the [EDB PostgreSQL](https://www.postgresql.org/download/windows/) package and add its `bin/` to `PATH` | Install the MySQL / MariaDB client and add to `PATH` |
7275

7376
- **Docker** _(optional)_ — only needed to run the integration test suite (`make test-integration`).
7477

docs/DRIVERS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ The `Fixtures` struct (in `internal/driver/_testing/fixtures.go`) you populate:
216216

217217
The worked example is `internal/driver/postgres/integration_test.go` — copy its
218218
shape. The suite runs behind the `integration` build tag (`make test-integration`).
219+
`internal/driver/mysql/` and `internal/driver/mariadb/` are a second worked example:
220+
two engines that share almost everything live in `internal/driver/_mysqlcommon/`
221+
(a shared `Conn` plus arg/DSN builders), leaving each driver a ~30-line wrapper
222+
that only injects the fork-specific binary names and capabilities. If you're
223+
adding an engine that's a fork of an existing one, follow that shared-helper
224+
pattern rather than copying a whole driver.
219225

220226
## Error mapping
221227

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ require (
88
github.com/charmbracelet/huh v1.0.0
99
github.com/charmbracelet/lipgloss v1.1.0
1010
github.com/charmbracelet/x/exp/teatest v0.0.0-20260527151214-009e6338d40d
11+
github.com/go-sql-driver/mysql v1.10.0
1112
github.com/jackc/pgx/v5 v5.9.2
1213
github.com/muesli/termenv v0.16.0
1314
github.com/oklog/ulid/v2 v2.1.1
1415
github.com/spf13/cobra v1.10.2
16+
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0
17+
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0
1518
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
1619
gopkg.in/yaml.v3 v3.0.1
1720
)
1821

1922
require (
2023
dario.cat/mergo v1.0.2 // indirect
24+
filippo.io/edwards25519 v1.2.0 // indirect
2125
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
2226
github.com/Microsoft/go-winio v0.6.2 // indirect
2327
github.com/atotto/clipboard v0.1.4 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
22
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
3+
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
4+
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
35
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
46
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
57
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -95,6 +97,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
9597
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
9698
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
9799
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
100+
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
101+
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
98102
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
99103
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
100104
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -195,6 +199,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
195199
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
196200
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
197201
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
202+
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0=
203+
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY=
204+
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU=
205+
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng=
198206
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo=
199207
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs=
200208
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=

internal/app/drivers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app
22

33
import (
44
"github.com/nixrajput/siphon/internal/driver"
5+
_ "github.com/nixrajput/siphon/internal/driver/mariadb" // register the mariadb driver
6+
_ "github.com/nixrajput/siphon/internal/driver/mysql" // register the mysql driver
57
_ "github.com/nixrajput/siphon/internal/driver/postgres" // register the postgres driver
68
)
79

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package mysqlcommon
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/nixrajput/siphon/internal/driver"
7+
)
8+
9+
// BuildDumpArgs assembles the mysqldump argument vector for a backup. The
10+
// binary name itself is supplied by the caller (mysqldump vs mariadb-dump).
11+
func BuildDumpArgs(p driver.Profile, opt driver.BackupOpts) []string {
12+
args := []string{
13+
"-h", p.Host,
14+
"-P", strconv.Itoa(p.Port),
15+
"-u", p.User,
16+
"--single-transaction",
17+
"--routines",
18+
"--triggers",
19+
"--events",
20+
"--no-tablespaces",
21+
"--skip-comments",
22+
p.Database,
23+
}
24+
if opt.SchemaOnly {
25+
args = append(args, "--no-data")
26+
}
27+
if opt.DataOnly {
28+
args = append(args, "--no-create-info")
29+
}
30+
args = append(args, opt.IncludeTables...)
31+
for _, t := range opt.ExcludeTables {
32+
args = append(args, "--ignore-table="+p.Database+"."+t)
33+
}
34+
return args
35+
}
36+
37+
// BuildRestoreArgs assembles the mysql client argument vector for a restore.
38+
// The dump file is authoritative for shape; the restore client just pipes it
39+
// in. Clean is a no-op here because mysqldump output already emits
40+
// DROP TABLE IF EXISTS / CREATE TABLE, making the restore idempotent.
41+
func BuildRestoreArgs(p driver.Profile, _ driver.RestoreOpts) []string {
42+
return []string{
43+
"-h", p.Host,
44+
"-P", strconv.Itoa(p.Port),
45+
"-u", p.User,
46+
"--default-character-set=utf8mb4",
47+
p.Database,
48+
}
49+
}
50+
51+
// NOTE: Profile.SSLMode is honored by the connect DSN (see dsn.go) but is NOT
52+
// yet propagated to the dump/restore CLI tools. The flag differs across forks —
53+
// MySQL's mysqldump uses --ssl-mode=<DISABLED|REQUIRED|VERIFY_CA|VERIFY_IDENTITY>
54+
// while MariaDB's mariadb-dump uses --ssl / --skip-ssl — so it needs per-fork
55+
// handling (and the dump tools' stderr surfaced so a rejected flag isn't opaque).
56+
// Tracked as a follow-up; see the PR #4 discussion.

0 commit comments

Comments
 (0)