Skip to content

Commit 200b002

Browse files
committed
SNOW-2306184: config refactor - connections.toml legacy behaviour
1 parent 613d50f commit 200b002

File tree

7 files changed

+91
-80
lines changed

7 files changed

+91
-80
lines changed

src/snowflake/cli/_plugins/connection/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ def list_connections(
108108
Lists configured connections.
109109
"""
110110
from snowflake.cli.api.config_provider import (
111-
_is_alternative_config_enabled,
112111
get_config_provider_singleton,
112+
is_alternative_config_enabled,
113113
)
114114

115115
# Use provider directly for config_ng to pass the flag
116-
if _is_alternative_config_enabled():
116+
if is_alternative_config_enabled():
117117
provider = get_config_provider_singleton()
118118
connections = provider.get_all_connections(include_env_connections=all_sources)
119119
else:

src/snowflake/cli/api/config.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -322,35 +322,17 @@ def config_section_exists(*path) -> bool:
322322

323323

324324
def get_all_connections() -> dict[str, ConnectionConfig]:
325-
# Use config provider if available
326-
try:
327-
from snowflake.cli.api.config_provider import get_config_provider_singleton
325+
from snowflake.cli.api.config_provider import get_config_provider_singleton
328326

329-
provider = get_config_provider_singleton()
330-
return provider.get_all_connections()
331-
except Exception:
332-
# Fall back to legacy implementation
333-
return {
334-
k: ConnectionConfig.from_dict(connection_dict)
335-
for k, connection_dict in get_config_section("connections").items()
336-
}
327+
provider = get_config_provider_singleton()
328+
return provider.get_all_connections()
337329

338330

339331
def get_connection_dict(connection_name: str) -> dict:
340-
# Use config provider if available
341-
try:
342-
from snowflake.cli.api.config_provider import get_config_provider_singleton
332+
from snowflake.cli.api.config_provider import get_config_provider_singleton
343333

344-
provider = get_config_provider_singleton()
345-
return provider.get_connection_dict(connection_name)
346-
except Exception:
347-
# Fall back to legacy implementation
348-
try:
349-
return get_config_section(CONNECTIONS_SECTION, connection_name)
350-
except KeyError:
351-
raise MissingConfigurationError(
352-
f"Connection {connection_name} is not configured"
353-
)
334+
provider = get_config_provider_singleton()
335+
return provider.get_connection_dict(connection_name)
354336

355337

356338
def get_default_connection_name() -> str:

src/snowflake/cli/api/config_ng/resolver.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,8 @@ def resolve(self, key: Optional[str] = None, default: Any = None) -> Dict[str, A
349349
1. Iterate sources in order (lowest to highest priority)
350350
2. Record all discovered values in history
351351
3. For connection keys (connections.{name}.{param}):
352-
- Merge connection-by-connection: later sources extend/overwrite individual params
352+
- If connections.toml defines a connection, it REPLACES cli_config_toml only
353+
- SnowSQL config, environment vars, and CLI parameters still override
353354
4. For flat keys: later sources overwrite earlier sources
354355
5. Mark which value was selected
355356
6. Return final resolved values
@@ -365,6 +366,19 @@ def resolve(self, key: Optional[str] = None, default: Any = None) -> Dict[str, A
365366
# Track connection values separately for intelligent merging
366367
connections: Dict[str, Dict[str, ConfigValue]] = defaultdict(dict)
367368

369+
# Identify sources that connections.toml replaces
370+
# connections.toml only replaces cli_config_toml, not SnowSQL config
371+
cli_config_source = "cli_config_toml"
372+
connections_file_source = None
373+
connections_to_replace: set[str] = set()
374+
375+
# First pass: find connections.toml and identify connections to replace
376+
for source in self._sources:
377+
if hasattr(source, "is_connections_file") and source.is_connections_file:
378+
connections_file_source = source
379+
connections_to_replace = source.get_defined_connections()
380+
break
381+
368382
# Process sources in order (first = lowest priority, last = highest)
369383
for source in self._sources:
370384
try:
@@ -384,6 +398,22 @@ def resolve(self, key: Optional[str] = None, default: Any = None) -> Dict[str, A
384398
param = parts[2]
385399
param_key = f"connections.{conn_name}.{param}"
386400

401+
# Replacement logic: Skip cli_config_toml if connection is in connections.toml
402+
# SnowSQL config is NOT replaced by connections.toml
403+
is_cli_config = source.source_name == cli_config_source
404+
connection_in_connections_toml = (
405+
conn_name in connections_to_replace
406+
)
407+
408+
if is_cli_config and connection_in_connections_toml:
409+
# Skip this value - connections.toml replaces cli_config_toml
410+
log.debug(
411+
"Skipping %s from %s (replaced by connections.toml)",
412+
param_key,
413+
source.source_name,
414+
)
415+
continue
416+
387417
# Merge at parameter level: later source overwrites/extends
388418
connections[conn_name][param_key] = config_value
389419
else:

src/snowflake/cli/api/config_ng/sources.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,42 @@ def __init__(self):
312312
def source_name(self) -> str:
313313
return "connections_toml"
314314

315+
@property
316+
def is_connections_file(self) -> bool:
317+
"""Mark this as the dedicated connections file source."""
318+
return True
319+
320+
def get_defined_connections(self) -> set[str]:
321+
"""
322+
Return set of connection names that are defined in connections.toml.
323+
This is used by the resolver to implement replacement behavior.
324+
"""
325+
if not self._file_path.exists():
326+
return set()
327+
328+
try:
329+
with open(self._file_path, "rb") as f:
330+
data = tomllib.load(f)
331+
332+
connection_names = set()
333+
334+
# Check for direct connection sections (legacy format)
335+
for section_name, section_data in data.items():
336+
if isinstance(section_data, dict) and section_name != "connections":
337+
connection_names.add(section_name)
338+
339+
# Check for nested [connections] section format
340+
connections_section = data.get("connections", {})
341+
if isinstance(connections_section, dict):
342+
for conn_name in connections_section.keys():
343+
connection_names.add(conn_name)
344+
345+
return connection_names
346+
347+
except Exception as e:
348+
log.debug("Failed to read connections.toml: %s", e)
349+
return set()
350+
315351
def discover(self, key: Optional[str] = None) -> Dict[str, ConfigValue]:
316352
"""
317353
Read connections.toml if it exists.

src/snowflake/cli/api/config_provider.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,14 @@ def get_connection_dict(self, connection_name: str) -> dict:
177177
)
178178

179179
def get_all_connections(self, include_env_connections: bool = False) -> dict:
180-
from snowflake.cli.api.config import get_all_connections
180+
from snowflake.cli.api.config import ConnectionConfig, get_config_section
181181

182182
# Legacy provider ignores the flag since it never had env connections
183-
return get_all_connections()
183+
connections = get_config_section("connections")
184+
return {
185+
name: ConnectionConfig.from_dict(self._transform_private_key_raw(config))
186+
for name, config in connections.items()
187+
}
184188

185189

186190
class AlternativeConfigProvider(ConfigProvider):

tests/config_ng/test_configuration.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ def test_complete_7_level_chain(config_ng_setup):
195195
# Level 3 provides warehouse
196196
assert conn["warehouse"] == "level3-wh"
197197

198-
# Level 2 provides password
199-
assert conn["password"] == "level2-pass"
198+
# Level 2 (cli_config) is skipped because connections.toml defines this connection
199+
# password from cli_config is NOT present
200200

201201
# Level 1 provides user
202202
assert conn["user"] == "level1-user"
@@ -444,7 +444,7 @@ def test_all_file_sources_precedence(config_ng_setup):
444444

445445
expected = {
446446
"account": "from-connections", # Level 3 wins
447-
"user": "cli-user", # Level 2 wins
447+
"user": "snowsql-user", # Level 1 only (cli_config skipped)
448448
"warehouse": "snowsql-warehouse", # Level 1 only source
449449
"password": "connections-pass", # Level 3 wins
450450
}
@@ -575,7 +575,7 @@ def test_all_files_plus_snowsql_env(config_ng_setup):
575575
expected = {
576576
"account": "env-account", # Level 4 wins
577577
"user": "snowsql-user", # Level 1 only
578-
"warehouse": "cli-warehouse", # Level 2 only
578+
# warehouse from cli_config is skipped (connections.toml replaces cli_config)
579579
"database": "toml-db", # Level 3 only
580580
}
581581
assert conn == expected
@@ -619,7 +619,7 @@ def test_all_files_plus_general_env(config_ng_setup):
619619
expected = {
620620
"account": "env-account", # Level 6 wins
621621
"user": "snowsql-user", # Level 1 only
622-
"role": "cli-role", # Level 2 only
622+
# role from cli_config is skipped (connections.toml replaces cli_config)
623623
"warehouse": "env-warehouse", # Level 6 wins
624624
}
625625
assert conn == expected
@@ -739,7 +739,7 @@ def test_all_files_plus_two_env_types(config_ng_setup):
739739
expected = {
740740
"account": "conn-specific", # Level 5 wins
741741
"user": "snowsql-user", # Level 1 only
742-
"password": "cli-password", # Level 2 only
742+
# password from cli_config is skipped (connections.toml replaces cli_config)
743743
"warehouse": "conn-warehouse", # Level 5 wins
744744
}
745745
assert conn == expected
@@ -904,7 +904,7 @@ def test_multiple_connections_different_source_patterns(config_ng_setup):
904904
conn1 = get_connection_dict("conn1")
905905
expected1 = {
906906
"account": "conn1-env", # Connection-specific env wins
907-
"user": "conn1-user", # CLI config
907+
# user from cli_config is skipped (connections.toml replaces cli_config)
908908
"warehouse": "conn1-warehouse", # Connections TOML
909909
"schema": "common-schema", # General env
910910
}

tests/test_config.py

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
get_env_variable_name,
2929
set_config_value,
3030
)
31-
from snowflake.cli.api.config_provider import is_alternative_config_enabled
3231
from snowflake.cli.api.exceptions import MissingConfigurationError
3332

3433
from tests.testing_utils.files_and_dirs import assert_file_permissions_are_strict
@@ -384,12 +383,7 @@ def assert_correct_connections_loaded():
384383
assert_correct_connections_loaded()
385384

386385

387-
# Legacy version - skip when config_ng is enabled
388-
@pytest.mark.skipif(
389-
is_alternative_config_enabled(),
390-
reason="Legacy behavior: connections.toml replaces all connections from config.toml",
391-
)
392-
def test_connections_toml_override_config_toml_legacy(
386+
def test_connections_toml_override_config_toml(
393387
test_snowcli_config, snowflake_home, config_manager
394388
):
395389
connections_toml = snowflake_home / "connections.toml"
@@ -400,49 +394,14 @@ def test_connections_toml_override_config_toml_legacy(
400394
)
401395
config_init(test_snowcli_config)
402396

403-
# Legacy: Only connections from connections.toml are present
397+
# Both legacy and config_ng: Only connections from connections.toml are present
398+
# connections.toml REPLACES config.toml connections (not merge)
404399
assert get_default_connection_dict() == {"database": "overridden_database"}
405400
assert config_manager["connections"] == {
406401
"default": {"database": "overridden_database"}
407402
}
408403

409404

410-
# Config_ng version - skip when config_ng is NOT enabled
411-
@pytest.mark.skipif(
412-
not is_alternative_config_enabled(),
413-
reason="Config_ng behavior: connections.toml merges with config.toml per-key",
414-
)
415-
def test_connections_toml_override_config_toml_config_ng(
416-
test_snowcli_config, snowflake_home, config_manager
417-
):
418-
"""Test config_ng behavior: connections.toml merges with config.toml per-key"""
419-
connections_toml = snowflake_home / "connections.toml"
420-
connections_toml.write_text(
421-
"""[default]
422-
database = "overridden_database"
423-
"""
424-
)
425-
config_init(test_snowcli_config)
426-
427-
# Config_ng: Merged - database from connections.toml, other keys from config.toml
428-
# The key difference from legacy: keys from config.toml are preserved
429-
default_conn = get_default_connection_dict()
430-
431-
# Key from connections.toml (level 3) overrides
432-
assert default_conn["database"] == "overridden_database"
433-
434-
# Keys from config.toml (level 2) are preserved
435-
assert default_conn["schema"] == "test_public"
436-
assert default_conn["role"] == "test_role"
437-
assert default_conn["warehouse"] == "xs"
438-
assert default_conn["password"] == "dummy_password"
439-
440-
# Verify other connections from config.toml are also accessible
441-
full_conn = get_connection_dict("full")
442-
assert full_conn["account"] == "dev_account"
443-
assert full_conn["user"] == "dev_user"
444-
445-
446405
parametrize_chmod = pytest.mark.parametrize(
447406
"chmod",
448407
[

0 commit comments

Comments
 (0)