Skip to content

Commit 3ab3346

Browse files
fix: Support role and warehouse as query parameters (#15)
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
1 parent e4c849a commit 3ab3346

File tree

3 files changed

+85
-9
lines changed

3 files changed

+85
-9
lines changed

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,27 @@ pipx inject meltano git+https://github.com/meltano/meltano-state-backend-snowfla
2929

3030
## Configuration
3131

32-
To store state in Snowflake, set the `state_backend.uri` setting to `snowflake://<user>:<password>@<account>/<database>/<schema>`.
32+
To store state in Snowflake, set the `state_backend.uri` setting to a [Snowflake SQLAlchemy][snowflake-sqlalchemy]-style URI:
33+
34+
```
35+
snowflake://<user>:<password>@<account>/<database>/<schema>?warehouse=<warehouse>&role=<role>
36+
```
3337

3438
State will be stored in two tables that Meltano will create automatically:
3539

3640
- `meltano_state` - Stores the actual state data
3741
- `meltano_state_locks` - Manages concurrency locks
3842

39-
To authenticate to Snowflake, you'll need to provide:
43+
All connection parameters can be provided in the URI (including as query parameters), as individual Meltano settings, or a mix of both. Explicit settings take precedence over URI values.
44+
45+
Using a single URI with query parameters:
46+
47+
```yaml
48+
state_backend:
49+
uri: snowflake://my_user:my_password@my_account/my_database/my_schema?warehouse=my_warehouse&role=my_role
50+
```
51+
52+
Using a URI with separate settings for warehouse and role:
4053
4154
```yaml
4255
state_backend:
@@ -46,11 +59,11 @@ state_backend:
4659
role: my_role # Optional: The role to use for the connection
4760
```
4861
49-
Alternatively, you can provide credentials via individual settings:
62+
Using individual settings for everything:
5063
5164
```yaml
5265
state_backend:
53-
uri: snowflake://my_account/my_database/my_schema
66+
uri: snowflake://my_account
5467
snowflake:
5568
account: my_account
5669
user: my_user
@@ -83,10 +96,11 @@ Example using environment variables:
8396

8497
```bash
8598
export MELTANO_STATE_BACKEND_SNOWFLAKE_PASSWORD='my_secure_password'
86-
meltano config set meltano state_backend.uri 'snowflake://my_user@my_account/my_database'
87-
meltano config set meltano state_backend.snowflake.warehouse 'my_warehouse'
99+
meltano config set meltano state_backend.uri 'snowflake://my_user@my_account/my_database?warehouse=my_warehouse'
88100
```
89101

102+
Passwords containing special characters (e.g. `@`, `%`) must be URL-encoded when included in the URI. For example, `p@ss` becomes `p%40ss`.
103+
90104
## Development
91105

92106
### Setup
@@ -115,5 +129,6 @@ gh release create v<new-version>
115129
[meltano]: https://meltano.com
116130
[pipx]: https://github.com/pypa/pipx
117131
[snowflake]: https://www.snowflake.com/
132+
[snowflake-sqlalchemy]: https://github.com/snowflakedb/snowflake-sqlalchemy
118133
[state-backend]: https://docs.meltano.com/concepts/state_backends
119134
[uv]: https://docs.astral.sh/uv

src/meltano_state_backend_snowflake/backend.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from contextlib import contextmanager
88
from functools import cached_property
99
from time import sleep
10-
from urllib.parse import urlparse
10+
from urllib.parse import parse_qs, urlparse
1111

1212
import snowflake.connector
1313
from meltano.core.error import MeltanoError
@@ -123,6 +123,7 @@ def __init__(
123123
super().__init__(**kwargs)
124124
self.uri = uri
125125
parsed = urlparse(uri)
126+
query_params = parse_qs(parsed.query)
126127

127128
# Extract connection details from URI and parameters
128129
self.account = account or parsed.hostname
@@ -140,7 +141,7 @@ def __init__(
140141
msg = "Snowflake password is required"
141142
raise MissingStateBackendSettingsError(msg)
142143

143-
self.warehouse = warehouse
144+
self.warehouse = warehouse or query_params.get("warehouse", [None])[0]
144145
if not self.warehouse:
145146
msg = "Snowflake warehouse is required"
146147
raise MissingStateBackendSettingsError(msg)
@@ -153,7 +154,7 @@ def __init__(
153154
raise MissingStateBackendSettingsError(msg)
154155

155156
self.schema = schema or (path_parts[1] if len(path_parts) > 1 else "PUBLIC")
156-
self.role = role
157+
self.role = role or query_params.get("role", [None])[0]
157158

158159
self._ensure_tables()
159160

tests/test_backend.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,66 @@ def test_acquire_lock_retry(
379379
assert mock_cursor.execute.call_count >= 2
380380

381381

382+
@pytest.mark.usefixtures("mock_connection")
383+
class TestURIQueryParams:
384+
"""Tests for URI query parameter parsing."""
385+
386+
def test_full_sqlalchemy_uri(self) -> None:
387+
"""Test full SQLAlchemy-style URI with all params."""
388+
manager = SnowflakeStateStoreManager(
389+
uri="snowflake://myuser:mypass@myaccount/mydb/myschema?warehouse=mywh&role=myrole",
390+
)
391+
assert manager.account == "myaccount"
392+
assert manager.user == "myuser"
393+
assert manager.password == "mypass" # noqa: S105
394+
assert manager.database == "mydb"
395+
assert manager.schema == "myschema"
396+
assert manager.warehouse == "mywh"
397+
assert manager.role == "myrole"
398+
399+
def test_warehouse_from_query_param(self) -> None:
400+
"""Test warehouse extracted from query parameter."""
401+
manager = SnowflakeStateStoreManager(
402+
uri="snowflake://myuser:mypass@myaccount/mydb?warehouse=mywh",
403+
)
404+
assert manager.warehouse == "mywh"
405+
assert manager.schema == "PUBLIC"
406+
407+
def test_role_from_query_param(self) -> None:
408+
"""Test role extracted from query parameter."""
409+
manager = SnowflakeStateStoreManager(
410+
uri="snowflake://myuser:mypass@myaccount/mydb?warehouse=mywh&role=analyst",
411+
)
412+
assert manager.role == "analyst"
413+
414+
def test_explicit_settings_override_query_params(self) -> None:
415+
"""Test that explicit settings take precedence over query params."""
416+
manager = SnowflakeStateStoreManager(
417+
uri="snowflake://uri_user:uri_pass@uri_account/uri_db/uri_schema?warehouse=uri_wh&role=uri_role",
418+
account="explicit_account",
419+
user="explicit_user",
420+
password="explicit_pass", # noqa: S106
421+
warehouse="explicit_wh",
422+
database="explicit_db",
423+
schema="explicit_schema",
424+
role="explicit_role",
425+
)
426+
assert manager.account == "explicit_account"
427+
assert manager.user == "explicit_user"
428+
assert manager.password == "explicit_pass" # noqa: S105
429+
assert manager.warehouse == "explicit_wh"
430+
assert manager.database == "explicit_db"
431+
assert manager.schema == "explicit_schema"
432+
assert manager.role == "explicit_role"
433+
434+
def test_role_defaults_to_none(self) -> None:
435+
"""Test role defaults to None when not provided."""
436+
manager = SnowflakeStateStoreManager(
437+
uri="snowflake://myuser:mypass@myaccount/mydb?warehouse=mywh",
438+
)
439+
assert manager.role is None
440+
441+
382442
def test_missing_account_validation() -> None:
383443
"""Test missing account validation."""
384444
with pytest.raises(

0 commit comments

Comments
 (0)