Skip to content

Commit d513f24

Browse files
authored
fix: prevent past ttl values for environment and snapshot (#4158)
1 parent 27d09c4 commit d513f24

2 files changed

Lines changed: 85 additions & 22 deletions

File tree

sqlmesh/core/config/root.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from sqlmesh.core.loader import Loader, SqlMeshLoader
4242
from sqlmesh.core.notification_target import NotificationTarget
4343
from sqlmesh.core.user import User
44+
from sqlmesh.utils.date import to_timestamp, now, now_timestamp
4445
from sqlmesh.utils.errors import ConfigError
4546
from sqlmesh.utils.pydantic import field_validator, model_validator
4647

@@ -88,7 +89,7 @@ class Config(BaseConfig):
8889
after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands.
8990
"""
9091

91-
gateways: t.Dict[str, GatewayConfig] = {"": GatewayConfig()}
92+
gateways: GatewayDict = {"": GatewayConfig()}
9293
default_connection: SerializableConnectionConfig = DuckDBConnectionConfig()
9394
default_test_connection_: t.Optional[SerializableConnectionConfig] = Field(
9495
default=None, alias="default_test_connection"
@@ -97,8 +98,8 @@ class Config(BaseConfig):
9798
default_gateway: str = ""
9899
notification_targets: t.List[NotificationTarget] = []
99100
project: str = ""
100-
snapshot_ttl: str = c.DEFAULT_SNAPSHOT_TTL
101-
environment_ttl: t.Optional[str] = c.DEFAULT_ENVIRONMENT_TTL
101+
snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL
102+
environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL
102103
ignore_patterns: t.List[str] = c.IGNORE_PATTERNS
103104
time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT
104105
users: t.List[User] = []
@@ -108,12 +109,12 @@ class Config(BaseConfig):
108109
loader_kwargs: t.Dict[str, t.Any] = {}
109110
env_vars: t.Dict[str, str] = {}
110111
username: str = ""
111-
physical_schema_mapping: t.Dict[re.Pattern, str] = {}
112+
physical_schema_mapping: RegexKeyDict = {}
112113
environment_suffix_target: EnvironmentSuffixTarget = Field(
113114
default=EnvironmentSuffixTarget.default
114115
)
115116
gateway_managed_virtual_layer: bool = False
116-
environment_catalog_mapping: t.Dict[re.Pattern, str] = {}
117+
environment_catalog_mapping: RegexKeyDict = {}
117118
default_target_environment: str = c.PROD
118119
log_limit: int = c.DEFAULT_LOG_LIMIT
119120
cicd_bot: t.Optional[CICDBotConfig] = None
@@ -154,23 +155,6 @@ class Config(BaseConfig):
154155
_scheduler_config_validator = scheduler_config_validator # type: ignore
155156
_variables_validator = variables_validator
156157

157-
@field_validator("gateways", mode="before")
158-
@classmethod
159-
def _gateways_ensure_dict(cls, value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
160-
try:
161-
if not isinstance(value, GatewayConfig):
162-
GatewayConfig.parse_obj(value)
163-
return {"": value}
164-
except Exception:
165-
return value
166-
167-
@field_validator("environment_catalog_mapping", "physical_schema_mapping", mode="before")
168-
@classmethod
169-
def _validate_regex_keys(
170-
cls, value: t.Dict[str | re.Pattern, t.Any]
171-
) -> t.Dict[re.Pattern, t.Any]:
172-
return compile_regex_mapping(value)
173-
174158
@model_validator(mode="before")
175159
def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any:
176160
if not isinstance(data, dict):
@@ -302,3 +286,37 @@ def dialect(self) -> t.Optional[str]:
302286
@property
303287
def fingerprint(self) -> str:
304288
return str(zlib.crc32(pickle.dumps(self.dict(exclude={"loader", "notification_targets"}))))
289+
290+
291+
def validate_no_past_ttl(v: str) -> str:
292+
current_time = now()
293+
if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time):
294+
raise ValueError(
295+
f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`."
296+
)
297+
return v
298+
299+
300+
def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
301+
try:
302+
if not isinstance(value, GatewayConfig):
303+
GatewayConfig.parse_obj(value)
304+
return {"": value}
305+
except Exception:
306+
return value
307+
308+
309+
def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re.Pattern, t.Any]:
310+
return compile_regex_mapping(value)
311+
312+
313+
if t.TYPE_CHECKING:
314+
NoPastTTLString = str
315+
GatewayDict = t.Dict[str, GatewayConfig]
316+
RegexKeyDict = t.Dict[re.Pattern, str]
317+
else:
318+
from pydantic.functional_validators import BeforeValidator
319+
320+
NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)]
321+
GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)]
322+
RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)]

tests/integrations/github/cicd/test_config.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,48 @@ def test_validation(tmp_path):
200200
ValueError, match="merge_method must be set if enable_deploy_command is True"
201201
):
202202
load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"])
203+
204+
205+
def test_ttl_in_past(tmp_path):
206+
create_temp_file(
207+
tmp_path,
208+
pathlib.Path("config.yaml"),
209+
"""
210+
environment_ttl: in 1 week
211+
model_defaults:
212+
dialect: duckdb
213+
""",
214+
)
215+
216+
config = load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"])
217+
assert config.environment_ttl == "in 1 week"
218+
219+
create_temp_file(
220+
tmp_path,
221+
pathlib.Path("config.yaml"),
222+
"""
223+
environment_ttl: 1 week
224+
model_defaults:
225+
dialect: duckdb
226+
""",
227+
)
228+
with pytest.raises(
229+
ValueError,
230+
match="TTL '1 week' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`.",
231+
):
232+
load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"])
233+
234+
create_temp_file(
235+
tmp_path,
236+
pathlib.Path("config.yaml"),
237+
"""
238+
snapshot_ttl: 1 week
239+
model_defaults:
240+
dialect: duckdb
241+
""",
242+
)
243+
with pytest.raises(
244+
ValueError,
245+
match="TTL '1 week' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`.",
246+
):
247+
load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"])

0 commit comments

Comments
 (0)