Skip to content

Commit 0edbdcd

Browse files
fix: reject unsupported options in MySQL structured config
MySQL's OptsBuilder does not support arbitrary key-value connection parameters. Return an error listing the unsupported keys instead of silently ignoring them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e4862c2 commit 0edbdcd

2 files changed

Lines changed: 59 additions & 32 deletions

File tree

docs/seeding.md

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ database:
4848
database:
4949
driver: postgres
5050
host: pg.example.com
51-
port: 5432 # Optional. Default: 5432 (postgres), 3306 (mysql)
51+
port: 5432 # Optional. Default: 5432 (postgres), 3306 (mysql)
5252
user: netbird
5353
password: "{{ env.DB_PASSWORD }}" # Special chars just work — no URL encoding
5454
name: mydb
55-
options: # Optional. Driver-specific connection parameters
55+
options: # Optional. Driver-specific connection parameters
5656
sslmode: disable
5757
```
5858
@@ -111,36 +111,36 @@ phases:
111111

112112
### Field reference
113113

114-
| Field | Type | Required | Description |
115-
| ----------------------------------------------- | ----------------- | -------- | ---------------------------------------------------------------------------- |
116-
| `database.driver` | string | Yes | Database driver: `postgres`, `mysql`, or `sqlite` |
117-
| `database.url` | string | No | Direct database connection URL (cannot combine with structured fields) |
118-
| `database.url_env` | string | No | Environment variable containing the database URL |
119-
| `database.host` | string | No | Database host (structured config; cannot combine with url/url_env) |
120-
| `database.port` | integer | No | Database port (default: 5432 for postgres, 3306 for mysql) |
121-
| `database.user` | string | No | Database user (structured config) |
122-
| `database.password` | string | No | Database password — special characters work without encoding |
123-
| `database.name` | string | No | Database name (structured config) |
124-
| `database.options` | map[string]string | No | Driver-specific connection parameters (e.g. `sslmode: disable`) |
125-
| `database.tracking_table` | string | No | Name of the seed tracking table (default: `initium_seed`) |
126-
| `phases[].name` | string | Yes | Unique phase name |
127-
| `phases[].order` | integer | No | Execution order (lower first, default: 0) |
128-
| `phases[].database` | string | No | Target database name (for create/switch) |
129-
| `phases[].schema` | string | No | Target schema name (for create/switch) |
130-
| `phases[].create_if_missing` | boolean | No | Create the database/schema if it does not exist (default: false) |
131-
| `phases[].timeout` | string | No | Default wait timeout (e.g. `30s`, `1m`, `1m30s`; default: `30s`) |
132-
| `phases[].wait_for[].type` | string | Yes | Object type: `table`, `view`, `schema`, or `database` |
133-
| `phases[].wait_for[].name` | string | Yes | Object name to wait for |
134-
| `phases[].wait_for[].timeout` | string | No | Per-object timeout override (e.g. `60s`, `2m`, `1m30s`) |
135-
| `phases[].seed_sets[].name` | string | Yes | Unique name for the seed set (used in tracking) |
136-
| `phases[].seed_sets[].order` | integer | No | Execution order (lower values first, default: 0) |
137-
| `phases[].seed_sets[].mode` | string | No | Seed mode: `once` (default) or `reconcile` |
138-
| `phases[].seed_sets[].tables[].table` | string | Yes | Target database table name |
139-
| `phases[].seed_sets[].tables[].order` | integer | No | Execution order within the seed set (default: 0) |
140-
| `phases[].seed_sets[].tables[].unique_key` | string[] | No | Columns for duplicate detection |
141-
| `phases[].seed_sets[].tables[].auto_id.column` | string | No | Auto-generated ID column name |
142-
| `phases[].seed_sets[].tables[].auto_id.id_type` | string | No | ID type (default: `integer`) |
143-
| `phases[].seed_sets[].tables[].rows[]._ref` | string | No | Internal reference name for cross-table references |
114+
| Field | Type | Required | Description |
115+
| ----------------------------------------------- | ----------------- | -------- | ---------------------------------------------------------------------- |
116+
| `database.driver` | string | Yes | Database driver: `postgres`, `mysql`, or `sqlite` |
117+
| `database.url` | string | No | Direct database connection URL (cannot combine with structured fields) |
118+
| `database.url_env` | string | No | Environment variable containing the database URL |
119+
| `database.host` | string | No | Database host (structured config; cannot combine with url/url_env) |
120+
| `database.port` | integer | No | Database port (default: 5432 for postgres, 3306 for mysql) |
121+
| `database.user` | string | No | Database user (structured config) |
122+
| `database.password` | string | No | Database password — special characters work without encoding |
123+
| `database.name` | string | No | Database name (structured config) |
124+
| `database.options` | map[string]string | No | Driver-specific connection parameters (e.g. `sslmode: disable`) |
125+
| `database.tracking_table` | string | No | Name of the seed tracking table (default: `initium_seed`) |
126+
| `phases[].name` | string | Yes | Unique phase name |
127+
| `phases[].order` | integer | No | Execution order (lower first, default: 0) |
128+
| `phases[].database` | string | No | Target database name (for create/switch) |
129+
| `phases[].schema` | string | No | Target schema name (for create/switch) |
130+
| `phases[].create_if_missing` | boolean | No | Create the database/schema if it does not exist (default: false) |
131+
| `phases[].timeout` | string | No | Default wait timeout (e.g. `30s`, `1m`, `1m30s`; default: `30s`) |
132+
| `phases[].wait_for[].type` | string | Yes | Object type: `table`, `view`, `schema`, or `database` |
133+
| `phases[].wait_for[].name` | string | Yes | Object name to wait for |
134+
| `phases[].wait_for[].timeout` | string | No | Per-object timeout override (e.g. `60s`, `2m`, `1m30s`) |
135+
| `phases[].seed_sets[].name` | string | Yes | Unique name for the seed set (used in tracking) |
136+
| `phases[].seed_sets[].order` | integer | No | Execution order (lower values first, default: 0) |
137+
| `phases[].seed_sets[].mode` | string | No | Seed mode: `once` (default) or `reconcile` |
138+
| `phases[].seed_sets[].tables[].table` | string | Yes | Target database table name |
139+
| `phases[].seed_sets[].tables[].order` | integer | No | Execution order within the seed set (default: 0) |
140+
| `phases[].seed_sets[].tables[].unique_key` | string[] | No | Columns for duplicate detection |
141+
| `phases[].seed_sets[].tables[].auto_id.column` | string | No | Auto-generated ID column name |
142+
| `phases[].seed_sets[].tables[].auto_id.id_type` | string | No | ID type (default: `integer`) |
143+
| `phases[].seed_sets[].tables[].rows[]._ref` | string | No | Internal reference name for cross-table references |
144144

145145
### Wait-for object support by driver
146146

src/seed/db.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,12 @@ fn connect_structured(
16031603
}
16041604
#[cfg(feature = "mysql")]
16051605
"mysql" => {
1606+
if !config.options.is_empty() {
1607+
return Err(format!(
1608+
"structured database config does not support 'options' for mysql (unsupported keys: {})",
1609+
config.options.keys().cloned().collect::<Vec<_>>().join(", ")
1610+
));
1611+
}
16061612
let port = config.port.unwrap_or(3306);
16071613
let mut opts = mysql::OptsBuilder::default()
16081614
.ip_or_hostname(Some(&config.host))
@@ -1813,6 +1819,27 @@ mod tests {
18131819
assert!(err.contains("not supported for sqlite"));
18141820
}
18151821

1822+
#[cfg(feature = "mysql")]
1823+
#[test]
1824+
fn test_connect_structured_mysql_rejects_options() {
1825+
use std::collections::HashMap;
1826+
let config = crate::seed::schema::DatabaseConfig {
1827+
driver: "mysql".into(),
1828+
host: "localhost".into(),
1829+
user: "root".into(),
1830+
name: "test".into(),
1831+
options: {
1832+
let mut m = HashMap::new();
1833+
m.insert("charset".into(), "utf8mb4".into());
1834+
m
1835+
},
1836+
..Default::default()
1837+
};
1838+
let err = connect(&config).err().expect("expected error");
1839+
assert!(err.contains("does not support 'options' for mysql"));
1840+
assert!(err.contains("charset"));
1841+
}
1842+
18161843
#[test]
18171844
fn test_escape_dsn_value() {
18181845
assert_eq!(escape_dsn_value("simple"), "simple");

0 commit comments

Comments
 (0)