Skip to content

Commit da4db5c

Browse files
Feat: Add support for dot env variables
1 parent f43a4c3 commit da4db5c

File tree

3 files changed

+134
-1
lines changed

3 files changed

+134
-1
lines changed

docs/guides/configuration.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,32 @@ All software runs within a system environment that stores information as "enviro
9898

9999
SQLMesh can access environment variables during configuration, which enables approaches like storing passwords/secrets outside the configuration file and changing configuration parameters dynamically based on which user is running SQLMesh.
100100

101-
You can use environment variables in two ways: specifying them in the configuration file or creating properly named variables to override configuration file values.
101+
You can specify environment variables in the configuration file or by storing them in a `.env` file.
102+
103+
### .env files
104+
105+
SQLMesh automatically loads environment variables from a `.env` file in your project directory. This provides a convenient way to manage environment variables without having to set them in your shell.
106+
107+
Create a `.env` file in your project root with key-value pairs:
108+
109+
```bash
110+
# .env file
111+
SNOWFLAKE_PW=my_secret_password
112+
S3_BUCKET=s3://my-data-bucket/warehouse
113+
DATABASE_URL=postgresql://user:pass@localhost/db
114+
115+
# Override specific SQLMesh configuration values
116+
SQLMESH__DEFAULT_GATEWAY=production
117+
SQLMESH__MODEL_DEFAULTS__DIALECT=snowflake
118+
```
119+
120+
See the [overrides](#overrides) section for a detailed explanation of how these are defined.
121+
122+
The rest of the `.env` file variables can be used in your configuration files with `{{ env_var('VARIABLE_NAME') }}` syntax in YAML or accessed via `os.environ['VARIABLE_NAME']` in Python.
123+
124+
**Important considerations:**
125+
- Add `.env` to your `.gitignore` file to avoid committing sensitive information
126+
- SQLMesh will only load the `.env` file if it exists in the project directory
102127

103128
### Configuration file
104129

sqlmesh/core/config/loader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77

88
from pydantic import ValidationError
9+
from dotenv import load_dotenv
910
from sqlglot.helper import ensure_list
1011

1112
from sqlmesh.core import constants as c
@@ -35,6 +36,9 @@ def load_configs(
3536
for p in (glob.glob(str(path)) or [str(path)])
3637
]
3738

39+
for path in absolute_paths:
40+
load_dotenv(dotenv_path=path / ".env", override=True)
41+
3842
if not isinstance(config, str):
3943
if type(config) != config_type:
4044
config = convert_config_type(config, config_type)

tests/core/test_config.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
load_config_from_env,
2525
load_config_from_paths,
2626
load_config_from_python_module,
27+
load_configs,
2728
)
2829
from sqlmesh.core.context import Context
2930
from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter
@@ -1132,3 +1133,106 @@ def test_environment_suffix_target_catalog(tmp_path: Path) -> None:
11321133
Config,
11331134
project_paths=[config_path],
11341135
)
1136+
1137+
1138+
def test_load_python_config_dot_env_vars(tmp_path_factory):
1139+
main_dir = tmp_path_factory.mktemp("python_config")
1140+
config_path = main_dir / "config.py"
1141+
with open(config_path, "w", encoding="utf-8") as fd:
1142+
fd.write(
1143+
"""from sqlmesh.core.config import Config, DuckDBConnectionConfig, GatewayConfig, ModelDefaultsConfig
1144+
config = Config(gateways={"duckdb_gateway": GatewayConfig(connection=DuckDBConnectionConfig())}, model_defaults=ModelDefaultsConfig(dialect=''))
1145+
"""
1146+
)
1147+
1148+
# The environment variable value from the dot env file should be set
1149+
# SQLMESH__ variables override config fields directly if they follow the naming structure
1150+
dot_path = main_dir / ".env"
1151+
with open(dot_path, "w", encoding="utf-8") as fd:
1152+
fd.write(
1153+
"""SQLMESH__GATEWAYS__DUCKDB_GATEWAY__STATE_CONNECTION__TYPE="bigquery"
1154+
SQLMESH__GATEWAYS__DUCKDB_GATEWAY__STATE_CONNECTION__CHECK_IMPORT="false"
1155+
SQLMESH__DEFAULT_GATEWAY="duckdb_gateway"
1156+
"""
1157+
)
1158+
1159+
# Use mock.patch.dict to isolate environment variables between the tests
1160+
with mock.patch.dict(os.environ, {}, clear=True):
1161+
configs = load_configs(
1162+
"config",
1163+
Config,
1164+
paths=[main_dir],
1165+
)
1166+
1167+
assert next(iter(configs.values())) == Config(
1168+
gateways={
1169+
"duckdb_gateway": GatewayConfig(
1170+
connection=DuckDBConnectionConfig(),
1171+
state_connection=BigQueryConnectionConfig(check_import=False),
1172+
),
1173+
},
1174+
model_defaults=ModelDefaultsConfig(dialect=""),
1175+
default_gateway="duckdb_gateway",
1176+
)
1177+
1178+
1179+
def test_load_yaml_config_dot_env_vars(tmp_path_factory):
1180+
main_dir = tmp_path_factory.mktemp("yaml_config")
1181+
config_path = main_dir / "config.yaml"
1182+
with open(config_path, "w", encoding="utf-8") as fd:
1183+
fd.write(
1184+
"""gateways:
1185+
duckdb_gateway:
1186+
connection:
1187+
type: duckdb
1188+
catalogs:
1189+
local: local.db
1190+
cloud_sales: {{ env_var('S3_BUCKET') }}
1191+
extensions:
1192+
- name: httpfs
1193+
secrets:
1194+
- type: "s3"
1195+
key_id: {{ env_var('S3_KEY') }}
1196+
secret: {{ env_var('S3_SECRET') }}
1197+
model_defaults:
1198+
dialect: ""
1199+
"""
1200+
)
1201+
1202+
# This test checks both using SQLMESH__ prefixed environment variables with underscores
1203+
# and setting a regular environment variable for use with env_var().
1204+
dot_path = main_dir / ".env"
1205+
with open(dot_path, "w", encoding="utf-8") as fd:
1206+
fd.write(
1207+
"""S3_BUCKET="s3://metrics_bucket/sales.db"
1208+
S3_KEY="S3_KEY_ID"
1209+
S3_SECRET="XXX_S3_SECRET_XXX"
1210+
SQLMESH__DEFAULT_GATEWAY="duckdb_gateway"
1211+
SQLMESH__MODEL_DEFAULTS__DIALECT="athena"
1212+
"""
1213+
)
1214+
1215+
# Use mock.patch.dict to isolate environment variables between the tests
1216+
with mock.patch.dict(os.environ, {}, clear=True):
1217+
configs = load_configs(
1218+
"config",
1219+
Config,
1220+
paths=[main_dir],
1221+
)
1222+
1223+
assert next(iter(configs.values())) == Config(
1224+
gateways={
1225+
"duckdb_gateway": GatewayConfig(
1226+
connection=DuckDBConnectionConfig(
1227+
catalogs={
1228+
"local": "local.db",
1229+
"cloud_sales": "s3://metrics_bucket/sales.db",
1230+
},
1231+
extensions=[{"name": "httpfs"}],
1232+
secrets=[{"type": "s3", "key_id": "S3_KEY_ID", "secret": "XXX_S3_SECRET_XXX"}],
1233+
),
1234+
),
1235+
},
1236+
default_gateway="duckdb_gateway",
1237+
model_defaults=ModelDefaultsConfig(dialect="athena"),
1238+
)

0 commit comments

Comments
 (0)