Skip to content

Commit d5c3ca7

Browse files
Feat(duckdb): Add support for authentication using secrets (#4459)
1 parent 5e997a8 commit d5c3ca7

File tree

7 files changed

+112
-6
lines changed

7 files changed

+112
-6
lines changed

docs/integrations/engines/duckdb.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
| `catalogs` | Mapping to define multiple catalogs. Can [attach DuckDB catalogs](#duckdb-catalogs-example) or [catalogs for other connections](#other-connection-catalogs-example). First entry is the default catalog. Cannot be defined if using `database`. | dict | N |
1818
| `extensions` | Extension to load into duckdb. Only autoloadable extensions are supported. | list | N |
1919
| `connector_config` | Configuration to pass into the duckdb connector. | dict | N |
20+
| `secrets` | Configuration for authenticating external sources (e.g., S3) using DuckDB secrets. | dict | N |
2021

2122
#### DuckDB Catalogs Example
2223

@@ -141,6 +142,69 @@ If a connector, like Postgres, requires sensitive information in the path, it mi
141142

142143
DuckDB can read data directly from cloud services via extensions (e.g., [httpfs](https://duckdb.org/docs/extensions/httpfs/s3api), [azure](https://duckdb.org/docs/extensions/azure)).
143144

144-
Loading credentials at runtime using `load_aws_credentials()` or similar functions may fail when using SQLMesh.
145+
The `secrets` option allows you to configure DuckDB's [Secrets Manager](https://duckdb.org/docs/configuration/secrets_manager.html) to authenticate with external services like S3. This is the recommended approach for cloud storage authentication in DuckDB v0.10.0 and newer, replacing the [legacy authentication method](https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html) via variables.
145146

146-
Instead, create persistent and automatically used authentication credentials with the [DuckDB secrets manager](https://duckdb.org/docs/configuration/secrets_manager.html) (available in DuckDB v0.10.0 or greater).
147+
##### Secrets Configuration Example for S3
148+
149+
The `secrets` accepts a list of secret configurations, each defining the necessary authentication parameters for the specific service:
150+
151+
=== "YAML"
152+
153+
```yaml linenums="1"
154+
gateways:
155+
duckdb:
156+
connection:
157+
type: duckdb
158+
catalogs:
159+
local: local.db
160+
remote: "s3://bucket/data/remote.duckdb"
161+
extensions:
162+
- name: httpfs
163+
secrets:
164+
- type: s3
165+
region: "YOUR_AWS_REGION"
166+
key_id: "YOUR_AWS_ACCESS_KEY"
167+
secret: "YOUR_AWS_SECRET_KEY"
168+
```
169+
170+
=== "Python"
171+
172+
```python linenums="1"
173+
from sqlmesh.core.config import (
174+
Config,
175+
ModelDefaultsConfig,
176+
GatewayConfig,
177+
DuckDBConnectionConfig
178+
)
179+
180+
config = Config(
181+
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
182+
gateways={
183+
"duckdb": GatewayConfig(
184+
connection=DuckDBConnectionConfig(
185+
catalogs={
186+
"local": "local.db",
187+
"remote": "s3://bucket/data/remote.duckdb"
188+
},
189+
extensions=[
190+
{"name": "httpfs"},
191+
],
192+
secrets=[
193+
{
194+
"type": "s3",
195+
"region": "YOUR_AWS_REGION",
196+
"key_id": "YOUR_AWS_ACCESS_KEY",
197+
"secret": "YOUR_AWS_SECRET_KEY"
198+
}
199+
]
200+
)
201+
),
202+
}
203+
)
204+
```
205+
206+
After configuring the secrets, you can directly reference S3 paths in your catalogs or in SQL queries without additional authentication steps.
207+
208+
Refer to the official DuckDB documentation for the full list of [supported S3 secret parameters](https://duckdb.org/docs/stable/extensions/httpfs/s3api.html#overview-of-s3-secret-parameters) and for more information on the [Secrets Manager configuration](https://duckdb.org/docs/configuration/secrets_manager.html).
209+
210+
> Note: Loading credentials at runtime using `load_aws_credentials()` or similar deprecated functions may fail when using SQLMesh.

docs/integrations/engines/motherduck.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ Congratulations \- your SQLMesh project is up and running on MotherDuck\!
104104
| `token` | The optional MotherDuck token. If not specified, the user will be prompted to login with their web browser. | string | N |
105105
| `extensions` | Extension to load into duckdb. Only autoloadable extensions are supported. | list | N |
106106
| `connector_config` | Configuration to pass into the duckdb connector. | dict | N |
107+
| `secrets` | Configuration for authenticating external sources (e.g. S3) using DuckDB secrets. | dict | N |

sqlmesh/core/config/connection.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import pydantic
1515
from pydantic import Field
16+
from packaging import version
1617
from sqlglot import exp
1718
from sqlglot.helper import subclasses
1819

@@ -201,6 +202,7 @@ class BaseDuckDBConnectionConfig(ConnectionConfig):
201202
catalogs: Key is the name of the catalog and value is the path.
202203
extensions: A list of autoloadable extensions to load.
203204
connector_config: A dictionary of configuration to pass into the duckdb connector.
205+
secrets: A list of dictionaries used to generate DuckDB secrets for authenticating with external services (e.g. S3).
204206
concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
205207
register_comments: Whether or not to register model comments with the SQL engine.
206208
pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
@@ -211,6 +213,7 @@ class BaseDuckDBConnectionConfig(ConnectionConfig):
211213
catalogs: t.Optional[t.Dict[str, t.Union[str, DuckDBAttachOptions]]] = None
212214
extensions: t.List[t.Union[str, t.Dict[str, t.Any]]] = []
213215
connector_config: t.Dict[str, t.Any] = {}
216+
secrets: t.List[t.Dict[str, t.Any]] = []
214217

215218
concurrent_tasks: int = 1
216219
register_comments: bool = True
@@ -283,6 +286,28 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None:
283286
except Exception as e:
284287
raise ConfigError(f"Failed to set connector config {field} to {setting}: {e}")
285288

289+
if self.secrets:
290+
duckdb_version = duckdb.__version__
291+
if version.parse(duckdb_version) < version.parse("0.10.0"):
292+
from sqlmesh.core.console import get_console
293+
294+
get_console().log_warning(
295+
f"DuckDB version {duckdb_version} does not support secrets-based authentication (requires 0.10.0 or later).\n"
296+
"To use secrets, please upgrade DuckDB. For older versions, configure legacy authentication via `connector_config`.\n"
297+
"More info: https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html"
298+
)
299+
else:
300+
for secrets in self.secrets:
301+
secret_settings: t.List[str] = []
302+
for field, setting in secrets.items():
303+
secret_settings.append(f"{field} '{setting}'")
304+
if secret_settings:
305+
secret_clause = ", ".join(secret_settings)
306+
try:
307+
cursor.execute(f"CREATE SECRET ({secret_clause});")
308+
except Exception as e:
309+
raise ConfigError(f"Failed to create secret: {e}")
310+
286311
for i, (alias, path_options) in enumerate(
287312
(getattr(self, "catalogs", None) or {}).items()
288313
):

sqlmesh/dbt/target.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class DuckDbConfig(TargetConfig):
151151
path: Location of the database file. If not specified, an in memory database is used.
152152
extensions: A list of autoloadable extensions to load.
153153
settings: A dictionary of settings to pass into the duckdb connector.
154+
secrets: A list of secrets to pass to the secret manager in the duckdb connector.
154155
"""
155156

156157
type: t.Literal["duckdb"] = "duckdb"
@@ -159,6 +160,7 @@ class DuckDbConfig(TargetConfig):
159160
path: str = DUCKDB_IN_MEMORY
160161
extensions: t.Optional[t.List[str]] = None
161162
settings: t.Optional[t.Dict[str, t.Any]] = None
163+
secrets: t.Optional[t.List[t.Dict[str, t.Any]]] = None
162164

163165
@model_validator(mode="before")
164166
def validate_authentication(cls, data: t.Any) -> t.Any:
@@ -192,6 +194,8 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig:
192194
kwargs["extensions"] = self.extensions
193195
if self.settings is not None:
194196
kwargs["connector_config"] = self.settings
197+
if self.secrets is not None:
198+
kwargs["secrets"] = self.secrets
195199
return DuckDBConnectionConfig(
196200
database=self.path,
197201
concurrent_tasks=1,

tests/core/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ def test_connection_config_serialization():
550550
"pre_ping": False,
551551
"pretty_sql": False,
552552
"connector_config": {},
553+
"secrets": [],
553554
"database": "my_db",
554555
}
555556
assert serialized["default_test_connection"] == {
@@ -560,6 +561,7 @@ def test_connection_config_serialization():
560561
"pre_ping": False,
561562
"pretty_sql": False,
562563
"connector_config": {},
564+
"secrets": [],
563565
"database": "my_test_db",
564566
}
565567

tests/core/test_connection_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,17 @@ def test_duckdb(make_config):
426426
type="duckdb",
427427
database="test",
428428
connector_config={"foo": "bar"},
429+
secrets=[
430+
{
431+
"type": "s3",
432+
"region": "aws_region",
433+
"key_id": "aws_access_key",
434+
"secret": "aws_secret",
435+
}
436+
],
429437
)
438+
assert config.connector_config
439+
assert config.secrets
430440
assert isinstance(config, DuckDBConnectionConfig)
431441
assert not config.is_recommended_for_state_sync
432442

tests/integrations/jupyter/test_magics.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,16 +759,16 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_
759759
"Models: 18",
760760
"Macros: 8",
761761
"",
762-
"Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}",
763-
"Test Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}",
762+
"Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None",
763+
"Test Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None",
764764
"Data warehouse connection succeeded",
765765
]
766766
assert get_all_html_output(output) == [
767767
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Models: <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">18</span></pre>",
768768
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Macros: <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">8</span></pre>",
769769
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"></pre>",
770-
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span></pre>',
771-
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Test Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span></pre>',
770+
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span></pre>',
771+
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Test Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span></pre>',
772772
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Data warehouse connection <span style=\"color: #008000; text-decoration-color: #008000\">succeeded</span></pre>",
773773
]
774774

0 commit comments

Comments
 (0)