Skip to content

Commit a406546

Browse files
committed
chore: updated docs
1 parent 6ebd58e commit a406546

File tree

7 files changed

+256
-122
lines changed

7 files changed

+256
-122
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
- id: mixed-line-ending
1818
- id: trailing-whitespace
1919
- repo: https://github.com/charliermarsh/ruff-pre-commit
20-
rev: "v0.14.7"
20+
rev: "v0.14.8"
2121
hooks:
2222
- id: ruff
2323
args: ["--fix"]

AGENTS.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,13 @@ def command(config: str | None):
453453
- Walk filesystem from cwd to find config files
454454
- Return `None` if not found to trigger helpful error message
455455

456-
**Reference implementation:** `sqlspec/cli.py` (lines 26-65), `sqlspec/utils/config_discovery.py`
456+
**Multi-config support:**
457+
- Split comma-separated values from CLI flag, env var, or pyproject.toml
458+
- Resolve each config path independently
459+
- Flatten results if callables return lists
460+
- Deduplicate by `bind_key` (later configs override earlier ones with same key)
461+
462+
**Reference implementation:** `sqlspec/cli.py` (lines 26-110), `sqlspec/utils/config_discovery.py`
457463

458464
### CLI Sync/Async Dispatch Pattern
459465

docs/examples/usage/usage_cli_1.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def test_single_and_multiple_configs() -> None:
2525
def get_configs() -> list[AsyncpgConfig]:
2626
return [db_config]
2727

28+
# Usage with CLI:
29+
# --config "myapp.config.db_config" # Single config
30+
# --config "myapp.config.configs" # Config list
31+
# --config "myapp.config.get_configs" # Callable
32+
# --config "myapp.config.db_config,myapp.config.configs" # Multiple paths (comma-separated)
33+
2834
# end-example
2935
assert isinstance(db_config, AsyncpgConfig)
3036
assert isinstance(configs, list)

docs/usage/cli.rst

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,14 @@ SQLSpec CLI supports three ways to specify your database configuration, in order
217217
2. **Environment Variable** (``SQLSPEC_CONFIG``) - Convenient for development workflows
218218
3. **pyproject.toml** (``[tool.sqlspec]``) - Project-wide default configuration
219219

220-
The ``--config`` option and ``SQLSPEC_CONFIG`` environment variable accept a dotted path to either:
220+
The ``--config`` option and ``SQLSPEC_CONFIG`` environment variable accept:
221221

222222
- **A single config object**: ``myapp.config.db_config``
223223
- **A config list**: ``myapp.config.configs``
224224
- **A callable function**: ``myapp.config.get_configs()``
225+
- **Multiple config paths (comma-separated)**: ``myapp.config.primary_config,myapp.config.analytics_config``
226+
227+
Each config path is resolved independently, and if a callable returns a list of configs, all configs are collected.
225228

226229
Example configuration file (``myapp/config.py``):
227230

@@ -239,36 +242,36 @@ Config Discovery Methods
239242

240243
.. code-block:: bash
241244
242-
sqlspec --config myapp.config:get_configs upgrade head
245+
sqlspec --config myapp.config.get_configs upgrade head
243246
244247
Use for one-off commands or to override other config sources.
245248

246249
**Method 2: Environment Variable**
247250

248251
.. code-block:: bash
249252
250-
export SQLSPEC_CONFIG=myapp.config:get_configs
253+
export SQLSPEC_CONFIG=myapp.config.get_configs
251254
sqlspec upgrade head # Uses environment variable
252255
253256
Convenient for development. Add to your shell profile:
254257

255258
.. code-block:: bash
256259
257260
# ~/.bashrc or ~/.zshrc
258-
export SQLSPEC_CONFIG=myapp.config:get_configs
261+
export SQLSPEC_CONFIG=myapp.config.get_configs
259262
260263
Multiple configs (comma-separated):
261264

262265
.. code-block:: bash
263266
264-
export SQLSPEC_CONFIG="app.db:primary_config,app.db:analytics_config"
267+
export SQLSPEC_CONFIG="app.db.primary_config,app.db.analytics_config"
265268
266269
**Method 3: pyproject.toml (Project Default)**
267270

268271
.. code-block:: toml
269272
270273
[tool.sqlspec]
271-
config = "myapp.config:get_configs"
274+
config = "myapp.config.get_configs"
272275
273276
Best for team projects - config is version controlled.
274277

@@ -278,22 +281,82 @@ Multiple configs (array):
278281
279282
[tool.sqlspec]
280283
config = [
281-
"myapp.config:primary_config",
282-
"myapp.config:analytics_config"
284+
"myapp.config.primary_config",
285+
"myapp.config.analytics_config"
283286
]
284287
285288
**Precedence:** CLI flag > Environment variable > pyproject.toml
286289

287290
If no config is found from any source, SQLSpec will show a helpful error message with examples.
288291

292+
Multi-Config Resolution Details
293+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
294+
295+
When using comma-separated config paths or list format in pyproject.toml, SQLSpec:
296+
297+
1. **Resolves each path independently**: Each dotted path is imported and resolved
298+
2. **Flattens callable results**: If a callable returns a list, all configs are collected
299+
3. **Deduplicates by bind_key**: Later configs override earlier ones with the same ``bind_key``
300+
4. **Validates final list**: Empty config lists result in an error
301+
302+
**Deduplication Example:**
303+
304+
.. code-block:: bash
305+
306+
# If primary_config has bind_key="db" and backup_config has bind_key="db"
307+
export SQLSPEC_CONFIG="app.primary_config,app.backup_config"
308+
309+
# Result: Only backup_config is used (last wins)
310+
311+
**Combined callable and list:**
312+
313+
.. code-block:: python
314+
315+
# myapp/config.py
316+
def get_all_configs():
317+
"""Returns list of configs."""
318+
return [primary_config, analytics_config]
319+
320+
single_config = AsyncpgConfig(bind_key="backup", ...)
321+
322+
.. code-block:: bash
323+
324+
# This resolves to 3 configs: primary, analytics, and backup
325+
export SQLSPEC_CONFIG="myapp.config.get_all_configs,myapp.config.single_config"
326+
327+
**Deduplication with callables:**
328+
329+
.. code-block:: python
330+
331+
# myapp/config.py
332+
primary = AsyncpgConfig(bind_key="db", ...)
333+
updated = AsyncpgConfig(bind_key="db", ...) # Same bind_key
334+
335+
def get_primary():
336+
return primary
337+
338+
def get_updated():
339+
return updated
340+
341+
.. code-block:: bash
342+
343+
# Only updated config is used (last wins for bind_key="db")
344+
sqlspec --config "myapp.config.get_primary,myapp.config.get_updated" upgrade
345+
289346
Global Options
290347
--------------
291348

292349
``--config PATH``
293-
Dotted path to SQLSpec config(s) or callable function. Optional when using
294-
environment variable or pyproject.toml config discovery.
350+
Dotted path to SQLSpec config(s) or callable function. Supports comma-separated
351+
multiple paths. Optional when using environment variable or pyproject.toml config discovery.
352+
353+
Examples:
354+
355+
- Single config: ``--config myapp.config.db_config``
356+
- Callable: ``--config myapp.config.get_configs``
357+
- Multiple paths: ``--config "myapp.config.primary_config,myapp.config.analytics_config"``
295358

296-
Example: ``--config myapp.config.get_configs``
359+
Configs with duplicate ``bind_key`` values are deduplicated (last wins).
297360

298361
``--validate-config``
299362
Validate configuration before executing migrations. Shows loaded configs
@@ -808,6 +871,41 @@ Multi-Config Operations
808871
When you have multiple database configurations, SQLSpec provides options to manage
809872
them collectively or selectively.
810873

874+
Quick Reference: Multi-Config Patterns
875+
---------------------------------------
876+
877+
.. list-table::
878+
:header-rows: 1
879+
:widths: 30 35 35
880+
881+
* - Pattern
882+
- Example
883+
- Behavior
884+
* - Single config
885+
- ``--config "app.config.db"``
886+
- Load one config
887+
* - Config list
888+
- ``--config "app.config.configs"``
889+
- Load all configs in list
890+
* - Callable returning list
891+
- ``--config "app.config.get_configs"``
892+
- Call function, load returned configs
893+
* - Comma-separated paths
894+
- ``--config "app.config.db1,app.config.db2"``
895+
- Load multiple configs, deduplicate by bind_key
896+
* - Env var (comma-separated)
897+
- ``SQLSPEC_CONFIG="app.config.db1,app.config.db2"``
898+
- Same as comma-separated CLI flag
899+
* - pyproject.toml (list)
900+
- ``config = ["app.config.db1", "app.config.db2"]``
901+
- Load all paths in array
902+
* - Mixed callables and configs
903+
- ``--config "app.config.get_configs,app.config.backup"``
904+
- Flatten callable results + direct configs
905+
* - Duplicate bind_key
906+
- ``--config "app.config.old,app.config.new"``
907+
- Later config overrides (new wins)
908+
811909
Scenario: Multiple Databases
812910
-----------------------------
813911

@@ -954,6 +1052,38 @@ Best Practices
9541052
9551053
sqlspec --config myapp.config upgrade --dry-run
9561054
1055+
7. **Use Unique bind_key for Multi-Config**
1056+
1057+
When managing multiple databases, always specify unique ``bind_key`` values:
1058+
1059+
.. code-block:: python
1060+
1061+
# Good - unique bind_keys
1062+
configs = [
1063+
AsyncpgConfig(bind_key="primary", ...),
1064+
AsyncpgConfig(bind_key="analytics", ...),
1065+
]
1066+
1067+
# Problematic - configs will overwrite each other
1068+
configs = [
1069+
AsyncpgConfig(bind_key="db", ...), # Same key
1070+
AsyncmyConfig(bind_key="db", ...), # Will override above
1071+
]
1072+
1073+
8. **Prefer pyproject.toml for Team Projects**
1074+
1075+
Store config paths in version control for consistency:
1076+
1077+
.. code-block:: toml
1078+
1079+
[tool.sqlspec]
1080+
config = [
1081+
"myapp.config.primary_db",
1082+
"myapp.config.analytics_db"
1083+
]
1084+
1085+
Team members automatically use the same config without manual setup.
1086+
9571087
Framework Integration
9581088
=====================
9591089

sqlspec/cli.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ def sqlspec_group(ctx: "click.Context", config: str | None, validate_config: boo
8282
if isinstance(config_result, Sequence) and not isinstance(config_result, str):
8383
all_configs.extend(config_result)
8484
else:
85-
all_configs.append(
86-
cast("AsyncDatabaseConfig[Any, Any, Any] | SyncDatabaseConfig[Any, Any, Any]", config_result)
87-
)
85+
all_configs.append(config_result) # pyright: ignore
8886

8987
# Deduplicate by bind_key (later configs override earlier ones)
9088
configs_by_key: dict[

tests/integration/test_cli_config_discovery.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ def test_cli_with_pyproject(tmp_path: "Path", monkeypatch: "pytest.MonkeyPatch",
8888
assert "Successfully loaded 1 config(s)" in result.output
8989

9090

91-
def test_cli_flag_overrides_env(tmp_path: "Path", monkeypatch: "pytest.MonkeyPatch", mock_config_module: "Path") -> None:
91+
def test_cli_flag_overrides_env(
92+
tmp_path: "Path", monkeypatch: "pytest.MonkeyPatch", mock_config_module: "Path"
93+
) -> None:
9294
"""Test --config flag overrides SQLSPEC_CONFIG."""
9395
monkeypatch.chdir(tmp_path)
9496

@@ -145,9 +147,7 @@ def test_cli_flag_overrides_pyproject(
145147
runner = CliRunner()
146148
cli = get_cli_with_commands()
147149

148-
result = runner.invoke(
149-
cli, ["--config", "test_config.config.get_test_config", "--validate-config", "show-config"]
150-
)
150+
result = runner.invoke(cli, ["--config", "test_config.config.get_test_config", "--validate-config", "show-config"])
151151

152152
assert result.exit_code == 0, f"Output: {result.output}"
153153
assert "Successfully loaded 1 config(s)" in result.output
@@ -338,9 +338,7 @@ def get_multiple_configs():
338338
cli = get_cli_with_commands()
339339

340340
result = runner.invoke(
341-
cli,
342-
["--validate-config", "show-config"],
343-
env={"SQLSPEC_CONFIG": "multiconf2.config.get_multiple_configs"},
341+
cli, ["--validate-config", "show-config"], env={"SQLSPEC_CONFIG": "multiconf2.config.get_multiple_configs"}
344342
)
345343

346344
assert result.exit_code == 0, f"Output: {result.output}"
@@ -350,7 +348,9 @@ def get_multiple_configs():
350348
sys.path.remove(str(tmp_path))
351349

352350

353-
def test_cli_config_with_bind_key(tmp_path: "Path", monkeypatch: "pytest.MonkeyPatch", mock_config_module: "Path") -> None:
351+
def test_cli_config_with_bind_key(
352+
tmp_path: "Path", monkeypatch: "pytest.MonkeyPatch", mock_config_module: "Path"
353+
) -> None:
354354
"""Test config with bind_key is displayed correctly."""
355355
monkeypatch.chdir(tmp_path)
356356

0 commit comments

Comments
 (0)