Skip to content

Commit e4862c2

Browse files
feat: support structured database connection config as alternative to URL (#39)
Add discrete connection fields (host, port, user, password, name, options) as an alternative to url/url_env. Connections are built using driver-native APIs (PostgreSQL key-value DSN, MySQL OptsBuilder), bypassing URL parsing so passwords with special characters work without encoding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 58037e3 commit e4862c2

9 files changed

Lines changed: 657 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `urlencode` template filter for percent-encoding strings in URLs. Useful for embedding passwords or other values containing URL-reserved characters (`@`, `%`, `:`, `/`, etc.) in connection strings.
1313
- `dprint` formatter for Markdown, JSON, TOML, YAML, and Dockerfile with CI check (`dprint/check@v2.2`) and definition-of-done gate.
14+
- Structured database connection config as an alternative to URL (`host`, `port`, `user`, `password`, `name`, `options` fields). Passwords with URL-reserved characters (`@`, `%`, `:`, etc.) work without encoding. Connections are built using driver-native APIs (PostgreSQL key-value DSN, MySQL `OptsBuilder`), bypassing URL parsing entirely. The `url`/`url_env` fields remain supported for backward compatibility. See [#39](https://github.com/KitStream/initium/issues/39).
1415

1516
### Changed
1617

docs/seeding.md

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,54 @@ initium seed --spec /seeds/seed.yaml --json
2929

3030
The seed spec file defines the complete seeding plan. Both YAML and JSON formats are supported (file extension determines parser). The spec file is a MiniJinja template rendered with environment variables before parsing.
3131

32+
### Database connection
33+
34+
Two connection styles are supported. Choose **one** — they cannot be combined.
35+
36+
**URL-based** (existing behavior):
37+
38+
```yaml
39+
database:
40+
driver: postgres
41+
url: "postgres://user:pass@host:5432/dbname"
42+
# or: url_env: DATABASE_URL
43+
```
44+
45+
**Structured fields** (no URL encoding needed):
46+
47+
```yaml
48+
database:
49+
driver: postgres
50+
host: pg.example.com
51+
port: 5432 # Optional. Default: 5432 (postgres), 3306 (mysql)
52+
user: netbird
53+
password: "{{ env.DB_PASSWORD }}" # Special chars just work — no URL encoding
54+
name: mydb
55+
options: # Optional. Driver-specific connection parameters
56+
sslmode: disable
57+
```
58+
59+
Structured config builds the connection using driver-native APIs, bypassing URL parsing entirely. Passwords with `@`, `%`, `:`, or other URL-reserved characters work without encoding.
60+
61+
> **Note:** Structured config is not supported for SQLite — use `url` instead.
62+
63+
### Full schema
64+
3265
```yaml
3366
database:
3467
driver: postgres # Required. One of: postgres, mysql, sqlite
68+
# --- URL-based connection (pick one style) ---
3569
url: "postgres://..." # Direct database URL
3670
url_env: DATABASE_URL # Or: name of env var containing the URL
71+
# --- Structured connection (alternative to url/url_env) ---
72+
host: pg.example.com # Database host
73+
port: 5432 # Optional. Default per driver
74+
user: myuser # Database user
75+
password: "secret" # Database password (special chars OK)
76+
name: mydb # Database name
77+
options: # Optional. Driver-specific parameters
78+
sslmode: disable
79+
# --- Common ---
3780
tracking_table: initium_seed # Default: "initium_seed"
3881
3982
phases:
@@ -68,12 +111,18 @@ phases:
68111

69112
### Field reference
70113

71-
| Field | Type | Required | Description |
72-
| ----------------------------------------------- | -------- | -------- | ---------------------------------------------------------------- |
73-
| `database.driver` | string | Yes | Database driver: `postgres`, `mysql`, or `sqlite` |
74-
| `database.url` | string | No | Direct database connection URL |
75-
| `database.url_env` | string | No | Environment variable containing the database URL |
76-
| `database.tracking_table` | string | No | Name of the seed tracking table (default: `initium_seed`) |
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`) |
77126
| `phases[].name` | string | Yes | Unique phase name |
78127
| `phases[].order` | integer | No | Execution order (lower first, default: 0) |
79128
| `phases[].database` | string | No | Target database name (for create/switch) |
@@ -115,14 +164,18 @@ phases:
115164

116165
SQLite does not support separate databases or schemas — each file is a database.
117166

118-
### Database URL resolution
167+
### Database connection resolution
119168

120-
The database URL is resolved in this order:
169+
If structured fields (`host`, `port`, `user`, `password`, `name`) are provided, the connection is built using driver-native APIs — no URL is needed.
170+
171+
Otherwise, the database URL is resolved in this order:
121172

122173
1. `database.url_env` — environment variable name containing the URL
123174
2. `database.url` — direct URL in the spec file
124175
3. `DATABASE_URL` — fallback environment variable
125176

177+
Structured fields and URL-based fields (`url`/`url_env`) are mutually exclusive — specifying both is a validation error.
178+
126179
## Features
127180

128181
### MiniJinja Templating
@@ -385,6 +438,7 @@ spec:
385438
See the [`examples/seed/`](../examples/seed/) directory:
386439

387440
- [`basic-seed.yaml`](../examples/seed/basic-seed.yaml) — PostgreSQL with departments and employees, cross-table references
441+
- [`structured-config-seed.yaml`](../examples/seed/structured-config-seed.yaml) — PostgreSQL with structured connection config (no URL encoding)
388442
- [`sqlite-seed.yaml`](../examples/seed/sqlite-seed.yaml) — SQLite configuration table seeding
389443
- [`env-credentials-seed.yaml`](../examples/seed/env-credentials-seed.yaml) — MySQL with credentials from Kubernetes secrets
390444
- [`phased-seed.yaml`](../examples/seed/phased-seed.yaml) — Multi-phase PostgreSQL seeding with wait-for, create-if-missing, and MiniJinja templating
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Structured database connection config — no URL encoding needed.
2+
#
3+
# Passwords with special characters (@, %, :, etc.) just work.
4+
# Initium builds the connection using driver-native APIs.
5+
#
6+
# Usage:
7+
# export DB_PASSWORD='p@ss:word/with%special&chars'
8+
# initium seed --spec examples/seed/structured-config-seed.yaml
9+
10+
database:
11+
driver: postgres
12+
host: pg.example.com
13+
port: 5432
14+
user: app_user
15+
password: "{{ env.DB_PASSWORD }}"
16+
name: myapp
17+
options:
18+
sslmode: disable
19+
20+
phases:
21+
- name: setup
22+
seed_sets:
23+
- name: config
24+
tables:
25+
- table: config
26+
unique_key: [key]
27+
rows:
28+
- key: app_name
29+
value: MyApp
30+
- key: version
31+
value: "1.0.0"

0 commit comments

Comments
 (0)