Skip to content

Commit 1097cef

Browse files
authored
Fix: Make gateway names case-insesitive (#5092)
1 parent e749958 commit 1097cef

File tree

7 files changed

+158
-11
lines changed

7 files changed

+158
-11
lines changed

docs/concepts/models/external_models.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ If SQLMesh does not have access to an external table's metadata, the table will
5656

5757
In some use-cases such as [isolated systems with multiple gateways](../../guides/isolated_systems.md#multiple-gateways), there are external models that only exist on a certain gateway.
5858

59+
**Gateway names are case-insensitive in external model configurations.** You can specify the gateway name using any case (e.g., `gateway: dev`, `gateway: DEV`, `gateway: Dev`) and SQLMesh will handle the matching correctly.
60+
5961
Consider the following model that queries an external table with a dynamic database based on the current gateway:
6062

6163
```
@@ -100,7 +102,7 @@ This example demonstrates the structure of a `external_models.yaml` file:
100102
column_d: float
101103
- name: external_db.gateway_specific_external_table
102104
description: Another external table that only exists when the gateway is set to "test"
103-
gateway: test
105+
gateway: test # Case-insensitive - could also be "TEST", "Test", etc.
104106
columns:
105107
column_e: int
106108
column_f: varchar

docs/guides/configuration.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ SQLMesh creates schemas, physical tables, and views in the data warehouse/engine
322322

323323
The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override *where* SQLMesh creates physical tables and views with the `physical_schema_mapping`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options.
324324

325-
You can also override *what* the physical tables are called by using the `physical_table_naming_convention` option.
325+
You can also override *what* the physical tables are called by using the `physical_table_naming_convention` option.
326326

327327
These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page.
328328

@@ -767,7 +767,9 @@ Even though the second change should have been a metadata change (thus not requi
767767
768768
The `gateways` configuration defines how SQLMesh should connect to the data warehouse, state backend, and scheduler. These options are in the [gateway](../reference/configuration.md#gateway) section of the configuration reference page.
769769

770-
Each gateway key represents a unique gateway name and configures its connections. For example, this configures the `my_gateway` gateway:
770+
Each gateway key represents a unique gateway name and configures its connections. **Gateway names are case-insensitive** - SQLMesh automatically normalizes gateway names to lowercase during configuration validation. This means you can use any case in your configuration files (e.g., `MyGateway`, `mygateway`, `MYGATEWAY`) and they will all work correctly.
771+
772+
For example, this configures the `my_gateway` gateway:
771773

772774
=== "YAML"
773775

docs/reference/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ SQLMesh UI settings.
141141

142142
The `gateways` dictionary defines how SQLMesh should connect to the data warehouse, state backend, test backend, and scheduler.
143143

144-
It takes one or more named `gateway` configuration keys, each of which can define its own connections. A named gateway does not need to specify all four components and will use defaults if any are omitted - more information is provided about [gateway defaults](#gatewayconnection-defaults) below.
144+
It takes one or more named `gateway` configuration keys, each of which can define its own connections. **Gateway names are case-insensitive** - SQLMesh normalizes all gateway names to lowercase during configuration validation, allowing you to use any case when referencing gateways. A named gateway does not need to specify all four components and will use defaults if any are omitted - more information is provided about [gateway defaults](#gatewayconnection-defaults) below.
145145

146146
For example, a project might configure the `gate1` and `gate2` gateways:
147147

@@ -247,7 +247,7 @@ If a configuration contains multiple gateways, SQLMesh will use the first one in
247247

248248
| Option | Description | Type | Required |
249249
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | :----: | :------: |
250-
| `default_gateway` | The name of a gateway to use if one is not provided explicitly (Default: the gateway defined first in the `gateways` option) | string | N |
250+
| `default_gateway` | The name of a gateway to use if one is not provided explicitly (Default: the gateway defined first in the `gateways` option). Gateway names are case-insensitive. | string | N |
251251

252252
### Default connections/scheduler
253253

sqlmesh/core/config/root.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
6262
GatewayConfig.parse_obj(value)
6363
return {"": value}
6464
except Exception:
65+
# Normalize all gateway keys to lowercase for case-insensitive matching
66+
if isinstance(value, dict):
67+
return {k.lower(): v for k, v in value.items()}
6568
return value
6669

6770

@@ -298,19 +301,23 @@ def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig:
298301
if isinstance(self.gateways, dict):
299302
if name is None:
300303
if self.default_gateway:
301-
if self.default_gateway not in self.gateways:
304+
# Normalize default_gateway name to lowercase for lookup
305+
default_key = self.default_gateway.lower()
306+
if default_key not in self.gateways:
302307
raise ConfigError(f"Missing gateway with name '{self.default_gateway}'")
303-
return self.gateways[self.default_gateway]
308+
return self.gateways[default_key]
304309

305310
if "" in self.gateways:
306311
return self.gateways[""]
307312

308313
return first(self.gateways.values())
309314

310-
if name not in self.gateways:
315+
# Normalize lookup name to lowercase since gateway keys are already lowercase
316+
lookup_key = name.lower()
317+
if lookup_key not in self.gateways:
311318
raise ConfigError(f"Missing gateway with name '{name}'.")
312319

313-
return self.gateways[name]
320+
return self.gateways[lookup_key]
314321
if name is not None:
315322
raise ConfigError("Gateway name is not supported when only one gateway is configured.")
316323
return self.gateways

sqlmesh/core/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,9 @@ def __init__(
400400
self.environment_ttl = self.config.environment_ttl
401401
self.pinned_environments = Environment.sanitize_names(self.config.pinned_environments)
402402
self.auto_categorize_changes = self.config.plan.auto_categorize_changes
403-
self.selected_gateway = gateway or self.config.default_gateway_name
403+
self.selected_gateway = (gateway or self.config.default_gateway_name).lower()
404404

405-
gw_model_defaults = self.config.gateways[self.selected_gateway].model_defaults
405+
gw_model_defaults = self.config.get_gateway(self.selected_gateway).model_defaults
406406
if gw_model_defaults:
407407
# Merge global model defaults with the selected gateway's, if it's overriden
408408
global_defaults = self.config.model_defaults.model_dump(exclude_unset=True)

sqlmesh/core/loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ def _load(path: Path) -> t.List[Model]:
374374

375375
# however, if there is a gateway defined, gateway-specific models take precedence
376376
if gateway:
377+
gateway = gateway.lower()
377378
for model in external_models:
378379
if model.gateway == gateway:
379380
if model.fqn in models and models[model.fqn].gateway == gateway:

tests/core/test_context.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,12 @@ def _get_external_model_names(gateway=None):
12211221
# gateway explicitly set to prod; prod model should now show
12221222
assert "prod_raw.model1" in _get_external_model_names(gateway="prod")
12231223

1224+
# test uppercase gateway name should match lowercase external model definition
1225+
assert "prod_raw.model1" in _get_external_model_names(gateway="PROD")
1226+
1227+
# test mixed case gateway name should also work
1228+
assert "prod_raw.model1" in _get_external_model_names(gateway="Prod")
1229+
12241230

12251231
def test_disabled_model(copy_to_temp_path):
12261232
path = copy_to_temp_path("examples/sushi")
@@ -2867,3 +2873,132 @@ def test_model_defaults_statements_with_on_virtual_update(tmp_path: Path):
28672873
# Default statements should come first
28682874
assert model.on_virtual_update[0].sql() == "SELECT 'Model-defailt virtual update' AS message"
28692875
assert model.on_virtual_update[1].sql() == "SELECT 'Model-specific update' AS message"
2876+
2877+
2878+
def test_uppercase_gateway_external_models(tmp_path):
2879+
# Create a temporary SQLMesh project with uppercase gateway name
2880+
config_py = tmp_path / "config.py"
2881+
config_py.write_text("""
2882+
from sqlmesh.core.config import Config, DuckDBConnectionConfig, GatewayConfig, ModelDefaultsConfig
2883+
2884+
config = Config(
2885+
gateways={
2886+
"UPPERCASE_GATEWAY": GatewayConfig(
2887+
connection=DuckDBConnectionConfig(),
2888+
),
2889+
},
2890+
default_gateway="UPPERCASE_GATEWAY",
2891+
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
2892+
)
2893+
""")
2894+
2895+
# Create external models file with lowercase gateway name (this should still match uppercase)
2896+
external_models_yaml = tmp_path / "external_models.yaml"
2897+
external_models_yaml.write_text("""
2898+
- name: test_db.uppercase_gateway_table
2899+
description: Test external model with lowercase gateway name that should match uppercase gateway
2900+
gateway: uppercase_gateway # lowercase in external model, but config has UPPERCASE_GATEWAY
2901+
columns:
2902+
id: int
2903+
name: text
2904+
2905+
- name: test_db.no_gateway_table
2906+
description: Test external model without gateway (should be available for all gateways)
2907+
columns:
2908+
id: int
2909+
name: text
2910+
""")
2911+
2912+
# Create a model that references the external model
2913+
models_dir = tmp_path / "models"
2914+
models_dir.mkdir()
2915+
model_sql = models_dir / "test_model.sql"
2916+
model_sql.write_text("""
2917+
MODEL (
2918+
name test.my_model,
2919+
kind FULL,
2920+
);
2921+
2922+
SELECT * FROM test_db.uppercase_gateway_table;
2923+
""")
2924+
2925+
# Test with uppercase gateway name - this should find both models
2926+
context_uppercase = Context(paths=[tmp_path], gateway="UPPERCASE_GATEWAY")
2927+
2928+
# Verify external model with lowercase gateway name in YAML is found when using uppercase gateway
2929+
gateway_specific_models = [
2930+
model
2931+
for model in context_uppercase.models.values()
2932+
if model.name == "test_db.uppercase_gateway_table"
2933+
]
2934+
assert len(gateway_specific_models) == 1, (
2935+
f"External model with lowercase gateway name should be found with uppercase gateway. Found {len(gateway_specific_models)} models"
2936+
)
2937+
2938+
# Verify external model without gateway is also found
2939+
no_gateway_models = [
2940+
model
2941+
for model in context_uppercase.models.values()
2942+
if model.name == "test_db.no_gateway_table"
2943+
]
2944+
assert len(no_gateway_models) == 1, (
2945+
f"External model without gateway should be found. Found {len(no_gateway_models)} models"
2946+
)
2947+
2948+
# Check that the column types are properly loaded (not UNKNOWN)
2949+
external_model = gateway_specific_models[0]
2950+
column_types = {name: str(dtype) for name, dtype in external_model.columns_to_types.items()}
2951+
assert column_types == {"id": "INT", "name": "TEXT"}, (
2952+
f"External model column types should not be UNKNOWN, got: {column_types}"
2953+
)
2954+
2955+
# Test that when using a different case for the gateway parameter, we get the same results
2956+
context_mixed_case = Context(
2957+
paths=[tmp_path], gateway="uppercase_gateway"
2958+
) # lowercase parameter
2959+
2960+
gateway_specific_models_mixed = [
2961+
model
2962+
for model in context_mixed_case.models.values()
2963+
if model.name == "test_db.uppercase_gateway_table"
2964+
]
2965+
# This should work but might fail if case sensitivity is not handled correctly
2966+
assert len(gateway_specific_models_mixed) == 1, (
2967+
f"External model should be found regardless of gateway parameter case. Found {len(gateway_specific_models_mixed)} models"
2968+
)
2969+
2970+
# Test a case that should demonstrate the potential issue:
2971+
# Create another external model file with uppercase gateway name in the YAML
2972+
external_models_yaml_uppercase = tmp_path / "external_models_uppercase.yaml"
2973+
external_models_yaml_uppercase.write_text("""
2974+
- name: test_db.uppercase_in_yaml
2975+
description: Test external model with uppercase gateway name in YAML
2976+
gateway: UPPERCASE_GATEWAY # uppercase in external model yaml
2977+
columns:
2978+
id: int
2979+
status: text
2980+
""")
2981+
2982+
# Add the new external models file to the project
2983+
models_dir = tmp_path / "external_models"
2984+
models_dir.mkdir(exist_ok=True)
2985+
(models_dir / "uppercase_gateway_models.yaml").write_text("""
2986+
- name: test_db.uppercase_in_yaml
2987+
description: Test external model with uppercase gateway name in YAML
2988+
gateway: UPPERCASE_GATEWAY # uppercase in external model yaml
2989+
columns:
2990+
id: int
2991+
status: text
2992+
""")
2993+
2994+
# Reload context to pick up the new external models
2995+
context_reloaded = Context(paths=[tmp_path], gateway="UPPERCASE_GATEWAY")
2996+
2997+
uppercase_in_yaml_models = [
2998+
model
2999+
for model in context_reloaded.models.values()
3000+
if model.name == "test_db.uppercase_in_yaml"
3001+
]
3002+
assert len(uppercase_in_yaml_models) == 1, (
3003+
f"External model with uppercase gateway in YAML should be found. Found {len(uppercase_in_yaml_models)} models"
3004+
)

0 commit comments

Comments
 (0)