db-ferry
v0.9.0
Published
Prebuilt db-ferry CLI distributed through npm
Readme
Multi-Database Migration Tool
A Go command-line utility that ferries data between Oracle, MySQL, PostgreSQL, SQL Server, SQLite, and DuckDB databases using declarative tasks. The tool automatically creates target schemas, streams data in batches with progress tracking, and supports flexible routing through named database aliases.
Features
- Connects to Oracle via
github.com/sijms/go-ora/v2, MySQL viagithub.com/go-sql-driver/mysql, PostgreSQL viagithub.com/lib/pq, SQL Server viagithub.com/denisenkom/go-mssqldb, SQLite viagithub.com/mattn/go-sqlite3, and DuckDB viagithub.com/duckdb/duckdb-go/v2 - Declarative
task.tomlwith alias-based source/target selection and optional index creation - Automatic table DDL generation based on source column metadata
- Batch inserts with transactional guarantees and efficient memory usage
- Incremental/resumable migrations via resume key and state file
- Task-level write modes (append/replace/merge), batch size, retries, and row-level validation (row_count / checksum / sample)
- Dead-letter queue (DLQ) for persisting failed rows after all retry attempts
- DAG-based scheduling for parallel execution of independent tasks
- Task-level
pre_sql/post_sqlhooks for running custom SQL before and after execution - Interactive configuration wizard (
db-ferry config init) with step-by-step prompts - PII masking and anonymization rules with 8 built-in rule types
- Schema evolution (auto
ALTER TABLE ADD COLUMN) in append/merge mode - Migration audit table written to target databases for traceability
- Adaptive batch size dynamic tuning based on latency and memory
- Column-level mapping and transform expressions for ETL-style pipelines
- Unified TLS/SSL support across all database adapters
diffcommand for source-target data comparison- MCP server with 5 agent-native tools for AI integration
- Range-based sharding for single-table parallel reads (append/merge mode)
- CDC polling mode for continuous incremental synchronization with cursor-based filtering
- Built-in cron scheduling for daemon mode with timezone, retry, and missed-catchup support
- Read replica and connection pool configuration
- Prometheus pull and OTLP HTTP push metrics export
- Webhook notification support after migration
- Data quality assertion rule engine with 7 built-in rule types
- Lua/JavaScript plugin support for row-level transformation
- Federated cross-database in-memory JOIN
- S3 and GCS output support for DLQ
- SSE real-time progress streaming
- Daemon mode with config hot-reload and health endpoint
- Progress bars for each task, including row counts when available
Installation
Install from npm
db-ferry can be installed globally or executed directly with npx:
npm install -g db-ferry
db-ferry -version
npx db-ferry -versionSupported npm binary packages:
| Platform | Arch | npm package | Notes |
|----------|------|-------------|-------|
| Linux | x64 | db-ferry-linux-x64 | Included via main db-ferry package |
| Linux | arm64 | db-ferry-linux-arm64 | Included via main db-ferry package |
| macOS | x64 | db-ferry-darwin-x64 | Included via main db-ferry package |
| macOS | arm64 | db-ferry-darwin-arm64 | Included via main db-ferry package |
| Windows | x64 | db-ferry-windows-x64 | Included via main db-ferry package; renamed from db-ferry-win32-x64 to avoid npm spam detection |
Windows arm64 npm binaries are not published yet. DuckDB remains unsupported on Windows builds.
Build from source
Clone the repository:
git clone <repository-url> cd db-ferryInstall dependencies:
go mod tidyBuild the application:
go build -o db-ferryDuckDB support relies on CGO. Ensure
CGO_ENABLED=1and the default C toolchain (clang on macOS, gcc/clang on Linux) are available when building binaries that include DuckDB aliases.
Configuration (task.toml)
All runtime configuration is stored in a single TOML file. Define every database once, then reference the aliases from individual tasks.
[[databases]]
name = "oracle_hr"
type = "oracle"
host = "db.example.com"
port = "1521"
service = "ORCLPDB1"
user = "hr"
password = "secret"
[[databases]]
name = "mysql_dw"
type = "mysql"
host = "mysql.internal"
port = "3306"
database = "warehouse"
user = "dw_writer"
password = "secret"
[[databases]]
name = "sqlite_local"
type = "sqlite"
path = "./data/output.db"
[[databases]]
name = "duckdb_local"
type = "duckdb"
path = "./data/local.duckdb"
[[tasks]]
table_name = "employees"
sql = "SELECT employee_id, first_name, last_name, department_id FROM employees"
source_db = "oracle_hr"
target_db = "sqlite_local"
ignore = false
[[tasks.indexes]]
name = "idx_employees_name"
columns = ["last_name:ASC", "first_name:ASC"]
[[tasks]]
table_name = "department_snapshot"
sql = "SELECT * FROM departments"
source_db = "oracle_hr"
target_db = "mysql_dw"
ignore = falseDatabase definitions
type:oracle,mysql,postgresql,sqlserver,sqlite, orduckdb- Oracle requires host, port, credentials, and service; MySQL/PostgreSQL/SQL Server require host, port, credentials, and database
- SQLite and DuckDB only require a file
path(relative or absolute,:memory:works for DuckDB) - Connection pool:
pool_max_open,pool_max_idletunesql.DBsettings - Read replicas:
[[databases.replicas]]withhostandpriority; setreplica_fallback = trueto fall back to the master - TLS/SSL:
ssl_mode(disable/require/verify-ca/verify-full), plusssl_cert,ssl_key,ssl_root_certas needed
Task definitions
sql: executed against thesource_dbsource_db/target_db: aliases declared in the[[databases]]sectionignore: skip execution without removing the taskmode:replace(default),append, ormerge(upsertis accepted)batch_size: number of rows per insert batch (default: 1000)max_retries: retry count for failed batch inserts (default: 0)validate:row_count(compare inserted rows vs target table count),checksum(hash-based row comparison), orsample(random sampling validation); skipped for merge modemerge_keys: columns used to match rows for merge/upsert (requires unique constraint on target)resume_key: column used for incremental/resume filteringresume_from: SQL literal for the resume filter (exclusive)state_file: JSON file to persist the last resume value per taskallow_same_table: allow migrations wheresource_dbequalstarget_db(acknowledges table drop risk)skip_create_table: skip dropping/creating the target table (use when the table already exists)pre_sql: custom SQL to execute against the target database before the task beginspost_sql: custom SQL to execute against the target database after the task completesdlq_path: dead-letter queue file path; failed rows are written here instead of failing the entire taskdlq_format: DLQ output format,jsonl(default) orcsvdepends_on: task dependencies declared bytable_name; enables DAG-based schedulingschema_evolution: in append/merge mode, automatically runALTER TABLE ADD COLUMNwhen the source introduces new columnscolumns: column-level mapping with optional transform expressions (source->target, withtransform)masking: PII masking rules per column (column,rule, optionalrange/value)adaptive_batch: dynamic batch-size tuning (enabled,min_size,max_size,target_latency_ms,memory_limit_mb)shard: range-based parallel sharding for single-table reads (enabled,shards); requiresresume_key, only in append/merge modecdc: continuous incremental sync via polling (enabled,cursor_column,poll_interval,initial_cursor,delete_detection); requiresmode = append/merge,state_file, andresume_key(auto-set tocursor_column); not supported with federated or shard tasksvalidate_sample_size: number of rows to sample whenvalidate = "sample"[[tasks.indexes]]: optional index creation statements applied after data load (partial indexes viawhereare supported on SQLite targets)[[tasks.sources]]/[tasks.join]: federated cross-database in-memory JOIN; define multiple sources withalias,db, andsql, then specify joinkeysandtype(inner/left/right); not compatible withresume_key,state_file, orshard[[tasks.assertions]]: data quality assertions per task (column/columns,rule,on_fail); rules includenot_null,range(withmin/max),in_set(withvalues),unique(withcolumns),regex(withpattern),min_length/max_length(withlength);on_faildefaults toabort, can be set towarnordlq[tasks.plugin]: row-level transformation plugin (engine:luaorjavascript,script: inline script,timeout_ms); executed per row before insert
History configuration
Global [history] section controls migration audit logging to the target database:
[history]
enabled = true
table_name = "db_ferry_migrations"enabled: write an audit record per task to the target database (table auto-created if missing)table_name: override the default audit table name
Metrics configuration
Global [metrics] section enables Prometheus pull or OTLP HTTP push metrics export:
[metrics]
enabled = true
listen_addr = ":9090" # Prometheus pull endpoint, e.g. http://localhost:9090/metrics
endpoint = "http://localhost:4318/v1/metrics" # OTLP HTTP push endpoint
interval = "30s" # Push interval when endpoint is configuredenabled: turn on metrics collection and exportlisten_addr: optional TCP address for Prometheus scrape endpoint (pull mode)endpoint: optional OTLP HTTP endpoint for push modeinterval: push interval, default30s; validated as a Go duration
Notify configuration
Global [notify] section defines webhook URLs called after migration completes or fails:
[notify]
on_success = ["https://hooks.slack.com/services/xxx"]
on_failure = ["https://hooks.slack.com/services/xxx"]
timeout = "10s"
retry = 2on_success: list of webhook URLs called when all tasks succeedon_failure: list of webhook URLs called when any task failstimeout: per-request timeout (default0, meaning no timeout)retry: number of retries for failed webhook requests (default0)
Schedule configuration
Global [schedule] section enables cron-based execution when running in daemon mode. Only active when db-ferry is started as a daemon (e.g., via a service manager or -watch).
[schedule]
cron = "0 2 * * *"
timezone = "Asia/Shanghai"
retry_on_failure = true
max_retry = 3
missed_catchup = true
start_at = "2026-01-01T00:00:00"
end_at = "2026-12-31T23:59:59"cron: cron expression (standard 5-field) or descriptor such as@every 1h; validated at startuptimezone: IANA timezone name (e.g.,America/New_York); defaults to system local timeretry_on_failure: when true, retries failed rounds up tomax_retrytimes with a fixed 1-minute intervalmax_retry: maximum retry attempts (must be >= 0)missed_catchup: when true, executes immediately on startup if the last scheduled run was missedstart_at/end_at: optional execution window boundaries in RFC3339 or2006-01-02T15:04:05format; rounds outside the window are skipped
Usage
# Generate task.toml interactively (wizard) or from the built-in sample
db-ferry config init
# Run with default task.toml
db-ferry
# Specify an alternate configuration file
db-ferry -config ./configs/task.toml
# Enable verbose logging
db-ferry -v
# Show version information
db-ferry -version
# Compare source and target data for a task
db-ferry diff -task employees
# Start MCP server for AI agent integration
db-ferry mcp serveCommand line options
config init: Interactive configuration wizard that createstask.tomlin the current directory; walks through engine selection, connection details, and table choices. Falls back to the built-in sample if non-interactive. Fails if the file already existsdiff: Compare source and target data for a given task. Flags:-task(required),-keys,-where,-limit,-output,-format(json/csv/html)mcp serve: Start an MCP server with 5 agent-native tools for AI integration-config: Path to the TOML configuration file (default:task.toml)-v: Enable verbose logging with file/line prefixes-version: Print build version and exit-sse-port: Start an SSE server (e.g.:8080) that streams real-time task progress via/eventsand exposes current status via/status; supports CORS for local frontend development
Development Commands
db-ferry provides a justfile with common Go quality checks:
# list all recipes
just
# format all go files
just fmt
# check formatting
just fmt-check
# run lint checks (golangci-lint)
just lint
# run tests
just test
# run coverage gate (global >=75%, each package >=65%)
just test-cover
# build all packages
just build
# run full local quality gate: fmt-check + lint + test-cover
just checkRelease
- Tag pushes matching
v*trigger multi-platform binary builds and npm publishing - npm publishing expects a repository secret named
NPM_TOKEN - Published package layout uses one public main package (
db-ferry) plus per-platform binary packages viaoptionalDependencies - npm publish steps skip package versions that already exist, so a fixed workflow can rerun the same tag release to backfill only the missing packages
Coverage rules:
- Global coverage must be
>= 75% - Each package coverage must be
>= 65% - Any package without test files is treated as failure in
test-cover
Data type mapping (high level)
| Source Type (Oracle/MySQL/PostgreSQL/SQL Server) | Target Mapping | |---------------------------|----------------| | NUMBER / DECIMAL | INTEGER or REAL (precision-aware) | | VARCHAR / CHAR / TEXT | TEXT | | DATE / DATETIME / TIMESTAMP | TEXT (ISO 8601) | | BLOB / RAW / BINARY | BLOB |
The processor inspects driver metadata to determine precision, scale, length, and nullability before creating the target table.
Project structure
db-ferry/
├── main.go # CLI entry point
├── go.mod # Go module definition
├── task.toml.sample # Sample configuration with database/task templates
├── config/
│ └── config.go # Configuration loading & validation
├── database/
│ ├── interface.go # Shared interfaces & metadata structs
│ ├── manager.go # Connection registry for aliased DBs
│ ├── mysql.go # MySQL source/target implementation
│ ├── oracle.go # Oracle source/target implementation
│ ├── duckdb.go # DuckDB source/target implementation
│ ├── postgres.go # PostgreSQL source/target implementation
│ ├── sqlserver.go # SQL Server source/target implementation
│ └── sqlite.go # SQLite source/target implementation
├── processor/
│ └── processor.go # Task execution engine
├── assertion/ # Data quality assertion engine
├── daemon/ # Daemon mode, hot-reload, health endpoint
├── metrics/ # Prometheus pull & OTLP push metrics
├── notify/ # Webhook notifications
├── sse/ # SSE real-time progress streaming
└── utils/
└── progress.go # Progress bar utilities