From ecc4784c8d84b4f27f7710a48b9e95c80c518dcc Mon Sep 17 00:00:00 2001 From: mday-io Date: Thu, 6 Aug 2026 18:29:34 +0000 Subject: [PATCH 01/67] fix(format): render warehouse-SQL header properties with the model dialect #5864 stopped transpiling MODEL/AUDIT/METRIC headers so that SQLMesh's own boolean properties would survive formatting -- on tsql, `allow_partials TRUE` was being rewritten to `(1 = 1)`, which then fails to parse at all and leaves the model file broken. That fix rendered the entire header generically, including the properties whose values are the user's warehouse SQL. Those lose their dialect: `columns (ts DATETIME2(6))` becomes `TIMESTAMP(6)`, and an audit argument such as `CAST('2024-01-01' AS DATETIME2)` is silently downgraded the same way. Split the header per property instead of per expression. The split is derived from the field declarations themselves: expression-typed fields (columns, audits, signals, partitioned_by, physical_properties, ...) hold warehouse SQL and render with the model dialect, while scalar-typed fields (allow_partials, description, kind, ...) are SQLMesh's own semantics and stay dialect-agnostic. Deriving it means the policy stays correct as properties are added, and a field that is missed fails safe -- a keyword is not canonicalized, rather than a user's SQL being corrupted. Covers MODEL, AUDIT and METRIC headers, and the expression properties nested inside `kind` such as `time_data_type` and `unique_key`. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 94 +++++++++++++++++++++++++++++++-- tests/core/test_dialect.py | 103 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index e4ab522198..d2f6fffd13 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -734,6 +734,52 @@ def parse(self: Parser) -> t.Optional[exp.Expr]: } +_SQLMESH_META_DIALECT = "sqlmesh_meta_dialect" + + +def _holds_expression(annotation: t.Any) -> bool: + """Whether a declared field type bottoms out in a SQLGlot expression. + + Covers List[exp.Expr], Optional[Dict[str, exp.DataType]], Optional[exp.Tuple] and + the nested Tuple[str, Dict[str, exp.Expr]] shape used by audits/signals. + """ + if isinstance(annotation, type) and issubclass(annotation, exp.Expr): + return True + return any(_holds_expression(arg) for arg in t.get_args(annotation)) + + +@functools.lru_cache(maxsize=1) +def _meta_render_policy() -> t.Dict[str, bool]: + """Map header property name -> whether its value is warehouse SQL. + + Derived from the field declarations themselves, so it stays correct as properties + are added: expression-typed values (columns, audits, physical_properties, ...) are + the user's warehouse SQL and must render in the model's dialect, while scalar-typed + values (allow_partials, description, kind, ...) are SQLMesh's own semantics and must + stay dialect-agnostic -- transpiling those is what corrupts `allow_partials TRUE` + into tsql's unparseable `(1 = 1)`. + """ + import inspect + + from sqlmesh.core.audit.definition import ModelAudit + from sqlmesh.core.metric.definition import MetricMeta + from sqlmesh.core.model import kind as kind_module + from sqlmesh.core.model.meta import ModelMeta + + sources: t.List[t.Any] = [ModelMeta, ModelAudit, MetricMeta] + sources.extend( + obj + for name, obj in vars(kind_module).items() + if inspect.isclass(obj) and hasattr(obj, "model_fields") and name.endswith("Kind") + ) + + policy: t.Dict[str, bool] = {} + for source in sources: + for name, field in source.model_fields.items(): + policy.setdefault((field.alias or name).lower(), _holds_expression(field.annotation)) + return policy + + def _props_sql(self: Generator, expressions: t.List[exp.Expr]) -> str: props = [] size = len(expressions) @@ -742,7 +788,31 @@ def _props_sql(self: Generator, expressions: t.List[exp.Expr]) -> str: if isinstance(prop, MacroFunc): sql = self.indent(self.sql(prop, comment=False)) else: - sql = self.indent(f"{prop.name} {self.sql(prop, 'value')}") + value = prop.args.get("value") + parent = prop.parent + meta_dialect = parent.meta.get(_SQLMESH_META_DIALECT) if parent else None + + if ( + meta_dialect + and isinstance(value, exp.Expr) + and _meta_render_policy().get(prop.name.lower()) + ): + value_sql = value.sql( + dialect=meta_dialect, + pretty=self.pretty, + identify=self.identify, + normalize=self.normalize, + pad=self.pad, + indent=self._indent, + normalize_functions=self.normalize_functions, + leading_comma=self.leading_comma, + max_text_width=self.max_text_width, + comments=self.comments, + ) + else: + value_sql = self.sql(prop, "value") + + sql = self.indent(f"{prop.name} {value_sql}") if i < size - 1: sql += "," @@ -853,11 +923,29 @@ def format_model_expressions( Returns: A string representing the formatted model. """ + + def tag_meta_dialect(expression: exp.Expr) -> exp.Expr: + """Record the model dialect on meta nodes so `_props_sql` can render the + warehouse-SQL properties (columns, audits, physical_properties, ...) with it + while the SQLMesh-owned ones stay dialect-agnostic. Tags nested ModelKind + nodes too, since kinds carry expression properties of their own such as + `time_data_type` and `unique_key`.""" + if not dialect or not is_meta_expression(expression): + return expression + + expression = expression.copy() + for node in expression.find_all(Model, Audit, Metric, ModelKind): + node.meta[_SQLMESH_META_DIALECT] = dialect + expression.meta[_SQLMESH_META_DIALECT] = dialect + return expression + if len(expressions) == 1 and is_meta_expression(expressions[0]): # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL, not standard SQL, # so they must never be transpiled to the target dialect (e.g. tsql would # rewrite a boolean property like `allow_partials TRUE` to `(1 = 1)`). - return expressions[0].sql( + # Individual properties whose values *are* warehouse SQL still render with + # the model dialect -- see `_props_sql` / `_meta_render_policy`. + return tag_meta_dialect(expressions[0]).sql( pretty=True, dialect=None, normalize_functions=normalize_functions ) @@ -893,7 +981,7 @@ def cast_to_colon(node: exp.Expr) -> exp.Expr: return ";\n\n".join( # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL and must stay # dialect-agnostic; only the actual query/statement expressions transpile. - expression.sql( + tag_meta_dialect(expression).sql( pretty=True, dialect=None if is_meta_expression(expression) else dialect, normalize_functions=normalize_functions, diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 142b40b31f..999e205af7 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -342,6 +342,109 @@ def test_format_model_expressions(): ) +@pytest.mark.parametrize( + "dialect,audit_type,int_type", + # These dialects spell the same types differently -- fabric renders a bare DATETIME2 + # with its default precision and keeps INT, tsql does the reverse. The point of the + # test is that each keeps *its own* spelling rather than being flattened. + [("tsql", "DATETIME2", "INTEGER"), ("fabric", "DATETIME2(6)", "INT")], +) +def test_format_model_expressions_meta_render_policy(dialect: str, audit_type: str, int_type: str): + """Header properties whose values are warehouse SQL render with the model dialect, + while SQLMesh's own properties stay dialect-agnostic. + + Rendering the whole header with the dialect corrupts SQLMesh DDL (tsql turns + `allow_partials TRUE` into the unparseable `(1 = 1)`), but rendering all of it + generically discards dialect-specific values the user authored, such as the + `DATETIME2` types below. The split is derived from the field declarations, so it + covers `columns`, `audits`, `physical_properties` and the expression properties + nested inside `kind` alike. + """ + formatted = format_model_expressions( + parse( + f""" + MODEL ( + name a.b, + dialect {dialect}, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + time_data_type DATETIME2(6) + ), + allow_partials true, + description 'my description', + columns ( + ts DATETIME2(6) + ), + audits ( + my_audit(threshold := CAST('2024-01-01' AS DATETIME2)) + ), + physical_properties ( + labels = (('env', 'prod')) + ) + ); + + SELECT CAST(x AS INT) AS y FROM t + """ + ), + dialect=dialect, + ) + + assert ( + formatted + == f"""MODEL ( + name a.b, + dialect {dialect}, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + time_data_type DATETIME2(6) + ), + allow_partials TRUE, + description 'my description', + columns ( + ts DATETIME2(6) + ), + audits ( + my_audit(threshold := '2024-01-01'::{audit_type}) + ), + physical_properties ( + labels = ( + ('env', 'prod') + ) + ) +); + +SELECT + x::{int_type} AS y +FROM t""" + ) + + +def test_format_audit_expressions_meta_render_policy(): + """AUDIT headers have their own meta model, and get the same split: `blocking` is + SQLMesh's own boolean and must not become tsql's `(1 = 0)`, while `defaults` holds + user expressions and keeps its dialect-specific type.""" + formatted = format_model_expressions( + parse( + """ + AUDIT ( + name my_audit, + dialect tsql, + blocking false, + defaults ( + cutoff := CAST('2024-01-01' AS DATETIME2) + ) + ); + + SELECT * FROM t WHERE x > 0 + """ + ), + dialect="tsql", + ) + + assert "blocking FALSE" in formatted + assert "cutoff := '2024-01-01'::DATETIME2" in formatted + + def test_format_model_expressions_normalize_functions(): """Regression: formatter function-name casing behavior. From 0a71611db69eb30ae25082c2d87cb7e08724d2b3 Mon Sep 17 00:00:00 2001 From: mday-io Date: Thu, 6 Aug 2026 21:32:40 +0000 Subject: [PATCH 02/67] test(format): assert header formatting is idempotent Rendering a dialect-specific type with the generic generator compounds across runs rather than merely looking different: tsql `DATETIME2` renders as `TIMESTAMP`, and tsql parses `TIMESTAMP` as ROWVERSION, so a second pass writes `VARBINARY`. Two runs of `sqlmesh format` silently turned a datetime into a binary type -- and for an SCD kind's `time_data_type` that is the physical type of the valid_from/valid_to columns. Covers columns, audits, nested kind properties, physical_properties and the SQLMesh-owned scalars. Signed-off-by: mday-io --- tests/core/test_dialect.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 999e205af7..870e98e64d 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -419,6 +419,33 @@ def test_format_model_expressions_meta_render_policy(dialect: str, audit_type: s ) +@pytest.mark.parametrize( + "header", + [ + "columns (ts DATETIME2(6))", + "audits (my_audit(t := CAST('2024-01-01' AS DATETIME2)))", + "kind SCD_TYPE_2_BY_COLUMN(unique_key id, columns (a, b), time_data_type DATETIME2(6))", + "physical_properties (labels = (('env', 'prod')))", + "allow_partials true, description 'my description'", + ], +) +def test_format_model_expressions_is_idempotent(header: str): + """Formatting an already-formatted model must be a no-op. + + Rendering a dialect-specific type with the generic generator does not merely lose + formatting, it compounds: tsql `DATETIME2` renders as `TIMESTAMP`, and tsql parses + `TIMESTAMP` as ROWVERSION (a binary type), so a second pass writes `VARBINARY`. Two + runs of `sqlmesh format` silently turned a datetime into a binary type -- and for + `time_data_type` that is the physical type of the SCD valid_from/valid_to columns. + """ + source = f"MODEL (name a.b, dialect tsql, {header});\nSELECT 1 AS x" + + once = format_model_expressions(parse(source, default_dialect="tsql"), dialect="tsql") + twice = format_model_expressions(parse(once, default_dialect="tsql"), dialect="tsql") + + assert once == twice + + def test_format_audit_expressions_meta_render_policy(): """AUDIT headers have their own meta model, and get the same split: `blocking` is SQLMesh's own boolean and must not become tsql's `(1 = 0)`, while `defaults` holds From f67fd27349ef952f96ae5b990e6c112f984f396b Mon Sep 17 00:00:00 2001 From: mday-io Date: Fri, 7 Aug 2026 12:48:07 +0000 Subject: [PATCH 03/67] fix(format): render macro properties in the header with the model dialect A macro in property position wraps user-authored arguments, so it carries warehouse SQL the same way `columns` or `audits` do. It took a separate branch in _props_sql and kept rendering generically, which left it on the compounding path: DATETIME2 -> TIMESTAMP -> VARBINARY across two format runs. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 42 ++++++++++++++++++++++++-------------- tests/core/test_dialect.py | 1 + 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index d2f6fffd13..4d66ad2a8c 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -785,30 +785,42 @@ def _props_sql(self: Generator, expressions: t.List[exp.Expr]) -> str: size = len(expressions) for i, prop in enumerate(expressions): + parent = prop.parent + meta_dialect = parent.meta.get(_SQLMESH_META_DIALECT) if parent else None + + def render_with_model_dialect(node: exp.Expr, **overrides: t.Any) -> str: + opts: t.Dict[str, t.Any] = { + "dialect": meta_dialect, + "pretty": self.pretty, + "identify": self.identify, + "normalize": self.normalize, + "pad": self.pad, + "indent": self._indent, + "normalize_functions": self.normalize_functions, + "leading_comma": self.leading_comma, + "max_text_width": self.max_text_width, + "comments": self.comments, + } + opts.update(overrides) + return node.sql(**opts) + if isinstance(prop, MacroFunc): - sql = self.indent(self.sql(prop, comment=False)) + # A macro in property position wraps user-authored arguments, so it carries + # warehouse SQL the same way `columns` or `audits` do. + sql = self.indent( + render_with_model_dialect(prop, comments=False) + if meta_dialect + else self.sql(prop, comment=False) + ) else: value = prop.args.get("value") - parent = prop.parent - meta_dialect = parent.meta.get(_SQLMESH_META_DIALECT) if parent else None if ( meta_dialect and isinstance(value, exp.Expr) and _meta_render_policy().get(prop.name.lower()) ): - value_sql = value.sql( - dialect=meta_dialect, - pretty=self.pretty, - identify=self.identify, - normalize=self.normalize, - pad=self.pad, - indent=self._indent, - normalize_functions=self.normalize_functions, - leading_comma=self.leading_comma, - max_text_width=self.max_text_width, - comments=self.comments, - ) + value_sql = render_with_model_dialect(value) else: value_sql = self.sql(prop, "value") diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 870e98e64d..c2e2b6d750 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -427,6 +427,7 @@ def test_format_model_expressions_meta_render_policy(dialect: str, audit_type: s "kind SCD_TYPE_2_BY_COLUMN(unique_key id, columns (a, b), time_data_type DATETIME2(6))", "physical_properties (labels = (('env', 'prod')))", "allow_partials true, description 'my description'", + "@my_prop(cutoff := CAST('2024-01-01' AS DATETIME2))", ], ) def test_format_model_expressions_is_idempotent(header: str): From e6cb87b03a23c5dec3864ef60db78961b38e9101 Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 9 Aug 2026 19:04:15 +0000 Subject: [PATCH 04/67] fix(format): recurse into nested Pydantic models and preserve macro property comments Two gaps in the header-property dialect-render policy from the previous fix: - `_holds_expression` only checked the outer type annotation and typing generics (`Optional`, `List`, ...), so a nested Pydantic model wrapping an expression field -- `TimeColumn` on `IncrementalByTimeRangeKind.time_column` -- was misclassified as a scalar property and fell back to generic rendering, losing dialect-specific identifier quoting (tsql `[end]` became ANSI `"end"`). Recurse into `model_fields` for any type that exposes them, guarded by a visited set. - The `MacroFunc` dialect-render branch passed `comments=False` into `render_with_model_dialect`, which threads it to `Expression.sql()`'s fresh per-call `Generator` constructor -- a generator-wide flag that disables every comment in the subtree, not just the redundant outer `maybe_comment` call. Comments inside macro header-properties (e.g. `@my_prop(cutoff := ... /* note */)`) were silently dropped whenever the model declared a `dialect`. Render a copy of the property with its own top-level comments cleared instead, leaving `.this`'s comments -- which `_macro_func_sql` already attaches -- untouched. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 44 ++++++++++++++----- tests/core/test_dialect.py | 89 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 12 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 4d66ad2a8c..d6af7edb7d 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -737,15 +737,28 @@ def parse(self: Parser) -> t.Optional[exp.Expr]: _SQLMESH_META_DIALECT = "sqlmesh_meta_dialect" -def _holds_expression(annotation: t.Any) -> bool: +def _holds_expression(annotation: t.Any, _visited: t.Optional[t.FrozenSet[t.Any]] = None) -> bool: """Whether a declared field type bottoms out in a SQLGlot expression. - Covers List[exp.Expr], Optional[Dict[str, exp.DataType]], Optional[exp.Tuple] and - the nested Tuple[str, Dict[str, exp.Expr]] shape used by audits/signals. + Covers List[exp.Expr], Optional[Dict[str, exp.DataType]], Optional[exp.Tuple], the + nested Tuple[str, Dict[str, exp.Expr]] shape used by audits/signals, and nested + Pydantic models that themselves wrap an expression field, such as `TimeColumn` + (IncrementalByTimeRangeKind.time_column). """ - if isinstance(annotation, type) and issubclass(annotation, exp.Expr): - return True - return any(_holds_expression(arg) for arg in t.get_args(annotation)) + if isinstance(annotation, type): + if issubclass(annotation, exp.Expr): + return True + visited = _visited or frozenset() + if annotation in visited: + return False + if hasattr(annotation, "model_fields"): + visited = visited | {annotation} + return any( + _holds_expression(field.annotation, visited) + for field in annotation.model_fields.values() + ) + return False + return any(_holds_expression(arg, _visited) for arg in t.get_args(annotation)) @functools.lru_cache(maxsize=1) @@ -806,12 +819,19 @@ def render_with_model_dialect(node: exp.Expr, **overrides: t.Any) -> str: if isinstance(prop, MacroFunc): # A macro in property position wraps user-authored arguments, so it carries - # warehouse SQL the same way `columns` or `audits` do. - sql = self.indent( - render_with_model_dialect(prop, comments=False) - if meta_dialect - else self.sql(prop, comment=False) - ) + # warehouse SQL the same way `columns` or `audits` do. Clear the outer node's + # own comments (not `.this`'s, which `_macro_func_sql` already attaches) + # before rendering with the model dialect, mirroring what `comment=False` + # does for the non-dialect path below -- passing `comments=False` here + # instead would build a fresh Generator with comments globally disabled, + # silently dropping every comment in the subtree rather than just the + # redundant outer one. + if meta_dialect: + prop_for_render = prop.copy() + prop_for_render.comments = None + sql = self.indent(render_with_model_dialect(prop_for_render)) + else: + sql = self.indent(self.sql(prop, comment=False)) else: value = prop.args.get("value") diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index c2e2b6d750..0ef9666cf3 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -473,6 +473,95 @@ def test_format_audit_expressions_meta_render_policy(): assert "cutoff := '2024-01-01'::DATETIME2" in formatted +def test_format_model_expressions_time_column_dialect(): + """`time_column` is a nested Pydantic model (`TimeColumn`) wrapping an expression, not + an `exp.Expr` annotation itself, so the render-policy reflection must recurse into + nested Pydantic models to classify it as warehouse SQL. Otherwise it falls back to + generic rendering and loses dialect-specific identifier quoting: tsql's `[end]` + becomes ANSI `"end"`, even though the same identifier in the query body is correctly + kept as `[end]`. + """ + formatted = format_model_expressions( + parse( + """ + MODEL ( + name a.b, + dialect tsql, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column [end] + ) + ); + + SELECT 1 AS x, [end] FROM t + """, + default_dialect="tsql", + ), + dialect="tsql", + ) + + assert ( + formatted + == """MODEL ( + name a.b, + dialect tsql, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column [end] + ) +); + +SELECT + 1 AS x, + [end] +FROM t""" + ) + + +def test_format_model_expressions_macro_property_comments_preserved_with_dialect(): + """Comments inside a macro header-property must survive formatting when the model + has a `dialect` set. + + The dialect-render path goes through `Expression.sql(dialect=...)`, which builds a + fresh `Generator` with `comments` as a constructor flag: passing `comments=False` + there disables comment rendering for the *entire* subtree, rather than just + suppressing the redundant outer-level `maybe_comment` call the way `comment=False` + does for `Generator.sql()`. That previously caused comments like `/* inline note */` + to be silently dropped whenever the model declared a `dialect`. + """ + formatted = format_model_expressions( + parse( + """ + MODEL ( + name a.b, + dialect tsql, + @my_prop(cutoff := CAST('2024-01-01' AS DATETIME2) /* inline note */) + ); + + SELECT 1 AS x + """, + default_dialect="tsql", + ), + dialect="tsql", + ) + + assert "/* inline note */" in formatted + assert ( + formatted + == """MODEL ( + name a.b, + dialect tsql, + @my_prop(cutoff := '2024-01-01'::DATETIME2 /* inline note */) +); + +SELECT + 1 AS x""" + ) + + # Idempotency: formatting an already-formatted macro property must not duplicate or + # drop the comment on a second pass. + twice = format_model_expressions(parse(formatted, default_dialect="tsql"), dialect="tsql") + assert formatted == twice + + def test_format_model_expressions_normalize_functions(): """Regression: formatter function-name casing behavior. From 157e38b765a82d6fba7c3ad42366c987c5be65d3 Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 9 Aug 2026 20:51:16 +0000 Subject: [PATCH 05/67] fix(format): stop kind's own render policy from transpiling scalar siblings Recursing _holds_expression into nested Pydantic models to correctly classify TimeColumn (IncrementalByTimeRangeKind.time_column) as warehouse SQL had the side effect of also matching ModelMeta.kind itself, since some member of the ModelKind union holds an expression field. That routed the entire kind (...) subtree through a dialect-specific generator, so on tsql a scalar sibling like forward_only TRUE was rewritten to (1 = 1) -- which reparses fine but silently evaluates to False on reload via str_to_bool. kind's own nested properties are already independently dialect-tagged via the ModelKind expression node's own meta when _props_sql recurses into them, so the outer kind property's policy should never route its subtree through render_with_model_dialect. Stop _holds_expression at _ModelKind subclasses to restore that. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 14 ++++++++++ tests/core/test_dialect.py | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index d6af7edb7d..ecd43a566f 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -744,10 +744,24 @@ def _holds_expression(annotation: t.Any, _visited: t.Optional[t.FrozenSet[t.Any] nested Tuple[str, Dict[str, exp.Expr]] shape used by audits/signals, and nested Pydantic models that themselves wrap an expression field, such as `TimeColumn` (IncrementalByTimeRangeKind.time_column). + + Stops at `_ModelKind` subclasses without recursing into their fields: a `kind` + property's own nested properties are independently dialect-tagged via the + `ModelKind` expression node's own meta when `_props_sql` recurses into them, so + treating the `kind` field itself as "holds an expression" -- true only because some + other member of the `ModelKind` union has an expression field, e.g. + `IncrementalByTimeRangeKind.time_column` -- would route its entire subtree, + including scalar sibling properties like `forward_only`, through a dialect-specific + generator and transpile them when they shouldn't be (tsql booleans becoming + `(1 = 1)`, which silently reparses as `False`). """ + from sqlmesh.core.model.kind import _ModelKind + if isinstance(annotation, type): if issubclass(annotation, exp.Expr): return True + if issubclass(annotation, _ModelKind): + return False visited = _visited or frozenset() if annotation in visited: return False diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 0ef9666cf3..c57ed7e8c4 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -516,6 +516,61 @@ def test_format_model_expressions_time_column_dialect(): ) +def test_format_model_expressions_kind_scalar_sibling_dialect(): + """A scalar sibling property of an expression-bearing property inside `kind` (e.g. + `forward_only` next to `time_column`) must stay dialect-agnostic even though the + render policy correctly marks `kind` as containing an expression-holding field + somewhere in the `ModelKind` union. + + Regression: recursing into nested Pydantic models to fix `time_column` (see + `test_format_model_expressions_time_column_dialect`) made `_holds_expression` also + match on `kind` itself, since *some* member of the `ModelKind` union + (`IncrementalByTimeRangeKind.time_column`) holds an expression. That routed the + entire `kind (...)` subtree through a dialect-specific generator, so tsql's + boolean-literal preprocessing rewrote `forward_only TRUE` into `forward_only (1 = 1)`. + That reparses without error, but `str_to_bool` on `Paren(EQ(1, 1)).name` (`""`) + evaluates to `False`, so the value silently flips on reload. + """ + formatted = format_model_expressions( + parse( + """ + MODEL ( + name a.b, + dialect tsql, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column [end], + forward_only true + ) + ); + + SELECT 1 AS x, [end] FROM t + """, + default_dialect="tsql", + ), + dialect="tsql", + ) + + assert ( + formatted + == """MODEL ( + name a.b, + dialect tsql, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column [end], + forward_only TRUE + ) +); + +SELECT + 1 AS x, + [end] +FROM t""" + ) + + model = load_sql_based_model(parse(formatted, default_dialect="tsql"), dialect="tsql") + assert model.kind.forward_only is True + + def test_format_model_expressions_macro_property_comments_preserved_with_dialect(): """Comments inside a macro header-property must survive formatting when the model has a `dialect` set. From ad2d11387c1f83f7a9439383100770fb0716159d Mon Sep 17 00:00:00 2001 From: Recoordinate <296084221+latent-9@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:16:29 +1200 Subject: [PATCH 06/67] docs: fix doubled words and a misspelling in the docs (#5946) Signed-off-by: latent-9 <296084221+latent-9@users.noreply.github.com> Co-authored-by: mday-io --- docs/concepts/macros/sqlmesh_macros.md | 2 +- docs/concepts/models/model_kinds.md | 4 ++-- docs/guides/ui.md | 2 +- docs/quickstart/notebook.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/concepts/macros/sqlmesh_macros.md b/docs/concepts/macros/sqlmesh_macros.md index c7d967b12c..5459d79ca8 100644 --- a/docs/concepts/macros/sqlmesh_macros.md +++ b/docs/concepts/macros/sqlmesh_macros.md @@ -1608,7 +1608,7 @@ def add_args( return argument_1 + argument_2 + argument_3 ``` -An `@add_args` call providing values for all arguments accepts positional arguments like this: `@add_args(5, 6, 7)` (which returns 5 + 6 + 7 = `18`). A call omitting and using the default value for the the final `argument_3` can also use positional arguments: `@add_args(5, 6)` (which returns 5 + 6 + 3 = `14`). +An `@add_args` call providing values for all arguments accepts positional arguments like this: `@add_args(5, 6, 7)` (which returns 5 + 6 + 7 = `18`). A call omitting and using the default value for the final `argument_3` can also use positional arguments: `@add_args(5, 6)` (which returns 5 + 6 + 3 = `14`). However, skipping an argument requires specifying the names of subsequent arguments (i.e., using "keyword arguments"). For example, skipping the second argument above by just omitting it - `@add_args(5, , 7)` - results in an error. diff --git a/docs/concepts/models/model_kinds.md b/docs/concepts/models/model_kinds.md index d01cc738a6..cde104790a 100644 --- a/docs/concepts/models/model_kinds.md +++ b/docs/concepts/models/model_kinds.md @@ -583,7 +583,7 @@ MODEL ( ### When Matched Expression -The logic to use when updating columns when a match occurs (the source and target match on the given keys) by default updates all the columns. This can be overriden with custom logic like below: +The logic to use when updating columns when a match occurs (the source and target match on the given keys) by default updates all the columns. This can be overridden with custom logic like below: ```sql linenums="1" hl_lines="5" MODEL ( @@ -1437,7 +1437,7 @@ GROUP BY SCD Type 2 models are designed by default to protect the data that has been captured because it is not possible to recreate the history once it has been lost. However, there are cases where you may want to clear the history and start fresh. -For this use use case you will want to start by setting `disable_restatement` to `false` in the model definition. +For this use case you will want to start by setting `disable_restatement` to `false` in the model definition. ```sql linenums="1" hl_lines="5" MODEL ( diff --git a/docs/guides/ui.md b/docs/guides/ui.md index 29bb204988..fa93b2448c 100644 --- a/docs/guides/ui.md +++ b/docs/guides/ui.md @@ -225,7 +225,7 @@ You may include all a project's models by clicking `All` in the Show drop-down o ![Lineage module - all models](./ui/ui-guide_lineage-all.png){ loading=lazy } -Click `Connected` in the Show drop-down menu to highlight edges between upstream parents and downstream children in blue. This may be helpful when when a project contains many models: +Click `Connected` in the Show drop-down menu to highlight edges between upstream parents and downstream children in blue. This may be helpful when a project contains many models: ![Lineage module - all models, connected edges](./ui/ui-guide_lineage-all-connected.png){ loading=lazy } diff --git a/docs/quickstart/notebook.md b/docs/quickstart/notebook.md index a1dae6b822..0398fc7038 100644 --- a/docs/quickstart/notebook.md +++ b/docs/quickstart/notebook.md @@ -248,7 +248,7 @@ You've now created a new production environment with all of history backfilled. ## 3. Update a model -Now that we have have populated the `prod` environment, let's modify one of the SQL models. +Now that we have populated the `prod` environment, let's modify one of the SQL models. We can modify the incremental SQL model using the `%model` *line* notebook magic (note the single `%`) and the model name: From 909ee91a9662dd862db6b04f43ae86c5cbef3aed Mon Sep 17 00:00:00 2001 From: Yuki <43386707+StuffbyYuki@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:29:01 -0600 Subject: [PATCH 07/67] TSC change: replace Reid Hooper with Cortland Goffena on the TSC (#5935) Signed-off-by: stuffbyyuki Co-authored-by: Cursor --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e1d8e1c6e..4c764255a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Initial TSC voting members are the project's Maintainers: |---------------------|---------------|----------------|------------| | Alexander Butler | z3z1ma | Harness | TSC Member | | Alexander Filipchik | afilipchik | Cloud Kitchens | TSC Member | -| Reid Hooper | rhooper9711 | Benzinga | TSC Member | +| Cortland Goffena | cmgoffena13 | Benzinga | TSC Member | | Yuki Kakegawa | StuffbyYuki | Jump.ai | TSC Member | | Toby Mao | tobymao | Fivetran | TSC Chair | | Alex Wilde | alexminerv | Minerva | TSC Member | From 7a09da6b5b6f1c5d50e21c8e8b82c7719d1fef97 Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Mon, 17 Aug 2026 13:36:47 -0700 Subject: [PATCH 08/67] fix: error when invalidating a nonexistent environment (#5878) Signed-off-by: Nishchay Mahor --- sqlmesh/cli/main.py | 2 +- sqlmesh/core/context.py | 11 ++++++++++- sqlmesh/magics.py | 2 +- tests/core/test_context.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 608b6eefb2..5574a892cb 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -641,7 +641,7 @@ def run(ctx: click.Context, environment: t.Optional[str] = None, **kwargs: t.Any def invalidate(ctx: click.Context, environment: str, **kwargs: t.Any) -> None: """Invalidate the target environment, forcing its removal during the next run of the janitor process.""" context = ctx.obj - context.invalidate_environment(environment, **kwargs) + context.invalidate_environment(environment, must_exist=True, **kwargs) @cli.command("janitor") diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d52b10e304..c3abff1d94 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1870,15 +1870,24 @@ def apply( ) @python_api_analytics - def invalidate_environment(self, name: str, sync: bool = False) -> None: + def invalidate_environment( + self, name: str, sync: bool = False, must_exist: bool = False + ) -> None: """Invalidates the target environment by setting its expiration timestamp to now. Args: name: The name of the environment to invalidate. sync: If True, the call blocks until the environment is deleted. Otherwise, the environment will be deleted asynchronously by the janitor process. + must_exist: If True, raise if the environment doesn't exist instead of silently doing nothing. + Used by the user-facing entry points, where a mistyped name should be reported rather than + look like it succeeded. Internal callers such as + `GithubController.try_invalidate_pr_environment` rely on the default no-op behavior, since + a PR environment may never have been created. """ name = Environment.sanitize_name(name) + if must_exist and self.state_sync.get_environment(name) is None: + raise SQLMeshError(f"Environment '{name}' was not found.") self.state_sync.invalidate_environment(name) if sync: self._cleanup_environments(name=name) diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 3a59fc4f7b..ed6a1b62de 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -983,7 +983,7 @@ def diff(self, context: Context, line: str) -> None: def invalidate(self, context: Context, line: str) -> None: """Invalidate the target environment, forcing its removal during the next run of the janitor process.""" args = parse_argstring(self.invalidate, line) - context.invalidate_environment(args.environment) + context.invalidate_environment(args.environment, must_exist=True) @magic_arguments() @argument( diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 7db129027b..e41382b078 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1928,6 +1928,40 @@ def test_invalidate_environment_no_sync_skips_cleanup(sushi_context, mocker: Moc state_sync_mock.delete_expired_environments.assert_not_called() +def test_invalidate_environment_nonexistent_raises(sushi_context, mocker: MockerFixture) -> None: + """Invalidating an environment that does not exist should error instead of + reporting success, so a mistyped name is caught rather than silently accepted.""" + state_sync_mock = mocker.patch.object( + type(sushi_context), "state_sync", new_callable=mocker.PropertyMock + ).return_value + state_sync_mock.get_environment.return_value = None + + with pytest.raises(SQLMeshError, match="Environment 'doesnotexist' was not found"): + sushi_context.invalidate_environment("doesnotexist", must_exist=True) + + state_sync_mock.invalidate_environment.assert_not_called() + + +def test_invalidate_environment_nonexistent_is_a_noop_by_default( + sushi_context, mocker: MockerFixture +) -> None: + """Without must_exist, invalidating a missing environment stays a no-op. + + Internal callers depend on this. `GithubController.try_invalidate_pr_environment` + invalidates the PR environment after a prod deploy, and that environment may never + have been created — a forward-only deploy, for instance. Raising there turns a + routine cleanup into a failed deploy. + """ + state_sync_mock = mocker.patch.object( + type(sushi_context), "state_sync", new_callable=mocker.PropertyMock + ).return_value + state_sync_mock.get_environment.return_value = None + + sushi_context.invalidate_environment("doesnotexist") + + state_sync_mock.invalidate_environment.assert_called_once_with("doesnotexist") + + @pytest.mark.slow def test_plan_default_end(sushi_context_pre_scheduling: Context): prod_plan_builder = sushi_context_pre_scheduling.plan_builder("prod") From 839c5b2caface322045441d41c74f528a508998e Mon Sep 17 00:00:00 2001 From: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com> Date: Tue, 18 Aug 2026 03:58:31 +0530 Subject: [PATCH 09/67] fix: raise ValueError for an out-of-range epoch in to_datetime (#5951) Signed-off-by: eeshsaxena Co-authored-by: mday-io --- sqlmesh/utils/date.py | 8 +++++++- tests/utils/test_date.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/date.py b/sqlmesh/utils/date.py index 5358719abe..f8df65c352 100644 --- a/sqlmesh/utils/date.py +++ b/sqlmesh/utils/date.py @@ -194,7 +194,13 @@ def to_datetime( try: dt = datetime.strptime(str(value), DATE_INT_FMT) except ValueError: - dt = datetime.fromtimestamp(epoch / 1000.0, tz=UTC) + try: + dt = datetime.fromtimestamp(epoch / 1000.0, tz=UTC) + except (OverflowError, OSError, ValueError): + # A non-finite or out-of-range epoch (e.g. "inf", 1e30, or a + # huge millis value) overflows fromtimestamp. Fall through to + # the ValueError below rather than leaking OverflowError/OSError. + dt = None if dt is None: raise ValueError(f"Could not convert `{value}` to datetime.") diff --git a/tests/utils/test_date.py b/tests/utils/test_date.py index bbd5bdf63a..c926507ccd 100644 --- a/tests/utils/test_date.py +++ b/tests/utils/test_date.py @@ -41,6 +41,17 @@ def test_to_datetime() -> None: assert to_datetime("31536000000") == target +@pytest.mark.parametrize( + "value", + ["inf", "-inf", "1e30", "99999999999999999999", float("inf")], +) +def test_to_datetime_out_of_range_raises(value: t.Any) -> None: + # A non-finite or out-of-range epoch overflows fromtimestamp; it must raise + # the documented ValueError rather than leaking OverflowError/OSError. + with pytest.raises(ValueError): + to_datetime(value) + + @pytest.mark.parametrize( "expression, result", [ From 7bd09efb146c3c7f0a49ca3389833294cf2ddceb Mon Sep 17 00:00:00 2001 From: Ha Vu Date: Tue, 18 Aug 2026 17:55:01 -0400 Subject: [PATCH 10/67] Bump fastapi to >=0.136.3 and starlette to >=1.01 for Python >= 3.10 to fix PYSEC-2026-161 (#5958) Signed-off-by: Ha Vu Co-authored-by: mday-io --- pyproject.toml | 8 ++++++-- web/server/settings.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ca7527868d..bc12bc30ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,9 @@ snowflake = [ starrocks = ["pymysql"] trino = ["trino"] web = [ - "fastapi==0.120.1", + "fastapi>=0.136.3; python_version >= '3.10'", + "starlette>=1.0.1; python_version >= '3.10'", + "fastapi==0.120.1; python_version < '3.10'", "watchfiles>=0.19.0", "uvicorn[standard]==0.22.0", "sse-starlette>=0.2.2", @@ -142,7 +144,9 @@ web = [ ] lsp = [ # Duplicate of web - "fastapi==0.120.1", + "fastapi>=0.136.3; python_version >= '3.10'", + "starlette>=1.0.1; python_version >= '3.10'", + "fastapi==0.120.1; python_version < '3.10'", "watchfiles>=0.19.0", # "uvicorn[standard]==0.22.0", "sse-starlette>=0.2.2", diff --git a/web/server/settings.py b/web/server/settings.py index a96d369de8..d893f52afa 100644 --- a/web/server/settings.py +++ b/web/server/settings.py @@ -96,7 +96,7 @@ def get_path_to_model_mapping( def get_loaded_context( settings: Settings = Depends(get_settings), -) -> t.Generator[Context, None]: +) -> t.Generator[Context, None, None]: try: with get_loaded_context_lock: yield _get_loaded_context(settings.project_path, settings.config, settings.gateway) From 86cd3b7d84bc92f692f56c4c4e40c2a4cdd5ca15 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:42:15 -0600 Subject: [PATCH 11/67] fix(makefile): exclude web install for dbt 1.6 to fix test (#5964) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Co-authored-by: Cursor --- .github/workflows/pr.yaml | 7 ++++++- Makefile | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8759bd484c..9e17e515d8 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -514,7 +514,12 @@ jobs: echo "DBT version is ${{ matrix.dbt-version }} (>= 1.6.0), keeping semantic_models and metrics sections" fi - make dbt-fast-test + # dbt 1.6 installs without web/lsp (pydantic conflict); skip collecting those tests + if [[ "${{ matrix.dbt-version }}" == "1.6" ]]; then + pytest -n auto -m "dbt and fast" --reruns 3 --ignore=tests/lsp --ignore=tests/web + else + make dbt-fast-test + fi - name: Test SQLMesh info in sushi_dbt working-directory: ./examples/sushi_dbt run: | diff --git a/Makefile b/Makefile index 300d96dc06..3edd6066ef 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,12 @@ install-dev-dbt-%: echo "Applying numpy<2 constraint for dbt $$version"; \ $(SED_INPLACE) 's/"numpy"/"numpy<2"/g' pyproject.toml; \ fi; \ - $(MAKE) install-dev; \ + if [ "$$version" = "1.6.0" ]; then \ + echo "Installing without web/lsp for dbt 1.6.0 (fastapi>=0.136 needs pydantic v2; dbt 1.6 needs pydantic v1)"; \ + $(PIP) install -e ".[dev,slack,dlt]" ./examples/custom_materializations; \ + else \ + $(MAKE) install-dev; \ + fi; \ if [ "$$version" = "1.6.0" ]; then \ echo "Applying overrides for dbt 1.6.0"; \ $(PIP) install 'pydantic>=2.0.0' 'google-cloud-bigquery==3.30.0' 'databricks-sdk==0.28.0' \ From 19cd73a599dbda7fd3c724cc5f6928faf62b75c5 Mon Sep 17 00:00:00 2001 From: davem-bis <68955845+davem-bis@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:38:55 +0100 Subject: [PATCH 12/67] feat: Add Development Container and Windows specific development pre-requisites (#5894) Signed-off-by: davem-bis <68955845+davem-bis@users.noreply.github.com> --- .devcontainer/devcontainer-lock.json | 24 ++++++++++++++++++++++++ .devcontainer/devcontainer.json | 24 ++++++++++++++++++++++++ .devcontainer/post-create-command.sh | 27 +++++++++++++++++++++++++++ docs/development.md | 16 ++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/post-create-command.sh diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000000..b33ff1a703 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.5.9", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a", + "integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a" + }, + "ghcr.io/devcontainers/features/docker-in-docker:4": { + "version": "4.0.0", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5", + "integrity": "sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "1.8.1", + "resolved": "ghcr.io/devcontainers/features/java@sha256:8157bab2d8d71e40b2f3128c162fab763e2b11038fdd33784549290a5d386b48", + "integrity": "sha256:8157bab2d8d71e40b2f3128c162fab763e2b11038fdd33784549290a5d386b48" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", + "integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..d0c250eb41 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "name": "SQLMesh Python Dev", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "21" + }, + "ghcr.io/devcontainers/features/docker-in-docker:4": {} + }, + "postCreateCommand": "bash .devcontainer/post-create-command.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ] + } + }, + "remoteUser": "vscode" +} diff --git a/.devcontainer/post-create-command.sh b/.devcontainer/post-create-command.sh new file mode 100644 index 0000000000..4ba84850bc --- /dev/null +++ b/.devcontainer/post-create-command.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# This script is intended to be run by an Ubuntu development container + +# Exit immediately if any command returns a non-zero code +set -e + +# Install OS-level dependencies + +## Ensure that the Microsoft package repository is available as it is required for msodbcsql18. Note that the repository +## may have already been added by a development container feature (e.g. docker-in-docker or java). +if apt-cache policy | grep -q 'packages.microsoft.com'; then + echo "Microsoft package repository already present" +else + echo "Microsoft package repository not present, it will be added." + + # ref: https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server + curl -sSL -O https://packages.microsoft.com/config/ubuntu/$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2)/packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb +fi + +ALL_DEPENDENCIES="libpq-dev netcat-traditional unixodbc-dev default-jdk msodbcsql18" +sudo apt-get clean && sudo apt-get -y update && sudo ACCEPT_EULA='Y' apt-get -y install $ALL_DEPENDENCIES + +# Install Python dependencies +make install-dev \ No newline at end of file diff --git a/docs/development.md b/docs/development.md index ff8b250d87..d80ef60689 100644 --- a/docs/development.md +++ b/docs/development.md @@ -11,6 +11,22 @@ Before you begin, ensure you have the following installed on your machine. Exact * OpenJDK >= 11 * Python >= 3.9 < 3.13 +### Windows Prerequisites + +The development environment of SQLMesh depends both on: + +* Symbolic links in the repository which, whilst available on Windows, typically require additional permissions for the process running git, and; +* Some Python functionality (e.g. `SIGUSR1`) that is only available on UNIX systems. Whilst this functionality is gated so shouldn't error on Windows, the development container enables its use. + +For the Python functionality, a development container is provided to develop against Ubuntu 24 with Python 3.12. + +For symbolic links, you must ensure that when checking out the repository: + +* The git configuration `core.symlinks` is set to `true` (this also needs to be done before bind mount, i.e. when the development container is started) +* The process that git runs as is permitted to create symbolic links. This can typically be done by running git as an administrator, or enabling [developer mode on Windows](https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode). + +Development containers are supported by [a number of IDEs](https://containers.dev/supporting.html). For developers using VSCode, [Microsoft has a tutorial on how to use development containers](https://code.visualstudio.com/docs/devcontainers/tutorial). + ## Virtual environment setup We do recommend using a virtual environment to develop SQLMesh. From 24bb095fcc5581d5d02f3bb253ddfa57f608cdcf Mon Sep 17 00:00:00 2001 From: Joseph Finlayson Date: Fri, 21 Aug 2026 22:53:59 +0200 Subject: [PATCH 13/67] Feat: Support gateway in model defaults (#5962) Signed-off-by: Joseph Finlayson Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- docs/guides/multi_engine.md | 8 +- docs/reference/model_configuration.md | 5 + sqlmesh/core/config/model.py | 2 + sqlmesh/core/model/decorator.py | 6 +- sqlmesh/core/model/definition.py | 20 ++- tests/core/integration/test_multi_repo.py | 65 ++++++++++ tests/core/test_config.py | 21 +++ tests/core/test_model.py | 149 ++++++++++++++++++++++ 8 files changed, 270 insertions(+), 6 deletions(-) diff --git a/docs/guides/multi_engine.md b/docs/guides/multi_engine.md index f2ccd31394..b4f179044e 100644 --- a/docs/guides/multi_engine.md +++ b/docs/guides/multi_engine.md @@ -11,9 +11,13 @@ SQLMesh enables this decoupling by supporting multiple engine adapters within a Configuring your project to use multiple engines follows a simple process: - Include all required [gateway connections](../reference/configuration.md#connection) in your configuration. -- Specify the `gateway` to be used for execution in the `MODEL` DDL. +- Set `model_defaults.gateway` to the gateway most models should use, and override individual models + with `gateway` in the `MODEL` DDL when needed. -If no gateway is explicitly defined for a model, the [default_gateway](../reference/configuration.md#default-gateway) of the project is used. +If no gateway is explicitly defined for a model, SQLMesh uses the project's +[`model_defaults.gateway`](../reference/model_configuration.md#model-defaults), when configured, +and otherwise uses its [default_gateway](../reference/configuration.md#default-gateway). This lets +all managed models in a project use a gateway without repeating it in every model definition. By default, virtual layer views are created in the `default_gateway`. This approach requires that all engines can read from and write to the same shared catalog, so a view in the `default_gateway` can access a table in another gateway. diff --git a/docs/reference/model_configuration.md b/docs/reference/model_configuration.md index f5dd0eadf0..47dcaaced1 100644 --- a/docs/reference/model_configuration.md +++ b/docs/reference/model_configuration.md @@ -193,10 +193,15 @@ The SQLMesh project-level `model_defaults` key supports the following options, d - allow_partials - enabled - interval_unit +- gateway - pre_statements (described [here](../concepts/models/sql_models.md#pre--and-post-statements)) - post_statements (described [here](../concepts/models/sql_models.md#pre--and-post-statements)) - on_virtual_update (described [here](../concepts/models/sql_models.md#on-virtual-update-statements)) +The `gateway` default applies to managed SQL, Python, and seed models. It does not apply to +external models because an external model's `gateway` selects a gateway-specific source +definition. Set that gateway explicitly in `external_models.yaml` when needed. + ### Model Naming diff --git a/sqlmesh/core/config/model.py b/sqlmesh/core/config/model.py index 326d0baf6f..e7af572cc7 100644 --- a/sqlmesh/core/config/model.py +++ b/sqlmesh/core/config/model.py @@ -50,6 +50,7 @@ class ModelDefaultsConfig(BaseConfig): pre_statements: The list of SQL statements that get executed before a model runs. post_statements: The list of SQL statements that get executed before a model runs. on_virtual_update: The list of SQL statements to be executed after the virtual update. + gateway: The gateway used by models that do not specify one explicitly. """ @@ -76,6 +77,7 @@ class ModelDefaultsConfig(BaseConfig): pre_statements: t.Optional[t.List[t.Union[str, exp.Expr]]] = None post_statements: t.Optional[t.List[t.Union[str, exp.Expr]]] = None on_virtual_update: t.Optional[t.List[t.Union[str, exp.Expr]]] = None + gateway: t.Optional[str] = None _model_kind_validator = model_kind_validator _on_destructive_change_validator = on_destructive_change_validator diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py index 54a43e0080..304c07276c 100644 --- a/sqlmesh/core/model/decorator.py +++ b/sqlmesh/core/model/decorator.py @@ -125,8 +125,12 @@ def models( blueprints = blueprints[0] + gateway = self.kwargs.get("gateway") + if isinstance(gateway, str) and gateway.lstrip().startswith("@"): + gateway = parse_one(gateway, dialect=dialect) + return create_models_from_blueprints( - gateway=self.kwargs.get("gateway"), + gateway=gateway, blueprints=blueprints, get_variables=get_variables, loader=self.model, diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index c3569f9bd3..e8e122dece 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2071,9 +2071,14 @@ def create_models_from_blueprints( loader_kwargs["default_catalog"] = original_default_catalog blueprint_variables = _extract_blueprint_variables(blueprint, path) - if gateway: + gateway_name: t.Optional[str] + if isinstance(gateway, str): + # Python decorator gateway names are literals, not SQL expressions. In particular, + # parsing a gateway such as "secondary-gw" as SQL would interpret it as subtraction. + gateway_name = gateway.lower() + elif gateway: rendered_gateway = render_expression( - expression=exp.maybe_parse(gateway, dialect=dialect), + expression=gateway, module_path=module_path, macros=loader_kwargs.get("macros"), jinja_macros=loader_kwargs.get("jinja_macros"), @@ -2082,7 +2087,11 @@ def create_models_from_blueprints( default_catalog=loader_kwargs.get("default_catalog"), blueprint_variables=blueprint_variables, ) - gateway_name = rendered_gateway[0].name if rendered_gateway else None + gateway_name = rendered_gateway[0].name.lower() if rendered_gateway else None + elif configured_gateway := (loader_kwargs.get("defaults") or {}).get("gateway"): + # Config gateway names are literals, not SQL expressions. In particular, parsing a + # gateway such as "secondary-gw" as SQL would interpret it as subtraction. + gateway_name = configured_gateway.lower() else: gateway_name = None @@ -2600,6 +2609,11 @@ def _create_model( kwargs["kind"] = create_model_kind(raw_kind, dialect, defaults or {}) defaults = {k: v for k, v in (defaults or {}).items() if k in klass.all_fields()} + if issubclass(klass, ExternalModel): + # An external model's gateway selects a gateway-specific source definition in + # external_models.yaml, so it must remain explicit rather than inheriting the + # gateway used to execute managed models in the project. + defaults.pop("gateway", None) if not issubclass(klass, SqlModel): defaults.pop("optimize_query", None) diff --git a/tests/core/integration/test_multi_repo.py b/tests/core/integration/test_multi_repo.py index 0b38231664..035c8bfda8 100644 --- a/tests/core/integration/test_multi_repo.py +++ b/tests/core/integration/test_multi_repo.py @@ -120,6 +120,71 @@ def test_multi(mocker): ] +def test_multi_repo_model_default_gateways(tmp_path: Path) -> None: + """Each project routes its models using its own default gateway.""" + repo_one = tmp_path / "repo_one" + repo_two = tmp_path / "repo_two" + (repo_one / "models").mkdir(parents=True) + (repo_two / "models").mkdir(parents=True) + + (repo_one / "models" / "default.sql").write_text( + "MODEL (name analytics.repo_one_default, kind FULL); SELECT @owner AS owner" + ) + (repo_one / "models" / "override.sql").write_text( + "MODEL (name analytics.explicit_override, kind FULL, gateway 'repo-two'); " + "SELECT @owner AS owner" + ) + (repo_two / "models" / "default.sql").write_text( + "MODEL (name analytics.repo_two_default, kind FULL); SELECT @owner AS owner" + ) + + def make_config(project: str, default_gateway: str) -> Config: + return Config( + project=project, + gateways={ + "repo-one": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "repo_one.duckdb")), + variables={"owner": "repo-one-variable"}, + ), + "repo-two": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "repo_two.duckdb")), + variables={"owner": "repo-two-variable"}, + ), + }, + default_gateway=default_gateway, + model_defaults=ModelDefaultsConfig(dialect="duckdb", gateway=default_gateway), + variables={"owner": "global-variable"}, + ) + + context = Context( + paths=[repo_one, repo_two], + config={ + repo_one: make_config("repo_one", "repo-one"), + repo_two: make_config("repo_two", "repo-two"), + }, + gateway="repo-one", + ) + + repo_one_model = context.get_model("repo_one.analytics.repo_one_default") + repo_two_model = context.get_model("repo_two.analytics.repo_two_default") + override_model = context.get_model("repo_two.analytics.explicit_override") + + assert repo_one_model.gateway == "repo-one" + assert repo_one_model.catalog == "repo_one" + assert repo_one_model.project == "repo_one" + assert context.render(repo_one_model.fqn).sql() == ("SELECT 'repo-one-variable' AS \"owner\"") + + assert repo_two_model.gateway == "repo-two" + assert repo_two_model.catalog == "repo_two" + assert repo_two_model.project == "repo_two" + assert context.render(repo_two_model.fqn).sql() == ("SELECT 'repo-two-variable' AS \"owner\"") + + assert override_model.gateway == "repo-two" + assert override_model.catalog == "repo_two" + assert override_model.project == "repo_one" + assert context.render(override_model.fqn).sql() == ("SELECT 'repo-two-variable' AS \"owner\"") + + @use_terminal_console def test_multi_repo_single_project_environment_statements_update(copy_to_temp_path): paths = copy_to_temp_path("examples/multi") diff --git a/tests/core/test_config.py b/tests/core/test_config.py index cbd4349999..0da5b6e22f 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -966,6 +966,27 @@ def test_gateway_model_defaults(tmp_path): assert ctx.config.model_defaults == expected +def test_model_defaults_gateway_from_yaml(tmp_path: Path) -> None: + config_path = tmp_path / "config.yaml" + config_path.write_text( + """ +gateways: + project_gateway: + connection: + type: duckdb + +model_defaults: + dialect: duckdb + gateway: project_gateway +""", + encoding="utf-8", + ) + + config = load_config_from_paths(Config, project_paths=[config_path]) + + assert config.model_defaults.gateway == "project_gateway" + + def test_model_defaults_cron_tz(tmp_path): """Test that cron_tz can be set in model_defaults.""" import zoneinfo diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 1f3cde265b..2f8ff49f7d 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -5039,6 +5039,62 @@ def python_model_prop(context, **kwargs): assert m.interval_unit == IntervalUnit.QUARTER_HOUR +def test_explicit_hyphenated_gateway_python_model() -> None: + @model( + name="model_schema.python_explicit_gateway", + kind="full", + gateway="secondary-gw", + columns={"some_col": "int"}, + ) + def python_explicit_gateway(context, **kwargs): + yield {"some_col": 1} + + requested_variable_gateways: t.List[t.Optional[str]] = [] + + def get_variables(gateway: t.Optional[str]) -> t.Dict[str, str]: + requested_variable_gateways.append(gateway) + return {} + + loaded_models = model.get_registry()["model_schema.python_explicit_gateway"].models( + get_variables=get_variables, + module_path=Path("."), + path=Path("."), + dialect="duckdb", + defaults=ModelDefaultsConfig().dict(), + default_catalog="default_db", + default_catalog_per_gateway={"secondary-gw": "secondary_db"}, + ) + + assert len(loaded_models) == 1 + assert loaded_models[0].gateway == "secondary-gw" + assert loaded_models[0].catalog == "secondary_db" + assert requested_variable_gateways == ["secondary-gw"] + + +def test_model_defaults_gateway_python_model() -> None: + @model( + name="model_schema.python_gateway_default", + kind="full", + columns={"some_col": "int"}, + ) + def python_gateway_default(context, **kwargs): + yield {"some_col": 1} + + loaded_models = model.get_registry()["model_schema.python_gateway_default"].models( + get_variables=lambda gateway: {}, + module_path=Path("."), + path=Path("."), + dialect="duckdb", + defaults=ModelDefaultsConfig(gateway="python_gateway").dict(), + default_catalog="default_db", + default_catalog_per_gateway={"python_gateway": "python_db"}, + ) + + assert len(loaded_models) == 1 + assert loaded_models[0].gateway == "python_gateway" + assert loaded_models[0].catalog == "python_db" + + def test_model_defaults_macros(make_snapshot): model_defaults = ModelDefaultsConfig( table_format="@IF(@gateway = 'dev', 'iceberg', NULL)", @@ -12982,6 +13038,99 @@ def test_default_catalog_still_applied_to_supported_gateway(): assert model.catalog == "other_db", f"Expected catalog 'other_db', got: {model.catalog}" +@pytest.mark.parametrize( + ("model_gateway", "expected_gateway", "expected_catalog"), + [ + (None, "secondary-gw", "secondary_db"), + ("default_gw", "default_gw", "example_catalog"), + ], +) +def test_model_defaults_gateway( + model_gateway: t.Optional[str], expected_gateway: str, expected_catalog: str +) -> None: + """A project-level gateway default controls loading unless the model overrides it.""" + gateway_property = f"gateway {model_gateway}," if model_gateway else "" + expressions = d.parse( + f""" + MODEL ( + name my_schema.my_model, + kind FULL, + {gateway_property} + ); + + SELECT 1 AS id + """, + default_dialect="duckdb", + ) + requested_variable_gateways: t.List[t.Optional[str]] = [] + + def get_variables(gateway: t.Optional[str]) -> t.Dict[str, str]: + requested_variable_gateways.append(gateway) + return {} + + models = load_sql_based_models( + expressions, + get_variables=get_variables, + defaults=ModelDefaultsConfig(gateway="secondary-gw").dict(), + dialect="duckdb", + default_catalog_per_gateway={ + "default_gw": "example_catalog", + "secondary-gw": "secondary_db", + }, + default_catalog="example_catalog", + ) + + assert len(models) == 1 + assert models[0].gateway == expected_gateway + assert models[0].catalog == expected_catalog + assert requested_variable_gateways == [expected_gateway] + + +def test_model_defaults_gateway_with_blueprints() -> None: + expressions = d.parse( + """ + MODEL ( + name model_@suffix.my_model, + kind FULL, + blueprints ( + (suffix := one), + (suffix := two), + ), + ); + + SELECT 1 AS id + """, + default_dialect="duckdb", + ) + + models = load_sql_based_models( + expressions, + get_variables=lambda gateway: {}, + defaults=ModelDefaultsConfig(gateway="other_duckdb").dict(), + dialect="duckdb", + default_catalog_per_gateway={"other_duckdb": "other_db"}, + default_catalog="example_catalog", + ) + + assert {model.gateway for model in models} == {"other_duckdb"} + assert {model.catalog for model in models} == {"other_db"} + + +def test_external_model_does_not_inherit_model_defaults_gateway() -> None: + default_external_model = create_external_model( + "source_schema.default_source", + defaults=ModelDefaultsConfig(gateway="managed_gateway").dict(), + ) + explicit_external_model = create_external_model( + "source_schema.explicit_source", + defaults=ModelDefaultsConfig(gateway="managed_gateway").dict(), + gateway="source_gateway", + ) + + assert default_external_model.gateway is None + assert explicit_external_model.gateway == "source_gateway" + + def test_no_gateway_uses_global_default_catalog(): """ Control test: when a model does NOT specify a gateway, the global From 17d0e4554f4c8a20a6ce1c95c2e64421fd29080f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:20:49 -0600 Subject: [PATCH 14/67] Chore(deps): Bump actions/upload-artifact from 5 to 7 (#5717) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 10 +++++----- .github/workflows/release.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 9e17e515d8..b93a3abcca 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -115,7 +115,7 @@ jobs: source .venv/bin/activate make cicd-test - name: Upload test results - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: name: test-results-style-cicd-${{ matrix.python-version }} @@ -151,7 +151,7 @@ jobs: python --version make fast-test - name: Upload test results - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: name: test-results-windows @@ -277,7 +277,7 @@ jobs: source .venv/bin/activate make ${{ matrix.engine }}-test - name: Upload test results - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: name: test-results-docker-${{ matrix.engine }} @@ -385,7 +385,7 @@ jobs: ./.github/scripts/manage-test-db.sh "${{ matrix.engine }}" "$TEST_DB_NAME" down - name: Upload test results - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: name: test-results-cloud-${{ matrix.engine }} @@ -446,7 +446,7 @@ jobs: run: | source ../../.venv/bin/activate pnpm run test:e2e - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: name: playwright-report diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b917b48578..9645cc0e75 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: - name: Build UI run: pnpm --prefix web/client run build - name: Upload UI build artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: ui-dist path: web/client/dist/ From cef5ec5513ef3179dc2a210a0e8b5484d849e4b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:55:05 -0600 Subject: [PATCH 15/67] Chore(deps): Bump dorny/paths-filter from 3 to 4 (#5733) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b93a3abcca..903908afc1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -19,7 +19,7 @@ jobs: ci: ${{ steps.filter.outputs.ci }} steps: - uses: actions/checkout@v5 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: filter with: filters: | From 9f2ef73e748bc59e054a6f2f1db1993a106ca941 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:24:11 -0600 Subject: [PATCH 16/67] Chore(deps): Bump actions/download-artifact from 4 to 8 (#5734) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9645cc0e75..3954c2f7e2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Download UI build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ui-dist path: web/client/dist/ From 43e656b6000fa35b64d9b71ef85e0dfd568a95bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:57:59 -0600 Subject: [PATCH 17/67] Chore(deps): Bump actions/cache from 4 to 5 (#5735) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 903908afc1..5b14e96450 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -203,7 +203,7 @@ jobs: - name: Get pnpm store directory id: pnpm-cache run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ steps.pnpm-cache.outputs.store }} key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} From e6ac0e5ba26c360916819f6f44dfc5d71478dbce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:30:49 -0600 Subject: [PATCH 18/67] Chore(deps): Bump actions/checkout from 5 to 7 (#5605) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 24 ++++++++++++------------ .github/workflows/release.yaml | 6 +++--- .github/workflows/release_extension.yaml | 2 +- .github/workflows/release_shared_js.yaml | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5b14e96450..c462a28076 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: client: ${{ steps.filter.outputs.client }} ci: ${{ steps.filter.outputs.ci }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: dorny/paths-filter@v4 id: filter with: @@ -48,7 +48,7 @@ jobs: env: UV: '1' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: @@ -79,7 +79,7 @@ jobs: PYTEST_XDIST_AUTO_NUM_WORKERS: 2 UV: '1' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Python @@ -131,7 +131,7 @@ jobs: steps: - name: Enable symlinks in git config run: git config --global core.symlinks true - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Install make run: choco install make which -y - name: Set up Python @@ -168,7 +168,7 @@ jobs: SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: '1' UV: '1' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Python @@ -193,7 +193,7 @@ jobs: # 'true' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '22' @@ -222,7 +222,7 @@ jobs: container: image: mcr.microsoft.com/playwright:v1.54.1-jammy steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Install pnpm via corepack run: | npm install --global corepack@latest @@ -258,7 +258,7 @@ jobs: SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: '1' UV: '1' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: @@ -342,7 +342,7 @@ jobs: FABRIC_TENANT_ID: ${{ secrets.FABRIC_TENANT_ID }} FABRIC_WORKSPACE_ID: ${{ secrets.FABRIC_WORKSPACE_ID }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: @@ -398,7 +398,7 @@ jobs: runs-on: ubuntu-latest if: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '22' @@ -415,7 +415,7 @@ jobs: # As at 2026-01-12 this job flakes 100% of the time. It needs investigation if: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '22' @@ -459,7 +459,7 @@ jobs: matrix: dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10'] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3954c2f7e2..3ba56dc761 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,7 +9,7 @@ jobs: ui-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: '22' @@ -31,7 +31,7 @@ jobs: needs: ui-build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Download UI build artifact uses: actions/download-artifact@v8 with: @@ -61,7 +61,7 @@ jobs: gh-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Create release on GitHub diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index cbc80b0ff9..ddaf5f7f67 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Check branch is main run: | if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml index 4ac7b73c6a..aa7427b542 100644 --- a/.github/workflows/release_shared_js.yaml +++ b/.github/workflows/release_shared_js.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Check branch is main run: | if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then From 9dcddbae40a9bdd5ce771e77ffa56660617a2330 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:44:35 -0600 Subject: [PATCH 19/67] Chore(deps): Bump actions/setup-node from 6 to 7 (#5982) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr.yaml | 6 +++--- .github/workflows/release.yaml | 2 +- .github/workflows/release_extension.yaml | 2 +- .github/workflows/release_shared_js.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index c462a28076..ecaafd4b5b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -194,7 +194,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22' - uses: pnpm/action-setup@v4 @@ -399,7 +399,7 @@ jobs: if: false steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22' - uses: pnpm/action-setup@v4 @@ -416,7 +416,7 @@ jobs: if: false steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22' - uses: pnpm/action-setup@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3ba56dc761..7c3d7a36b8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22' - uses: pnpm/action-setup@v4 diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index ddaf5f7f67..53eb9c80f5 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -28,7 +28,7 @@ jobs: fi echo "Version format is valid: $version" - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' - name: Install pnpm diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml index aa7427b542..c0dd95c0f3 100644 --- a/.github/workflows/release_shared_js.yaml +++ b/.github/workflows/release_shared_js.yaml @@ -31,7 +31,7 @@ jobs: fi echo "Version format is valid: $version" - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' registry-url: 'https://registry.npmjs.org' From ff0f66f9042791e02a831d72aad1eaa8d58c5fd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:16:23 -0600 Subject: [PATCH 20/67] Chore(deps): Bump actions/setup-python from 6 to 7 (#5981) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 16 ++++++++-------- .github/workflows/release.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index ecaafd4b5b..83b11b9b58 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Install uv @@ -83,7 +83,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install uv @@ -135,7 +135,7 @@ jobs: - name: Install make run: choco install make which -y - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' - name: Install SQLMesh dev dependencies @@ -172,7 +172,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Install uv @@ -260,7 +260,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' - name: Install uv @@ -344,7 +344,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' - name: Install uv @@ -425,7 +425,7 @@ jobs: - name: Install dependencies run: pnpm install - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' - name: Install uv @@ -461,7 +461,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Install uv diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7c3d7a36b8..19fb472ddf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -38,7 +38,7 @@ jobs: name: ui-dist path: web/client/dist/ - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Install uv From d4c262faad1612fc88715234c5e78f96faa9cea0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:52:19 -0600 Subject: [PATCH 21/67] Chore(deps): Bump pnpm/action-setup from 4 to 6 (#5983) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 6 +++--- .github/workflows/release.yaml | 2 +- .github/workflows/release_extension.yaml | 2 +- .github/workflows/release_shared_js.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 83b11b9b58..f6871f9223 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -197,7 +197,7 @@ jobs: - uses: actions/setup-node@v7 with: node-version: '22' - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: latest - name: Get pnpm store directory @@ -402,7 +402,7 @@ jobs: - uses: actions/setup-node@v7 with: node-version: '22' - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: latest - name: Install dependencies @@ -419,7 +419,7 @@ jobs: - uses: actions/setup-node@v7 with: node-version: '22' - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: latest - name: Install dependencies diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 19fb472ddf..78d1d9bedd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-node@v7 with: node-version: '22' - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: latest - name: Install dependencies diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index 53eb9c80f5..832f3b466c 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -32,7 +32,7 @@ jobs: with: node-version: '22' - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: 10 - name: Install dependencies diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml index c0dd95c0f3..98bc29d5df 100644 --- a/.github/workflows/release_shared_js.yaml +++ b/.github/workflows/release_shared_js.yaml @@ -40,7 +40,7 @@ jobs: - name: Print npm version run: npm --version - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: 10 - name: Install dependencies From 43678edcaee32e7d0522aeaf2a81a935af30c56f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:22:49 -0600 Subject: [PATCH 22/67] Chore(deps): Bump actions/cache from 5 to 6 (#5984) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f6871f9223..52a561985d 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -203,7 +203,7 @@ jobs: - name: Get pnpm store directory id: pnpm-cache run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: ${{ steps.pnpm-cache.outputs.store }} key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} From db84671b3176faa06fe2dbd41de29b6db3b0f59e Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Tue, 25 Aug 2026 08:02:49 +0800 Subject: [PATCH 23/67] fix: stabilize disable restatement test at UTC boundary (#5980) Signed-off-by: nightcityblade Co-authored-by: nightcityblade Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- tests/core/test_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 79313c36bd..add85d2eec 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -2026,6 +2026,7 @@ def test_added_forward_only_model(make_snapshot, mocker: MockerFixture): assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING +@time_machine.travel("2026-08-22 15:00:00 UTC") def test_disable_restatement(make_snapshot, mocker: MockerFixture): snapshot = make_snapshot( SqlModel( From b4c2d29118ac75bebc00e5d69386d7c7d6bc3372 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:20:59 -0600 Subject: [PATCH 24/67] Chore(deps): Bump softprops/action-gh-release from 2 to 3 (#5985) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 78d1d9bedd..47fdd9ac7a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -65,7 +65,7 @@ jobs: with: fetch-depth: 0 - name: Create release on GitHub - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: generate_release_notes: true tag_name: ${{ github.ref_name }} From b44b9d30260b321616f4aba008ab0e0900915e9e Mon Sep 17 00:00:00 2001 From: Andreas <65893109+fresioAS@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:07:57 +0200 Subject: [PATCH 25/67] Perf: add opt-in project-index loading for lint (#5913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Fredhøi Co-authored-by: Claude Fable 5 --- docs/guides/configuration.md | 10 ++ docs/guides/linter.md | 15 +++ docs/reference/cli.md | 5 +- docs/reference/configuration.md | 7 ++ sqlmesh/cli/main.py | 21 +++- sqlmesh/core/config/linter.py | 3 + sqlmesh/core/context.py | 157 +++++++++++++++++++++----- sqlmesh/core/loader.py | 191 ++++++++++++++++++++++++++++++-- sqlmesh/dbt/loader.py | 17 ++- tests/cli/test_cli.py | 45 ++++++++ tests/core/test_context.py | 187 ++++++++++++++++++++++++++++++- 11 files changed, 616 insertions(+), 42 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index b80afa1388..41f4b05594 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -329,6 +329,16 @@ By default, the SQLMesh cache is stored in a `.cache` directory within your proj The cache directory is automatically created if it doesn't exist. You can clear the cache using the `sqlmesh clean` command. +#### Project index + +The `--use-project-index` option on supported commands maintains a persistent model dependency index in the cache directory. Each project writes a file named `__model_index.json`. + +A full project load with the option enabled creates or refreshes the index. SQLMesh invalidates it when relevant configuration, gateway, macro, audit, or signal metadata changes, or when the set of model files changes. If the index is missing, invalid, or stale, SQLMesh safely falls back to a full project load and rebuilds it. + +For operations targeting selected models, the index allows SQLMesh to load only those models and their upstream dependencies. + +In multi-repository projects, dependencies that cross project boundaries may not be represented by an individual project's index. SQLMesh detects incomplete scoped loads and falls back to loading the full configured project set. + ### Table/view storage locations SQLMesh creates schemas, physical tables, and views in the data warehouse/engine. Learn more about why and how SQLMesh creates schema in the ["Why does SQLMesh create schemas?" FAQ](../faq/faq.md#schema-question). diff --git a/docs/guides/linter.md b/docs/guides/linter.md index 0a2e3ea828..22d8ed9ce5 100644 --- a/docs/guides/linter.md +++ b/docs/guides/linter.md @@ -135,6 +135,21 @@ $ sqlmesh lint --local This can make linting faster in repositories where all referenced models are loaded from local files. In multi-repository setups, or when linting only a subset of projects, `--local` may cause additional linting errors because SQLMesh will not resolve references or schemas from models that exist only in remote state. +For faster targeted linting, enable the persistent project index with `--use-project-index`. When +models are selected with `--model`, SQLMesh loads, resolves, and validates only those models and +their transitive upstream dependencies. The same behavior can be enabled by default for the +Python API and CLI with the `linter.use_project_index` configuration option: + +```yaml +linter: + enabled: true + use_project_index: true +``` + +`Context.lint_models` uses this configuration value when `use_project_index` is omitted. Passing +`use_project_index=False` explicitly disables it for that call. If a context was already loaded, +an indexed lint of selected models reloads the context so the requested scope is applied. + ## Applying linting rules diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9b6e28fe14..82b4161277 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -650,9 +650,12 @@ Usage: sqlmesh lint [OPTIONS] Options: --model TEXT A model to lint. Multiple models can be linted. If no models are specified, every model will be linted. + --use-project-index Use the persistent project index. With --model, only the selected models and their upstream dependencies + are loaded, resolved, and validated, so errors in unrelated models are not reported. Without --model, + every model is still loaded and linted. --local Lint using only locally loaded project files without loading state. In multi-repository setups, or when linting only a subset of projects, this may cause additional linting errors because SQLMesh will not resolve references or schemas from models that exist only in remote state. --help Show this message and exit. -``` \ No newline at end of file +``` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a7788df73d..a1bf400c32 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -60,6 +60,13 @@ The `model_defaults` key is **required** and must contain a value for the `diale See all the keys allowed in `model_defaults` at the [model configuration reference page](./model_configuration.md#model-defaults). +### Linter + +| Option | Description | Type | Required | +|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `linter.enabled` | Whether linting is enabled (Default: `False`) | boolean | N | +| `linter.use_project_index` | Whether to use the persistent project index for linting. Targeted linting loads selected models and their upstream dependencies. (Default: `False`) | boolean | N | + ### Variables The `variables` key can be used to provide values for user-defined variables, accessed using the [`@VAR` macro function](../concepts/macros/sqlmesh_macros.md#global-variables) in SQL model definitions, [`context.var` method](../concepts/models/python_models.md#global-variables) in Python model definitions, and [`evaluator.var` method](../concepts/macros/sqlmesh_macros.md#accessing-global-variable-values) in Python macro functions. diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 5574a892cb..c2dc1e3dbb 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -141,6 +141,10 @@ def cli( if ctx.invoked_subcommand in SKIP_LOAD_COMMANDS: load = False + # Unlike the other commands above, lint can scope its own load for multi-project contexts. + if ctx.invoked_subcommand == "lint": + load = False + configs = load_configs(config, Context.CONFIG_TYPE, paths, dotenv_path=dotenv) log_limit = list(configs.values())[0].log_limit @@ -1209,6 +1213,12 @@ def environments(obj: Context) -> None: multiple=True, help="A model to lint. Multiple models can be linted. If no models are specified, every model will be linted.", ) +@click.option( + "--use-project-index", + is_flag=True, + default=None, + help="Use the persistent project index. With --model, only the selected models and their upstream dependencies are loaded, resolved, and validated, so errors in unrelated models are not reported. Without --model, every model is still loaded and linted. Can also be enabled with linter.use_project_index.", +) @click.option( "--local", is_flag=True, @@ -1221,9 +1231,18 @@ def environments(obj: Context) -> None: def lint( obj: Context, models: t.Iterator[str], + use_project_index: t.Optional[bool], ) -> None: """Run the linter for the target model(s).""" - obj.lint_models(models) + obj.lint_models( + models, + use_project_index=use_project_index, + ) + + if not obj.models: + raise click.ClickException( + f"`{obj.path}` doesn't seem to have any models... cd into the proper directory or specify the path(s) with -p." + ) @cli.group(no_args_is_help=True) diff --git a/sqlmesh/core/config/linter.py b/sqlmesh/core/config/linter.py index 11d700c540..0c072e1977 100644 --- a/sqlmesh/core/config/linter.py +++ b/sqlmesh/core/config/linter.py @@ -16,6 +16,8 @@ class LinterConfig(BaseConfig): Args: enabled: Flag indicating whether the linter should run + use_project_index: Whether to use the persistent project index when linting. + rules: A list of error rules to be applied on model warn_rules: A list of rules to be applied on models but produce warnings instead of raising errors. ignored_rules: A list of rules to be excluded/ignored @@ -23,6 +25,7 @@ class LinterConfig(BaseConfig): """ enabled: bool = False + use_project_index: bool = False rules: t.Set[str] = set() warn_rules: t.Set[str] = set() diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index c3abff1d94..e335253016 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -418,6 +418,7 @@ def __init__( self._linters: t.Dict[str, Linter] = {} self._loaded: bool = False self._load_state: bool = load_state + self._uncached_model_names: t.Set[str] = set() self._selector_cls = selector or NativeSelector self.path, self.config = t.cast(t.Tuple[Path, C], next(iter(self.configs.items()))) @@ -641,11 +642,31 @@ def refresh(self) -> None: if any(loader.reload_needed() for loader in self._loaders): self.load() - def load(self, update_schemas: bool = True) -> GenericContext[C]: - """Load all files in the context's path.""" + def load( + self, + update_schemas: bool = True, + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> GenericContext[C]: + """Load files in the context's path, optionally scoped to specific models. + + Args: + update_schemas: Whether to update model schemas and validate model definitions. + model_fqns: If provided with ``use_project_index=True``, only the selected models + and their transitive upstream dependencies are loaded. + use_project_index: Whether to use and maintain the persistent project model index. + When ``model_fqns`` is not provided, all models are loaded and the index is + refreshed for future scoped loads. + """ load_start_ts = time.perf_counter() - loaded_projects = [loader.load() for loader in self._loaders] + loaded_projects = [ + loader.load( + model_fqns=model_fqns, + use_project_index=use_project_index, + ) + for loader in self._loaders + ] self.dag = DAG() self._standalone_audits.clear() @@ -688,6 +709,27 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: BUILTIN_RULES.union(project.user_rules), config.linter ) + indexed_model_fqns = { + fqn for project in loaded_projects for fqn in (project.indexed_model_fqns or set()) + } + if model_fqns and ( + not model_fqns <= self._models.keys() + or any( + dependency in indexed_model_fqns and dependency not in self._models + for model in self._models.values() + for dependency in model.depends_on + ) + ): + # A missing or stale index, a new model, or a dependency crossing project + # boundaries requires a full load to preserve existing behavior. + self.load( + update_schemas=False, + use_project_index=use_project_index, + ) + if update_schemas: + self._update_model_schemas_and_validate(model_fqns) + return self + # Load environment statements from state for projects not in current load if self._load_state and any(self._projects): prod = self.state_reader.get_environment(c.PROD) @@ -713,34 +755,13 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: else: local_store[snapshot.name] = snapshot.node # type: ignore + self._uncached_model_names = uncached + for model in self._models.values(): self.dag.add(model.fqn, model.depends_on) if update_schemas: - for fqn in self.dag: - model = self._models.get(fqn) # type: ignore - - if not model or fqn in uncached: - continue - - # make a copy of remote models that depend on local models or in the downstream chain - # without this, a SELECT * FROM local will not propogate properly because the downstream - # model will get mutated (schema changes) but the object is the same as the remote cache - if any(dep in uncached for dep in model.depends_on): - uncached.add(fqn) - self._models.update({fqn: model.copy(update={"mapping_schema": {}})}) - continue - - update_model_schemas( - self.dag, - models=self._models, - cache_dir=self.cache_dir, - ) - - models = self.models.values() - for model in models: - # The model definition can be validated correctly only after the schema is set. - model.validate_definition() + self._update_model_schemas_and_validate(model_fqns or None) duplicates = set(self._models) & set(self._standalone_audits) if duplicates: @@ -767,6 +788,53 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: self._loaded = True return self + def _update_model_schemas_and_validate(self, model_fqns: t.Optional[t.Set[str]] = None) -> None: + """Updates the mapping schemas of the given models (all models by default) and validates their definitions. + + Args: + model_fqns: If provided, only these models and their transitive upstream + dependencies are processed. + """ + if model_fqns is not None: + model_fqns = { + fqn for target in model_fqns for fqn in (target, *self.dag.upstream(target)) + } + + uncached = set(self._uncached_model_names) + + for fqn in self.dag: + if model_fqns is not None and fqn not in model_fqns: + continue + + model = self._models.get(fqn) + + if not model or fqn in uncached: + continue + + # make a copy of remote models that depend on local models or in the downstream chain + # without this, a SELECT * FROM local will not propogate properly because the downstream + # model will get mutated (schema changes) but the object is the same as the remote cache + if any(dep in uncached for dep in model.depends_on): + uncached.add(fqn) + self._models.update({fqn: model.copy(update={"mapping_schema": {}})}) + continue + + models = self._models + if model_fqns is not None: + models = UniqueKeyDict( + "models", {fqn: model for fqn, model in self._models.items() if fqn in model_fqns} + ) + + update_model_schemas( + self.dag, + models=models, + cache_dir=self.cache_dir, + ) + + for model in models.values(): + # The model definition can be validated correctly only after the schema is set. + model.validate_definition() + @python_api_analytics def run( self, @@ -3435,7 +3503,42 @@ def lint_models( self, models: t.Optional[t.Iterable[t.Union[str, Model]]] = None, raise_on_error: bool = True, + use_project_index: t.Optional[bool] = None, ) -> t.List[AnnotatedRuleViolation]: + """Lint the selected models. + + Args: + models: Models to lint. If omitted, all loaded models are linted. + raise_on_error: Whether to raise when an error-level violation is found. + use_project_index: Whether to use the persistent project index. If omitted, the + value of ``linter.use_project_index`` is used. Indexed linting of selected + models reloads an already-loaded context so the requested scope is applied. + """ + models = list(models) if models is not None else [] + use_project_index = ( + self.config.linter.use_project_index if use_project_index is None else use_project_index + ) + + target_fqns = ( + { + normalize_model_name( + model, + default_catalog=self.default_catalog, + dialect=self.default_dialect, + ) + if isinstance(model, str) + else model.fqn + for model in models + } + if models and use_project_index + else None + ) + + # An already-loaded context does not otherwise enter the loading path. Reload when + # indexed linting is requested for specific models so the scope is actually applied. + if not self._loaded or target_fqns is not None: + self.load(model_fqns=target_fqns, use_project_index=use_project_index) + found_error = False model_list = ( diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index cb951b4f9e..5a1ce8b17d 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -3,9 +3,11 @@ import abc import glob import itertools +import json import linecache import os import re +import tempfile import typing as t from collections import Counter, defaultdict from dataclasses import dataclass @@ -38,6 +40,7 @@ from sqlmesh.core.test import ModelTestMetadata from sqlmesh.utils import UniqueKeyDict, sys_path from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.hashing import crc32 from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor from sqlmesh.utils.metaprogramming import import_python_file from sqlmesh.utils.pydantic import validation_error_message @@ -65,6 +68,7 @@ class LoadedProject: environment_statements: t.List[EnvironmentStatements] user_rules: RuleSet model_test_metadata: t.List[ModelTestMetadata] + indexed_model_fqns: t.Optional[t.Set[str]] = None class CacheBase(abc.ABC): @@ -188,10 +192,19 @@ def __init__(self, context: GenericContext, path: Path) -> None: } _init_model_defaults(self.config_essentials, self.context.selected_gateway) - def load(self) -> LoadedProject: + def load( + self, + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> LoadedProject: """ Loads all macros and models in the context's path. + Args: + model_fqns: Optional model names to load. Loaders that support partial loading + also include these models' transitive upstream dependencies. + use_project_index: Whether to use and maintain the persistent project model index. + Returns: A loaded project object. """ @@ -228,12 +241,14 @@ def load(self) -> LoadedProject: else: standalone_audits[name] = audit - models = self._load_models( + models, indexed_model_fqns = self._load_models( macros, jinja_macros, self.context.selected_gateway, audits, signals, + model_fqns, + use_project_index, ) metrics = self._load_metrics() @@ -258,6 +273,7 @@ def load(self) -> LoadedProject: environment_statements=environment_statements, user_rules=user_rules, model_test_metadata=model_test_metadata, + indexed_model_fqns=indexed_model_fqns, ) return project @@ -286,7 +302,9 @@ def _load_models( gateway: t.Optional[str], audits: UniqueKeyDict[str, ModelAudit], signals: UniqueKeyDict[str, signal], - ) -> UniqueKeyDict[str, Model]: + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> t.Tuple[UniqueKeyDict[str, Model], t.Optional[t.Set[str]]]: """Loads all models.""" @abc.abstractmethod @@ -481,6 +499,10 @@ def _failed_to_load_model_error(self, path: Path, error: t.Union[str, Exception] class SqlMeshLoader(Loader): """Loads macros and models for a context using the SQLMesh file formats""" + MODEL_INDEX_VERSION = 1 + _signals_max_mtime: t.Optional[float] + _audits_max_mtime: t.Optional[float] + def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: """Loads all user defined macros.""" # Store a copy of the macro registry @@ -533,23 +555,170 @@ def _load_models( gateway: t.Optional[str], audits: UniqueKeyDict[str, ModelAudit], signals: UniqueKeyDict[str, signal], - ) -> UniqueKeyDict[str, Model]: + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> t.Tuple[UniqueKeyDict[str, Model], t.Optional[t.Set[str]]]: """ Loads all of the models within the model directory with their associated audits into a Dict and creates the dag """ cache = SqlMeshLoader._Cache(self, self.config_path) - - sql_models = self._load_sql_models(macros, jinja_macros, audits, signals, cache, gateway) + selected_model_index = ( + self._selected_model_paths(model_fqns) if use_project_index and model_fqns else None + ) + selected_paths, indexed_model_fqns = selected_model_index or (None, None) + + sql_models = self._load_sql_models( + macros, + jinja_macros, + audits, + signals, + cache, + gateway, + selected_paths=selected_paths, + ) external_models = self._load_external_models(audits, cache, gateway) - python_models = self._load_python_models(macros, jinja_macros, audits, signals) + python_models = self._load_python_models( + macros, + jinja_macros, + audits, + signals, + selected_paths=selected_paths, + ) all_model_names = list(sql_models) + list(external_models) + list(python_models) duplicates = [name for name, count in Counter(all_model_names).items() if count > 1] if duplicates: raise ConfigError(f"Duplicate model name(s) found: {', '.join(duplicates)}.") - return UniqueKeyDict("models", **sql_models, **external_models, **python_models) + models: UniqueKeyDict[str, Model] = UniqueKeyDict( + "models", **sql_models, **external_models, **python_models + ) + if use_project_index and selected_paths is None: + self._write_model_index(models) + return models, indexed_model_fqns + + @property + def _model_index_path(self) -> Path: + project_path_hash = crc32([str(self.config_path.resolve())]) + return ( + self.context.cache_dir + / f"{self.config.project or 'default'}_{project_path_hash}_model_index.json" + ) + + def _model_index_id(self) -> t.List[t.Optional[t.Union[str, int, float]]]: + return [ + self.MODEL_INDEX_VERSION, + self.config.fingerprint, + self.context.default_catalog, + self.context.gateway or self.config.default_gateway_name, + self._macros_max_mtime, + self._signals_max_mtime, + self._audits_max_mtime, + self._config_mtimes.get(self.config_path), + self._config_mtimes.get(c.SQLMESH_PATH), + ] + + def _model_paths(self) -> t.Set[Path]: + paths: t.Set[Path] = set() + for extension in (".sql", ".py"): + paths.update( + path + for path in self._glob_paths( + self.config_path / c.MODELS, + ignore_patterns=self.config.ignore_patterns, + extension=extension, + ) + if os.path.getsize(path) + ) + return paths + + def _selected_model_paths( + self, model_fqns: t.Set[str] + ) -> t.Optional[t.Tuple[t.Set[Path], t.Set[str]]]: + try: + index = json.loads(self._model_index_path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + + if not isinstance(index, dict): + return None + + if index.get("index_id") != self._model_index_id(): + return None + + current_paths = self._model_paths() + indexed_files = index.get("files") + if not isinstance(indexed_files, dict) or not all( + isinstance(relative_path, str) and isinstance(file_models, dict) + for relative_path, file_models in indexed_files.items() + ): + return None + + indexed_paths = {self.config_path / relative_path for relative_path in indexed_files} + if current_paths != indexed_paths: + return None + + model_to_path: t.Dict[str, Path] = {} + dependencies: t.Dict[str, t.Set[str]] = {} + for relative_path, file_models in indexed_files.items(): + path = self.config_path / relative_path + for fqn, depends_on in file_models.items(): + if ( + not isinstance(fqn, str) + or not isinstance(depends_on, list) + or not all(isinstance(dependency, str) for dependency in depends_on) + ): + return None + model_to_path[fqn] = path + dependencies[fqn] = set(depends_on) + + selected = {fqn for fqn in model_fqns if fqn in model_to_path} + stack = list(selected) + while stack: + fqn = stack.pop() + for dependency in dependencies.get(fqn, set()): + if dependency in model_to_path and dependency not in selected: + selected.add(dependency) + stack.append(dependency) + + return {model_to_path[fqn] for fqn in selected}, set(model_to_path) + + def _write_model_index(self, models: UniqueKeyDict[str, Model]) -> None: + model_paths = self._model_paths() + if not model_paths: + return + + # Maps each model file to the models it defines and their dependencies. + files: t.Dict[str, t.Dict[str, t.List[str]]] = { + str(path.relative_to(self.config_path)): {} for path in model_paths + } + for model in models.values(): + if model._path in model_paths: + relative_path = str(t.cast(Path, model._path).relative_to(self.config_path)) + files[relative_path][model.fqn] = sorted(model.depends_on) + + self._model_index_path.parent.mkdir(parents=True, exist_ok=True) + temporary_path: t.Optional[Path] = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=self._model_index_path.parent, + prefix=f".{self._model_index_path.name}.", + suffix=".tmp", + delete=False, + ) as temporary_file: + json.dump( + {"index_id": self._model_index_id(), "files": files}, + temporary_file, + sort_keys=True, + ) + temporary_path = Path(temporary_file.name) + temporary_path.replace(self._model_index_path) + finally: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) def _load_sql_models( self, @@ -559,6 +728,7 @@ def _load_sql_models( signals: UniqueKeyDict[str, signal], cache: CacheBase, gateway: t.Optional[str], + selected_paths: t.Optional[t.Set[Path]] = None, ) -> UniqueKeyDict[str, Model]: """Loads the sql models into a Dict""" models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") @@ -570,6 +740,8 @@ def _load_sql_models( ignore_patterns=self.config.ignore_patterns, extension=".sql", ): + if selected_paths is not None and path not in selected_paths: + continue if not os.path.getsize(path): continue @@ -641,6 +813,7 @@ def _load_python_models( jinja_macros: JinjaMacroRegistry, audits: UniqueKeyDict[str, ModelAudit], signals: UniqueKeyDict[str, signal], + selected_paths: t.Optional[t.Set[Path]] = None, ) -> UniqueKeyDict[str, Model]: """Loads the python models into a Dict""" models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") @@ -655,6 +828,8 @@ def _load_python_models( ignore_patterns=self.config.ignore_patterns, extension=".py", ): + if selected_paths is not None and path not in selected_paths: + continue if not os.path.getsize(path): continue diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index fb3ecb2c77..ac4764f2af 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -133,9 +133,16 @@ def __init__( self._profiles_dir = profiles_dir super().__init__(context, path) - def load(self) -> LoadedProject: + def load( + self, + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> LoadedProject: self._projects = [] - return super().load() + return super().load( + model_fqns=model_fqns, + use_project_index=use_project_index, + ) def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: macro_files = list(Path(self.config_path, "macros").glob("**/*.sql")) @@ -156,7 +163,9 @@ def _load_models( gateway: t.Optional[str], audits: UniqueKeyDict[str, ModelAudit], signals: UniqueKeyDict[str, signal], - ) -> UniqueKeyDict[str, Model]: + model_fqns: t.Optional[t.Set[str]] = None, + use_project_index: bool = False, + ) -> t.Tuple[UniqueKeyDict[str, Model], t.Optional[t.Set[str]]]: models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: @@ -200,7 +209,7 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: models.update(self._load_external_models(audits, cache)) - return models + return models, None def _load_audits( self, macros: MacroRegistry, jinja_macros: JinjaMacroRegistry diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index c625cb084d..a8ee1aaa39 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1352,6 +1352,51 @@ def test_lint(runner, tmp_path): assert result.exit_code == 1 +def test_lint_no_models(runner, tmp_path): + with open(tmp_path / "config.yaml", "w", encoding="utf-8") as f: + f.write("model_defaults:\n dialect: duckdb\n") + + result = runner.invoke(cli, ["--paths", tmp_path, "lint"]) + assert result.exit_code == 1 + assert "doesn't seem to have any models" in result.output + + +def test_lint_model_scopes_validation_with_multiple_projects(runner, tmp_path): + project_a = tmp_path / "project_a" + project_b = tmp_path / "project_b" + for project in (project_a, project_b): + (project / "models").mkdir(parents=True) + with open(project / "config.yaml", "w", encoding="utf-8") as f: + f.write( + f"project: {project.name}\n" + "model_defaults:\n" + " dialect: duckdb\n" + "linter:\n" + " enabled: true\n" + ) + + with open(project_a / "models" / "selected.sql", "w", encoding="utf-8") as f: + f.write("MODEL(name selected); SELECT 1 AS col;") + with open(project_b / "models" / "unrelated.sql", "w", encoding="utf-8") as f: + f.write("MODEL(name unrelated, kind FULL, partitioned_by (col, col)); SELECT 1 AS col;") + + result = runner.invoke( + cli, + [ + "--paths", + project_a, + "--paths", + project_b, + "lint", + "--use-project-index", + "--model", + "selected", + ], + ) + + assert result.exit_code == 0, result.output + + def test_state_export(runner: CliRunner, tmp_path: Path) -> None: create_example_project(tmp_path) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index e41382b078..75737f1edb 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1,3 +1,4 @@ +import json import logging import pathlib import typing as t @@ -36,9 +37,10 @@ from sqlmesh.core.dialect import parse, schema_ from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter from sqlmesh.core.environment import Environment, EnvironmentNamingInfo, EnvironmentStatements +from sqlmesh.core.loader import SqlMeshLoader from sqlmesh.core.plan.definition import Plan from sqlmesh.core.macros import MacroEvaluator, RuntimeStage -from sqlmesh.core.model import load_sql_based_model, model, SqlModel, Model +from sqlmesh.core.model import load_sql_based_model, model, SqlModel, Model, update_model_schemas from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.model.cache import OptimizedQueryCache from sqlmesh.core.snapshot import SnapshotChangeCategory @@ -3288,6 +3290,189 @@ def model4_entrypoint(context, **kwargs): sushi_context.plan(environment="dev", auto_apply=True, no_prompts=True) +def test_lint_models_scoped_schema_resolution(tmp_path: pathlib.Path) -> None: + def create_context() -> Context: + return Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + linter=LinterConfig(enabled=True, rules=["noselectstar"]), + ), + paths=tmp_path, + load=False, + ) + + create_temp_file( + tmp_path, + pathlib.Path("models", "a.sql"), + "MODEL(name a); SELECT 1 AS col FROM raw.unregistered_source;", + ) + create_temp_file(tmp_path, pathlib.Path("models", "b.sql"), "MODEL(name b); SELECT col FROM a;") + create_temp_file(tmp_path, pathlib.Path("models", "c.sql"), "MODEL(name c); SELECT * FROM b;") + create_temp_file(tmp_path, pathlib.Path("models", "d.sql"), "MODEL(name d); SELECT 2 AS col;") + + # Case: Without the opt-in, linting a specific model preserves the full-load behavior. + ctx = create_context() + with patch( + "sqlmesh.core.context.update_model_schemas", wraps=update_model_schemas + ) as schemas_mock: + violations = ctx.lint_models(["b"]) + + assert violations == [] + assert schemas_mock.call_count == 1 + assert set(schemas_mock.call_args.kwargs["models"]) == set(ctx.models) + + # Case: The project-index opt-in scopes schema resolution to the target and its + # upstream dependencies. The initial full file load also creates the index. + ctx = create_context() + with patch( + "sqlmesh.core.context.update_model_schemas", wraps=update_model_schemas + ) as schemas_mock: + violations = ctx.lint_models(["b"], use_project_index=True) + + assert violations == [] + assert schemas_mock.call_count == 1 + updated_models = schemas_mock.call_args.kwargs["models"] + assert set(updated_models) == { + ctx.get_model("a", raise_if_missing=True).fqn, + ctx.get_model("b", raise_if_missing=True).fqn, + } + + # Case: Once a full load has populated the model index, an edited target still + # loads only its file and upstream dependencies. If the edit introduces a new + # dependency outside this set, Context.load safely retries with a full load. + create_temp_file( + tmp_path, + pathlib.Path("models", "b.sql"), + "MODEL(name b); SELECT col + 1 AS col FROM a;", + ) + ctx = create_context() + loader = t.cast(SqlMeshLoader, ctx._loaders[0]) + with patch.object( + loader, + "_load_sql_models", + wraps=loader._load_sql_models, + ) as load_sql_models_mock: + assert ctx.lint_models(["b"], use_project_index=True) == [] + + assert load_sql_models_mock.call_count == 1 + selected_paths = load_sql_models_mock.call_args.kwargs["selected_paths"] + assert {path.name for path in selected_paths} == {"a.sql", "b.sql"} + assert set(ctx.models) == { + ctx.get_model("a", raise_if_missing=True).fqn, + ctx.get_model("b", raise_if_missing=True).fqn, + } + + # Case: A newly introduced dependency that is known to the index triggers a safe + # full reload instead of leaving the context incomplete. + create_temp_file( + tmp_path, + pathlib.Path("models", "b.sql"), + "MODEL(name b); SELECT col FROM d;", + ) + ctx = create_context() + loader = t.cast(SqlMeshLoader, ctx._loaders[0]) + with patch.object( + loader, + "_load_sql_models", + wraps=loader._load_sql_models, + ) as load_sql_models_mock: + assert ctx.lint_models(["b"], use_project_index=True) == [] + + assert load_sql_models_mock.call_count == 2 + assert set(ctx.models) == { + ctx.get_model(model_name, raise_if_missing=True).fqn for model_name in ("a", "b", "c", "d") + } + + # Case: Violations are still detected when linting specific models. + with pytest.raises( + LinterError, match="Linter detected errors in the code. Please fix them before proceeding." + ): + create_context().lint_models(["c"], use_project_index=True) + + # Case: Linting without a model selection triggers a full load and lints every model. + ctx = create_context() + with patch( + "sqlmesh.core.context.update_model_schemas", wraps=update_model_schemas + ) as schemas_mock: + with pytest.raises( + LinterError, + match="Linter detected errors in the code. Please fix them before proceeding.", + ): + ctx.lint_models() + + assert schemas_mock.call_count == 1 + assert set(schemas_mock.call_args.kwargs["models"]) == set(ctx.models) + + +def test_lint_models_project_index_reloads_loaded_context(tmp_path: pathlib.Path) -> None: + create_temp_file(tmp_path, pathlib.Path("models", "a.sql"), "MODEL(name a); SELECT 1 AS col;") + create_temp_file(tmp_path, pathlib.Path("models", "b.sql"), "MODEL(name b); SELECT col FROM a;") + create_temp_file(tmp_path, pathlib.Path("models", "c.sql"), "MODEL(name c); SELECT 2 AS col;") + + context = Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + linter=LinterConfig(enabled=True, use_project_index=True), + ), + paths=tmp_path, + ) + context.load(use_project_index=True) + loader = t.cast(SqlMeshLoader, context._loaders[0]) + + with patch.object(loader, "_load_sql_models", wraps=loader._load_sql_models) as load_mock: + assert context.lint_models(["b"]) == [] + + assert load_mock.call_count == 1 + selected_paths = load_mock.call_args.kwargs["selected_paths"] + assert {path.name for path in selected_paths} == {"a.sql", "b.sql"} + assert set(context.models) == { + context.get_model(model_name, raise_if_missing=True).fqn for model_name in ("a", "b") + } + + +@pytest.mark.parametrize("invalid_files_shape", ["file_list", "model_list"]) +def test_invalid_model_index_falls_back_to_full_load( + tmp_path: pathlib.Path, invalid_files_shape: str +) -> None: + create_temp_file( + tmp_path, + pathlib.Path("models", "a.sql"), + "MODEL(name a); SELECT 1 AS col;", + ) + create_temp_file( + tmp_path, + pathlib.Path("models", "b.sql"), + "MODEL(name b); SELECT col FROM a;", + ) + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + + indexed_context = Context(config=config, paths=tmp_path, load=False) + indexed_context.load(use_project_index=True) + indexed_loader = t.cast(SqlMeshLoader, indexed_context._loaders[0]) + index = json.loads(indexed_loader._model_index_path.read_text(encoding="utf-8")) + if invalid_files_shape == "file_list": + index["files"] = list(index["files"]) + else: + relative_path = next(iter(index["files"])) + index["files"][relative_path] = [] + indexed_loader._model_index_path.write_text(json.dumps(index), encoding="utf-8") + + context = Context(config=config, paths=tmp_path, load=False) + loader = t.cast(SqlMeshLoader, context._loaders[0]) + with patch.object( + loader, + "_load_sql_models", + wraps=loader._load_sql_models, + ) as load_sql_models_mock: + assert context.lint_models(["b"], use_project_index=True) == [] + + assert load_sql_models_mock.call_args.kwargs["selected_paths"] is None + assert set(context.models) == { + context.get_model("a", raise_if_missing=True).fqn, + context.get_model("b", raise_if_missing=True).fqn, + } + + def test_plan_selector_expression_no_match(sushi_context: Context) -> None: with pytest.raises( PlanError, From ada45e9a031e47efbdbeffd48b740facd59e5cb4 Mon Sep 17 00:00:00 2001 From: Chaitanya Panchal <83701005+Chaitanya-0310@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:39:06 -0400 Subject: [PATCH 26/67] fix(tests): make Athena S3 prefix tests self cleaning to avoid flaky CI reruns (#5987) Signed-off-by: Chaitanya Panchal Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .../integration/test_integration_athena.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/core/engine_adapter/integration/test_integration_athena.py b/tests/core/engine_adapter/integration/test_integration_athena.py index 9d23af206e..a4b91f823d 100644 --- a/tests/core/engine_adapter/integration/test_integration_athena.py +++ b/tests/core/engine_adapter/integration/test_integration_athena.py @@ -48,8 +48,21 @@ def s3_list_objects(s3: t.Any, location: str, **list_objects_kwargs: t.Any) -> t return lst +def s3_delete_objects(s3: t.Any, location: str) -> None: + # The S3 location for a given test is stable across pytest-rerunfailures retries within the same + # test session (it's derived from the session's testrun_uid + the test's name), so a prior failed + # attempt can leave objects behind that a subsequent retry would otherwise trip over. Proactively + # clearing the prefix makes these tests self-healing instead of just asserting it's already empty. + bucket, prefix = parse_s3_uri(location) + for page in s3.get_paginator("list_objects_v2").paginate(Bucket=bucket, Prefix=prefix): + objects = [{"Key": o["Key"]} for o in page.get("Contents", [])] + if objects: + s3.delete_objects(Bucket=bucket, Delete={"Objects": objects}) + + def test_clear_partition_data(ctx: TestContext, engine_adapter: AthenaEngineAdapter, s3: t.Any): base_uri = engine_adapter.s3_warehouse_location_or_raise + s3_delete_objects(s3, base_uri) assert len(s3_list_objects(s3, base_uri)) == 0 src_table = ctx.table("src_table") @@ -239,6 +252,7 @@ def test_hive_truncate_table(ctx: TestContext, engine_adapter: AthenaEngineAdapt ] ) + s3_delete_objects(s3, base_uri) assert len(s3_list_objects(s3, base_uri)) == 0 engine_adapter.ctas(table_name=table_1, query_or_df=base_data) From 7a68c2a2c6cfbd53521567180a45c906b29fd774 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:52:59 -0600 Subject: [PATCH 27/67] fix: removing time machine to fix auth (#5975) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .../integration/test_integration_bigquery.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index ce31c255ad..75909948b2 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -2,7 +2,6 @@ from unittest import mock import pytest -import time_machine from pathlib import Path from sqlglot import exp from sqlglot.optimizer.qualify_columns import quote_identifiers @@ -474,7 +473,6 @@ def test_plan_correlation_id_in_job_labels(ctx: TestContext): assert labels == {correlation_id.job_type.value.lower(): correlation_id.job_id} -@time_machine.travel("2023-01-08 15:00:00 UTC") def test_run_correlation_id_in_job_labels(ctx: TestContext): run_id = "test_run_id" model_name = ctx.table("run_test") @@ -497,7 +495,7 @@ def test_run_correlation_id_in_job_labels(ctx: TestContext): ) ) ) - sqlmesh.plan(auto_apply=True, no_prompts=True) + sqlmesh.plan(auto_apply=True, no_prompts=True, execution_time="2023-01-08 15:00:00") captured_evaluators: t.List = [] original_scheduler = sqlmesh.scheduler @@ -510,12 +508,9 @@ def scheduler_wrapper( captured_evaluators.append(snapshot_evaluator) return original_scheduler(environment=environment, snapshot_evaluator=snapshot_evaluator) - with time_machine.travel("2023-01-09 00:00:00 UTC"): - with mock.patch( - "sqlmesh.core.context.analytics.collector.on_run_start", return_value=run_id - ): - with mock.patch.object(sqlmesh, "scheduler", scheduler_wrapper): - sqlmesh.run() + with mock.patch("sqlmesh.core.context.analytics.collector.on_run_start", return_value=run_id): + with mock.patch.object(sqlmesh, "scheduler", scheduler_wrapper): + sqlmesh.run(execution_time="2023-01-09 00:00:00") assert captured_evaluators adapter = t.cast(BigQueryEngineAdapter, captured_evaluators[-1].adapter) From 0317c1b5224c8548b9fe9368340c65a3b5dce0e2 Mon Sep 17 00:00:00 2001 From: Sakshi Patil Date: Sun, 30 Aug 2026 02:42:49 +0530 Subject: [PATCH 28/67] fix(postgres): randomize test username and update grant tests (#5990) Signed-off-by: sakshipatil-hue --- .../integration/test_integration_postgres.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration_postgres.py b/tests/core/engine_adapter/integration/test_integration_postgres.py index f236fdebce..2bc1dcc2e7 100644 --- a/tests/core/engine_adapter/integration/test_integration_postgres.py +++ b/tests/core/engine_adapter/integration/test_integration_postgres.py @@ -1,4 +1,5 @@ import typing as t +import uuid from contextlib import contextmanager import pytest from pytest import FixtureRequest @@ -53,7 +54,8 @@ def create_users( _cleanup_user(engine_adapter, user_name) for role_name in role_names: - user_name = f"test_{role_name}" + random_suffix = uuid.uuid4().hex[:6] + user_name = f"test_{role_name}_{random_suffix}" password = random_id() engine_adapter.execute(f"CREATE USER \"{user_name}\" WITH PASSWORD '{password}'") engine_adapter.execute(f'GRANT USAGE ON SCHEMA public TO "{user_name}"') @@ -381,12 +383,14 @@ def test_grants_plan_target_layer_physical_only( with create_users(engine_adapter, "reader") as roles: (tmp_path / "models").mkdir(exist_ok=True) - model_def = """ + reader_username = roles["reader"]["username"] + + model_def = f""" MODEL ( name test_schema.physical_grants_model, kind FULL, grants ( - 'select' = ['test_reader'] + 'select' = ['{reader_username}'] ), grants_target_layer 'physical' ); @@ -421,12 +425,14 @@ def test_grants_plan_target_layer_virtual_only( with create_users(engine_adapter, "viewer") as roles: (tmp_path / "models").mkdir(exist_ok=True) - model_def = """ + viewer_username = roles["viewer"]["username"] + + model_def = f""" MODEL ( name test_schema.virtual_grants_model, kind FULL, grants ( - 'select' = ['test_viewer'] + 'select' = ['{viewer_username}'] ), grants_target_layer 'virtual' ); From fc2976affd8937440f8bbf8d6383c30f7100ffa6 Mon Sep 17 00:00:00 2001 From: davem-bis <68955845+davem-bis@users.noreply.github.com> Date: Sun, 30 Aug 2026 06:45:14 +0100 Subject: [PATCH 29/67] chore: Resolve starlette deprecation warnings (#5967) Signed-off-by: davem-bis <68955845+davem-bis@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- pyproject.toml | 2 +- tests/web/test_main.py | 2 +- web/server/exceptions.py | 4 ++-- web/server/utils.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bc12bc30ff..2c897de225 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "google-auth", "google-cloud-bigquery", "google-cloud-bigquery-storage", - "httpx", + "httpx2", "mypy~=1.13.0", "numpy", "pandas-stubs", diff --git a/tests/web/test_main.py b/tests/web/test_main.py index b20947c49d..69d0cd0449 100644 --- a/tests/web/test_main.py +++ b/tests/web/test_main.py @@ -7,7 +7,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from httpx import ASGITransport, AsyncClient +from httpx2 import ASGITransport, AsyncClient from pytest_mock.plugin import MockerFixture from sqlmesh.core.context import Context diff --git a/web/server/exceptions.py b/web/server/exceptions.py index 8c62b0b005..21365b7f7d 100644 --- a/web/server/exceptions.py +++ b/web/server/exceptions.py @@ -3,7 +3,7 @@ import typing as t from fastapi import HTTPException -from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from starlette.status import HTTP_422_UNPROCESSABLE_CONTENT from sqlmesh.utils.date import now_timestamp from web.server.models import ApiExceptionPayload @@ -14,7 +14,7 @@ def __init__( self, origin: str, message: str, - status_code: int = HTTP_422_UNPROCESSABLE_ENTITY, + status_code: int = HTTP_422_UNPROCESSABLE_CONTENT, trigger: t.Optional[str] = None, ): super().__init__(status_code) diff --git a/web/server/utils.py b/web/server/utils.py index 868425e76e..1ddebf764d 100644 --- a/web/server/utils.py +++ b/web/server/utils.py @@ -9,7 +9,7 @@ import pyarrow as pa # type: ignore from fastapi import Depends, HTTPException from starlette.responses import StreamingResponse -from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY +from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_CONTENT from sqlmesh.core import constants as c from web.server.console import api_console @@ -45,7 +45,7 @@ def func_wrapper() -> R: message="An unexpected error occurred", origin="API -> utils -> run_in_executor", trigger="An unexpected error occurred", - status_code=HTTP_422_UNPROCESSABLE_ENTITY, + status_code=HTTP_422_UNPROCESSABLE_CONTENT, ) ) raise e From 366712435b0942c4010733c3a1224a2b811b54bb Mon Sep 17 00:00:00 2001 From: Mohamed Reguigui Date: Mon, 31 Aug 2026 04:54:43 +0100 Subject: [PATCH 30/67] fix(model): keep quoting when normalizing column_descriptions keys (#5959) Signed-off-by: ReguiguiMohamed --- sqlmesh/core/model/meta.py | 18 +++++----- tests/core/test_model.py | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 94956dff99..67ea32f9d9 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -321,17 +321,19 @@ def _column_descriptions_validator( if isinstance(vs, (exp.Tuple, exp.Array)): vs = vs.expressions - raw_col_descriptions = ( - vs + # Normalize each part while it is still an identifier, so that a quoted + # column keeps its case on dialects where quoting makes it significant. + col_descriptions = ( + {normalize_identifiers(k, dialect=dialect).name: v for k, v in vs.items()} if isinstance(vs, dict) - else {".".join([part.this for part in v.this.parts]): v.expression.name for v in vs} + else { + ".".join( + normalize_identifiers(part, dialect=dialect).name for part in v.this.parts + ): v.expression.name + for v in vs + } ) - col_descriptions = { - normalize_identifiers(k, dialect=dialect).name: v - for k, v in raw_col_descriptions.items() - } - columns_to_types = data.get("columns_to_types_") if columns_to_types: from sqlmesh.core.console import get_console diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 2f8ff49f7d..c9045c9506 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1000,6 +1000,75 @@ def test_column_descriptions(sushi_context, assert_exp_eq): assert model.column_descriptions == {"id": "primary key", "foo": "bar"} +def test_column_descriptions_quoted_identifier(): + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect snowflake, + column_descriptions ( + "myColumn" = 'a case-sensitive column', + other_column = 'an unquoted column' + ) + ); + + SELECT 1 AS "myColumn", 2 AS other_column + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + # A quoted key keeps its case, an unquoted one is still normalized. + assert model.column_descriptions == { + "myColumn": "a case-sensitive column", + "OTHER_COLUMN": "an unquoted column", + } + assert set(model.column_descriptions) <= set(model.columns_to_types) + + +def test_column_descriptions_dotted_identifier(): + # A nested field is looked up by its dotted path, so every part normalizes on its own. + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect bigquery, + column_descriptions ( + record.`myField` = 'a nested field' + ) + ); + + SELECT STRUCT(1 AS `myField`) AS record + """ + ) + model = load_sql_based_model(expressions, dialect="bigquery") + + assert model.column_descriptions == {"record.myfield": "a nested field"} + + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect snowflake, + column_descriptions ( + nested.field = 'an unquoted path', + "MyStruct"."myField" = 'a quoted path' + ) + ); + + SELECT 1 AS c + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + assert model.column_descriptions == { + "NESTED.FIELD": "an unquoted path", + "MyStruct.myField": "a quoted path", + } + + def test_model_jinja_macro_reference_extraction(): @macro() def test_macro(**kwargs) -> None: From c99553f8ddf08d861a263c11284937de6c62e31f Mon Sep 17 00:00:00 2001 From: Aravind S <63240570+ARAVIND281@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:10:53 +0530 Subject: [PATCH 31/67] chore(ci): add dbt 1.11 to the test matrix (#5999) Signed-off-by: Aravind S <63240570+ARAVIND281@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 2 +- Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 52a561985d..d9e9552970 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -457,7 +457,7 @@ jobs: strategy: fail-fast: false matrix: - dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10'] + dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11'] steps: - uses: actions/checkout@v7 - name: Set up Python diff --git a/Makefile b/Makefile index 3edd6066ef..94cc8e492d 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,8 @@ install-dev-dbt-%: echo "Installing dbt version: $$version"; \ cp pyproject.toml pyproject.toml.backup; \ $(SED_INPLACE) 's/"pydantic>=2.0.0"/"pydantic"/g' pyproject.toml; \ - if [ "$$version" = "1.10.0" ]; then \ - echo "Applying special handling for dbt 1.10.0"; \ + if [ "$$version" = "1.10.0" ] || [ "$$version" = "1.11.0" ]; then \ + echo "Applying special handling for dbt $$version"; \ $(SED_INPLACE) -E 's/"(dbt-core)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ $(SED_INPLACE) -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|redshift|trino))[^"]*"/"\1"/g' pyproject.toml; \ $(SED_INPLACE) -E 's/"(dbt-databricks)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ From cf0b176185406fcb49439dc8bf55e26872420d53 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:58:53 -0600 Subject: [PATCH 32/67] fix(web): adjusting paths to fix windows (#5998) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- tests/web/test_main.py | 28 +++++++++++++++++++++++++++- web/server/api/endpoints/files.py | 4 ++-- web/server/watcher.py | 6 +++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/web/test_main.py b/tests/web/test_main.py index 69d0cd0449..422bcfefbe 100644 --- a/tests/web/test_main.py +++ b/tests/web/test_main.py @@ -1,7 +1,7 @@ from __future__ import annotations import threading -from pathlib import Path +from pathlib import Path, PureWindowsPath import pyarrow as pa # type: ignore import pytest @@ -75,6 +75,32 @@ def test_get_file(client: TestClient, project_tmp_path: Path) -> None: } +def test_get_file_nested_path_matches_directory_listing( + client: TestClient, project_tmp_path: Path +) -> None: + models_dir = project_tmp_path / "models" + models_dir.mkdir() + (models_dir / "mymodel.sql").write_text("SELECT 1") + + response = client.get("/api/files/models/mymodel.sql") + assert response.status_code == 200 + assert response.json()["path"] == "models/mymodel.sql" + + +def test_get_file_relative_path_uses_posix_separators(tmp_path: Path) -> None: + file_path = tmp_path / "models" / "mymodel.sql" + file_path.parent.mkdir(parents=True) + file_path.write_text("SELECT 1") + + windows_relative = PureWindowsPath("models/mymodel.sql") + assert windows_relative.as_posix() == "models/mymodel.sql" + assert str(windows_relative) == "models\\mymodel.sql" + + file = _get_file_with_content(file_path, windows_relative.as_posix()) + assert file.path == "models/mymodel.sql" + assert file.path != str(windows_relative) + + def test_get_file_not_found(client: TestClient) -> None: response = client.get("/api/files/not_found.txt") assert response.status_code == 404 diff --git a/web/server/api/endpoints/files.py b/web/server/api/endpoints/files.py index db58fce55e..5be9032c6a 100644 --- a/web/server/api/endpoints/files.py +++ b/web/server/api/endpoints/files.py @@ -37,7 +37,7 @@ def get_file( """Get a file, including its contents.""" try: file_path = Path(path) - file = _get_file_with_content(settings.project_path / file_path, str(file_path)) + file = _get_file_with_content(settings.project_path / file_path, file_path.as_posix()) except FileNotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND) @@ -155,7 +155,7 @@ def walk_path( return sorted(directories, key=lambda x: x.name), sorted(files, key=lambda x: x.name) directories, files = walk_path(path) - relative_path = str(Path(path).relative_to(settings.project_path)) + relative_path = Path(path).relative_to(settings.project_path).as_posix() return models.Directory( name=os.path.basename(path), diff --git a/web/server/watcher.py b/web/server/watcher.py index 8bc87c8719..696d7dfd96 100644 --- a/web/server/watcher.py +++ b/web/server/watcher.py @@ -55,7 +55,7 @@ async def watch_project() -> None: changes.append( models.ArtifactChange( change=Change.deleted, - path=str(relative_path), + path=relative_path.as_posix(), ) ) elif change == Change.added: @@ -69,9 +69,9 @@ async def watch_project() -> None: models.ArtifactChange( type=models.ArtifactType.file, change=change, - path=str(relative_path), + path=relative_path.as_posix(), file=_get_file_with_content( - settings.project_path / relative_path, str(relative_path) + settings.project_path / relative_path, relative_path.as_posix() ), ) ) From 4c54937f156929e089a721ae19fb0c5ca15150ab Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:07:06 -0600 Subject: [PATCH 33/67] chore(docs): updating engine lists (#6001) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- docs/concepts/models/overview.md | 1 + docs/guides/configuration.md | 7 +++++-- docs/guides/connections.md | 7 ++++++- docs/installation.md | 3 +++ docs/quickstart/cli.md | 2 ++ docs/reference/configuration.md | 6 +++++- 6 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/concepts/models/overview.md b/docs/concepts/models/overview.md index e37be3822a..1fd16a96fc 100644 --- a/docs/concepts/models/overview.md +++ b/docs/concepts/models/overview.md @@ -190,6 +190,7 @@ This table lists each engine's support for `TABLE` and `VIEW` object comments: | Redshift | Y | N | | Snowflake | Y | Y | | Spark | Y | Y | +| StarRocks | Y | Y | | Trino | Y | Y | diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 41f4b05594..38f6814c8b 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -929,19 +929,22 @@ Example snowflake connection configuration: These pages describe the connection configuration options for each execution engine. * [Athena](../integrations/engines/athena.md) +* [Azure SQL](../integrations/engines/azuresql.md) * [BigQuery](../integrations/engines/bigquery.md) +* [ClickHouse](../integrations/engines/clickhouse.md) * [Databricks](../integrations/engines/databricks.md) * [DuckDB](../integrations/engines/duckdb.md) * [Fabric](../integrations/engines/fabric.md) * [MotherDuck](../integrations/engines/motherduck.md) -* [MySQL](../integrations/engines/mysql.md) * [MSSQL](../integrations/engines/mssql.md) +* [MySQL](../integrations/engines/mysql.md) * [Postgres](../integrations/engines/postgres.md) * [GCP Postgres](../integrations/engines/gcp-postgres.md) * [Redshift](../integrations/engines/redshift.md) +* [RisingWave](../integrations/engines/risingwave.md) * [Snowflake](../integrations/engines/snowflake.md) -* [StarRocks](../integrations/engines/starrocks.md) * [Spark](../integrations/engines/spark.md) +* [StarRocks](../integrations/engines/starrocks.md) * [Trino](../integrations/engines/trino.md) #### State connection diff --git a/docs/guides/connections.md b/docs/guides/connections.md index bc763f3f5a..5af44c5dac 100644 --- a/docs/guides/connections.md +++ b/docs/guides/connections.md @@ -79,15 +79,20 @@ default_gateway: local_db ## Supported engines +* [Athena](../integrations/engines/athena.md) +* [Azure SQL](../integrations/engines/azuresql.md) * [BigQuery](../integrations/engines/bigquery.md) +* [ClickHouse](../integrations/engines/clickhouse.md) * [Databricks](../integrations/engines/databricks.md) * [DuckDB](../integrations/engines/duckdb.md) +* [Fabric](../integrations/engines/fabric.md) * [MotherDuck](../integrations/engines/motherduck.md) -* [MySQL](../integrations/engines/mysql.md) * [MSSQL](../integrations/engines/mssql.md) +* [MySQL](../integrations/engines/mysql.md) * [Postgres](../integrations/engines/postgres.md) * [GCP Postgres](../integrations/engines/gcp-postgres.md) * [Redshift](../integrations/engines/redshift.md) +* [RisingWave](../integrations/engines/risingwave.md) * [Snowflake](../integrations/engines/snowflake.md) * [Spark](../integrations/engines/spark.md) * [StarRocks](../integrations/engines/starrocks.md) diff --git a/docs/installation.md b/docs/installation.md index f12ec566e2..6bcac928e5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -54,13 +54,16 @@ Other extras are required to use specific SQL engines, like Bigquery or Postgres | Bigquery | `pip install "sqlmesh[bigquery]"` | | ClickHouse | `pip install "sqlmesh[clickhouse]"` | | Databricks | `pip install "sqlmesh[databricks]"` | + | Fabric | `pip install "sqlmesh[fabric]"` | | GCP Postgres | `pip install "sqlmesh[gcppostgres]"` | | MS SQL Server | `pip install "sqlmesh[mssql]"` | + | MotherDuck | `pip install "sqlmesh[motherduck]"` | | MySQL | `pip install "sqlmesh[mysql]"` | | Postgres | `pip install "sqlmesh[postgres]"` | | Redshift | `pip install "sqlmesh[redshift]"` | | RisingWave | `pip install "sqlmesh[risingwave]"` | | Snowflake | `pip install "sqlmesh[snowflake]"` | + | StarRocks | `pip install "sqlmesh[starrocks]"` | | Trino | `pip install "sqlmesh[trino]"` | Multiple extras can be installed at once, as in `pip install "sqlmesh[github,slack]"`. diff --git a/docs/quickstart/cli.md b/docs/quickstart/cli.md index a592847470..6aa4fc34ed 100644 --- a/docs/quickstart/cli.md +++ b/docs/quickstart/cli.md @@ -116,6 +116,8 @@ Choose your SQL engine: [14] MySQL [15] Athena [16] RisingWave + [17] Fabric + [18] StarRocks Enter a number: 1 ``` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a1bf400c32..ae036a7774 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -218,18 +218,22 @@ Most parameters are specific to the connection engine `type` - see [below](#engi These pages describe the connection configuration options for each execution engine. * [Athena](../integrations/engines/athena.md) +* [Azure SQL](../integrations/engines/azuresql.md) * [BigQuery](../integrations/engines/bigquery.md) * [ClickHouse](../integrations/engines/clickhouse.md) * [Databricks](../integrations/engines/databricks.md) * [DuckDB](../integrations/engines/duckdb.md) +* [Fabric](../integrations/engines/fabric.md) * [MotherDuck](../integrations/engines/motherduck.md) -* [MySQL](../integrations/engines/mysql.md) * [MSSQL](../integrations/engines/mssql.md) +* [MySQL](../integrations/engines/mysql.md) * [Postgres](../integrations/engines/postgres.md) * [GCP Postgres](../integrations/engines/gcp-postgres.md) * [Redshift](../integrations/engines/redshift.md) +* [RisingWave](../integrations/engines/risingwave.md) * [Snowflake](../integrations/engines/snowflake.md) * [Spark](../integrations/engines/spark.md) +* [StarRocks](../integrations/engines/starrocks.md) * [Trino](../integrations/engines/trino.md) ### Scheduler From 240471c1d2f7f51cc2fa5764159d857e8d10a740 Mon Sep 17 00:00:00 2001 From: cookieark7 <67856015+cookieark7@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:05:10 +0530 Subject: [PATCH 34/67] fix(clickhouse): register table comments via command instead of inline CTAS (#6007) Signed-off-by: cookieark Co-authored-by: cookieark --- sqlmesh/core/engine_adapter/clickhouse.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index d1f67e0564..92e3c33e6f 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -13,6 +13,7 @@ DataObjectType, EngineRunMode, SourceQuery, + CommentCreationTable, CommentCreationView, InsertOverwriteStrategy, ) @@ -36,6 +37,7 @@ class ClickhouseEngineAdapter(EngineAdapterWithIndexSupport, LogicalMergeMixin): SUPPORTS_TRANSACTIONS = False SUPPORTS_VIEW_SCHEMA = False SUPPORTS_REPLACE_TABLE = False + COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY SCHEMA_DIFFER_KWARGS = {} From b2fa0a64d0f61e0c27f1a8edd880026454472066 Mon Sep 17 00:00:00 2001 From: avakili-voleon Date: Tue, 1 Sep 2026 12:54:21 -0400 Subject: [PATCH 35/67] fix: open files read-only during format --check (#5961) Signed-off-by: Amir Vakili --- sqlmesh/core/context.py | 3 ++- tests/core/test_format.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e335253016..eca42e1295 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1338,7 +1338,8 @@ def format( ): # introduced to satisfy type checker as still want to pull filter out as many targets as possible before loop continue - with open(target._path, "r+", encoding="utf-8") as file: + mode = "r" if check else "r+" + with open(target._path, mode, encoding="utf-8") as file: before = file.read() after = self._format( diff --git a/tests/core/test_format.py b/tests/core/test_format.py index 5a44e1b381..e5b2e8460b 100644 --- a/tests/core/test_format.py +++ b/tests/core/test_format.py @@ -1,4 +1,6 @@ +import os import pathlib +import stat from pytest_mock.plugin import MockerFixture from sqlmesh.core.config import Config @@ -146,6 +148,25 @@ def test_ignore_formating_files(tmp_path: pathlib.Path): ) +def test_format_check_read_only_files(tmp_path: pathlib.Path, mocker: MockerFixture): + models_dir = pathlib.Path("models") + + model_text = "MODEL(name this.model, dialect 'duckdb'); SELECT 1 AS col" + model = create_temp_file( + tmp_path, + pathlib.Path(models_dir, "model.sql"), + model_text, + ) + os.chmod(model, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + + context = Context(paths=tmp_path, config=Config()) + context.console = mocker.Mock() + context.load() + + assert not context.format(check=True) + assert model.read_text(encoding="utf-8") == model_text + + def test_format_without_state_load(tmp_path: pathlib.Path, mocker: MockerFixture): mock = mocker.patch( "sqlmesh.core.state_sync.db.facade.EngineAdapterStateSync.get_versions", From 4b203623454d73b40d46919f2cd2afdf902be6a4 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:29:08 -0600 Subject: [PATCH 36/67] chore(docs): dbt audits are supported (#5995) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- docs/integrations/dbt.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index 3a5b4f383f..4ea0fd27f7 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -287,9 +287,12 @@ If set to `False`, then the snapshot will be skipped and a warning will be logge Support for this will be added soon. ## Tests -SQLMesh uses dbt tests to perform SQLMesh [audits](../concepts/audits.md) (coming soon). -Add SQLMesh [unit tests](../concepts/tests.md) to a dbt project by placing them in the "tests" directory. +dbt and SQLMesh use the word "test" differently. See [Workflow differences](#workflow-differences-between-sqlmesh-and-dbt) above for an overview. + +SQLMesh converts dbt tests (singular and generic `.sql` files in the `tests/` directory) into SQLMesh [audits](../concepts/audits.md). These run against data that already exists in your tables, just like dbt tests. + +Add SQLMesh [unit tests](../concepts/tests.md) to a dbt project by placing YAML files named `test*.yaml` or `test*.yml` in the `tests/` directory. Unit tests validate model query logic with predefined inputs and expected outputs; they are separate from dbt's SQL test files. ## Seed column types From cc3065065689386aa8863338112a31958f0ec778 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:46:00 -0600 Subject: [PATCH 37/67] fix(bigquery): ci test now partitions correctly (#5989) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .../engine_adapter/integration/test_integration_bigquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 75909948b2..3c8d07210f 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -490,7 +490,7 @@ def test_run_correlation_id_in_job_labels(ctx: TestContext): cron '@daily', start '2023-01-07' ); -SELECT 1 AS col, '2023-01-07' AS event_ts +SELECT 1 AS col, CAST('2023-01-07' AS DATE) AS event_ts """ ) ) From 869248c38aa06e8792be4c69922b92ab43bdb206 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:31:21 -0600 Subject: [PATCH 38/67] feature: adding smarter unit test execution (#5966) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Co-authored-by: Cursor --- docs/concepts/overview.md | 4 +- docs/concepts/tests.md | 4 +- docs/faq/faq.md | 2 +- docs/guides/models.md | 2 +- docs/guides/testing.md | 10 +- docs/reference/cli.md | 4 + docs/reference/notebook.md | 12 +- sqlmesh/cli/main.py | 17 +++ sqlmesh/core/console.py | 7 + sqlmesh/core/context.py | 79 +++++++++-- sqlmesh/core/test/__init__.py | 1 + sqlmesh/core/test/discovery.py | 33 +++++ sqlmesh/core/test/result.py | 3 + sqlmesh/magics.py | 19 +++ tests/cli/test_cli.py | 124 ++++++++++++++++++ tests/core/test_context.py | 12 +- tests/core/test_test.py | 26 ++++ .../github/cicd/test_github_controller.py | 22 +++- 18 files changed, 353 insertions(+), 28 deletions(-) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 32ad4176ae..47ae8ffd24 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -52,7 +52,9 @@ Bad data is worse than no data. The best way to keep bad data out of your system ### [Tests](./tests.md) SQLMesh "tests" are similar to unit tests in software development, where the unit is a single model. SQLMesh tests validate model *code* — you specify the input data and expected output, then SQLMesh runs the test and compares the expected and actual output. -SQLMesh automatically runs tests when you apply a `plan`, or you can run them on demand with the [`test` command](../reference/cli.md#test). +SQLMesh automatically runs all unit tests when a `plan` is created. Use `--test-changed-only` to run tests only for models included in the plan (added, modified, or restated), `--skip-tests` to skip, run tests for specific models with [`sqlmesh test --select-model`](../reference/cli.md#test), or run the full suite on demand with the [`test` command](../reference/cli.md#test). + +Learn more in the [testing guide](../guides/testing.md). ### [Audits](./audits.md) In contrast to tests, SQLMesh "audits" validate the results of model code applied to your actual data. diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index c1714ea982..a293237687 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -1,6 +1,8 @@ # Testing -Testing allows you to protect your project from regression by continuously verifying that the output of each model matches your expectations. Unlike [audits](audits.md), tests are executed either on demand (for example, as part of a CI/CD job) or every time a new [plan](plans.md) is created. +Testing allows you to protect your project from regression by continuously verifying that the output of each model matches your expectations. Unlike [audits](audits.md), tests are executed either on demand (for example, as part of a CI/CD job or via [`sqlmesh test`](../reference/cli.md#test)) or when a new [plan](plans.md) is created. + +By default, `sqlmesh plan` runs all unit tests. Use `--test-changed-only` to run tests only for models included in the plan (added, modified, or restated), or `--skip-tests` to run none. With both `--select-model` and `--test-changed-only`, tests run only for selected models that changed. Similar to unit testing in software development, SQLMesh evaluates the model's logic against predefined inputs and then compares the output to expected outcomes provided as part of each test. diff --git a/docs/faq/faq.md b/docs/faq/faq.md index b4a0d7e4d4..accd41a85f 100644 --- a/docs/faq/faq.md +++ b/docs/faq/faq.md @@ -102,7 +102,7 @@ SQLMesh's default behavior is appropriate for most deployments, but you can override where SQLMesh creates physical tables and views with [schema configuration options](../guides/configuration.md#environment-schemas). ??? question "What's the difference between a `test` and an `audit`?" - A SQLMesh [`test`](../concepts/tests.md) is analogous to a "unit test" in software engineering. It tests *code* based on known inputs and outputs. In SQLMesh, the inputs and outputs are specified in a YAML file, and SQLMesh automatically runs them when `sqlmesh plan` is executed. + A SQLMesh [`test`](../concepts/tests.md) is analogous to a "unit test" in software engineering. It tests *code* based on known inputs and outputs. In SQLMesh, the inputs and outputs are specified in a YAML file, and SQLMesh runs all unit tests when `sqlmesh plan` is executed (use `--test-changed-only` to run only tests for models in the plan). Writing YAML is annoying and error-prone, so SQLMesh's [`create_test` command](../concepts/tests.md#automatic-test-generation) allows you to automatically generate YAML test files based on queries of existing data tables. diff --git a/docs/guides/models.md b/docs/guides/models.md index e3b4ab1cfa..074e581937 100644 --- a/docs/guides/models.md +++ b/docs/guides/models.md @@ -161,7 +161,7 @@ Reverting to a previous model version is a quick operation since no additional w SQLMesh automatically validates your models in order to ensure the quality and accuracy of your data. This is done via the following: -* Running unit tests by default when you execute the `plan` command. This ensures all changes to applied to any environment are logically validated. Refer to [testing](../concepts/tests.md) for more information. +* Running all unit tests when you execute the `plan` command (use `--test-changed-only` to run only tests for models in the plan). This ensures changes applied to any environment are logically validated. Refer to [testing](../concepts/tests.md) for more information. * Running audits whenever data is loaded to a table (either for backfill or loading on a cadence). This way you know all data present in any table has passed all defined audits. Refer to [auditing](../concepts/audits.md) for more information. SQLMesh also provides automatic validation via CI/CD by automatically creating a preview environment. diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 1045759e25..f837e9f00a 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -12,11 +12,19 @@ OK ``` As the unit tests run, SQLMesh will identify any that fail. +By default, `sqlmesh plan` runs all unit tests. Use `--test-changed-only` to run tests only for models included in the plan (added, modified, or restated), or `--skip-tests` to run none. With both `--select-model` and `--test-changed-only`, tests run only for selected models that changed. + For more information about tests, refer to [testing](../concepts/tests.md). ### Test changes to a specific model -To run a specific model test, pass in the suite file name followed by `::` and the name of the test; for example: `sqlmesh test tests/test_suite.yaml::test_example_full_model`. +To run unit tests for a specific model, use `--select-model`: + +```bash +$ sqlmesh test --select-model sqlmesh_example.full_model +``` + +Alternatively, pass in the suite file name followed by `::` and the name of the test; for example: `sqlmesh test tests/test_suite.yaml::test_example_full_model`. ### Run a subset of tests diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 82b4161277..1367f8551b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -357,6 +357,8 @@ Options: Default: prod. --skip-tests Skip tests prior to generating the plan if they are defined. + --test-changed-only Run unit tests only for models included in + the plan instead of all tests. --skip-linter Skip linting prior to generating the plan if the linter is enabled. -r, --restate-model TEXT Restate data for specified models and models @@ -626,6 +628,8 @@ Options: -v, --verbose Verbose output. --preserve-fixtures Preserve the fixture tables in the testing database, useful for debugging. + --select-model TEXT Select specific models to run unit tests for. Can be + specified multiple times. --help Show this message and exit. ``` diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 6cac4e1078..17ce26b4c6 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -96,7 +96,7 @@ options: #### plan ``` %plan [--start START] [--end END] [--execution-time EXECUTION_TIME] - [--create-from CREATE_FROM] [--skip-tests] + [--create-from CREATE_FROM] [--skip-tests] [--test-changed-only] [--restate-model [RESTATE_MODEL ...]] [--no-gaps] [--skip-backfill, --dry-run] [--forward-only] [--effective-from EFFECTIVE_FROM] [--no-prompts] [--auto-apply] @@ -120,6 +120,8 @@ options: The environment to create the target environment from if it doesn't exist. Default: prod. --skip-tests, -t Skip the unit tests defined for the model. + --test-changed-only Run unit tests only for models included in the plan + instead of all tests. --restate-model <[RESTATE_MODEL ...]>, -r <[RESTATE_MODEL ...]> Restate data for specified models (and models downstream from the one specified). For production @@ -131,7 +133,8 @@ options: comparing to existing snapshots for matching models in the target environment. --skip-backfill, --dry-run - Skip the backfill step and only create a virtual update for the plan. + Skip the backfill step and only create a virtual + update for the plan. --forward-only Create a plan for forward-only changes. --effective-from EFFECTIVE_FROM The effective date from which to apply forward-only @@ -430,7 +433,8 @@ options: #### run_test ``` -%run_test [--pattern [PATTERN ...]] [--verbose] [--preserve-fixtures] [tests ...] +%run_test [--pattern [PATTERN ...]] [--verbose] [--preserve-fixtures] + [--select-model [SELECT_MODEL ...]] [tests ...] Run unit test(s). @@ -443,6 +447,8 @@ options: --verbose, -v Verbose output. --preserve-fixtures Preserve the fixture tables in the testing database, useful for debugging. + --select-model <[SELECT_MODEL ...]> + Select specific models to run unit tests for. ``` #### audit diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index c2dc1e3dbb..b6678136f0 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -427,6 +427,12 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: help="Skip tests prior to generating the plan if they are defined.", default=None, ) +@click.option( + "--test-changed-only", + is_flag=True, + help="Run unit tests only for models included in the plan instead of all tests.", + default=None, +) @click.option( "--skip-linter", is_flag=True, @@ -799,6 +805,12 @@ def create_test( default=False, help="Preserve the fixture tables in the testing database, useful for debugging.", ) +@click.option( + "--select-model", + type=str, + multiple=True, + help="Select specific models to run unit tests for.", +) @click.argument("tests", nargs=-1) @click.pass_obj @error_handler @@ -808,14 +820,19 @@ def test( k: t.List[str], verbose: int, preserve_fixtures: bool, + select_model: t.List[str], tests: t.List[str], ) -> None: """Run model unit tests.""" + model_names = ( + obj._new_selector().expand_model_selections(select_model) if select_model else None + ) result = obj.test( match_patterns=k, tests=tests, verbosity=Verbosity(verbose), preserve_fixtures=preserve_fixtures, + model_names=model_names, ) if not result.wasSuccessful(): exit(1) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index f9a758b405..ca11b6e607 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2232,6 +2232,9 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> message = ( f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds." ) + if result.tests_skipped: + message = f"{message}\nSkipped {result.tests_skipped} tests" + if result.wasSuccessful(): self._print("=" * divider_length) self._print( @@ -3160,6 +3163,8 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> message = ( f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds." ) + if result.tests_skipped: + message = f"{message}\nSkipped {result.tests_skipped} tests" if result.wasSuccessful(): success_color = {"color": "#008000"} @@ -3600,6 +3605,8 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> return message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`" + if result.tests_skipped: + message = f"{message}\n**Skipped `{result.tests_skipped}` Tests**" if result.wasSuccessful(): self._print(f"**Successfully {message}**\n\n") diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index eca42e1295..b70ccae141 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -115,6 +115,7 @@ ModelTestMetadata, generate_test, run_tests, + filter_tests_by_model_names, filter_tests_by_patterns, ) from sqlmesh.core.user import User @@ -1416,6 +1417,7 @@ def plan( execution_time: t.Optional[TimeLike] = None, create_from: t.Optional[str] = None, skip_tests: t.Optional[bool] = None, + test_changed_only: t.Optional[bool] = None, restate_models: t.Optional[t.Iterable[str]] = None, no_gaps: t.Optional[bool] = None, skip_backfill: t.Optional[bool] = None, @@ -1453,6 +1455,7 @@ def plan( create_from: The environment to create the target environment from if it doesn't exist. If not specified, the "prod" environment will be used. skip_tests: Unit tests are run by default so this will skip them if enabled + test_changed_only: Run unit tests only for models included in the plan instead of all tests restate_models: A list of either internal or external models, or tags, that need to be restated for the given plan interval. If the target environment is a production environment, ALL snapshots that depended on these upstream tables will have their intervals deleted @@ -1499,6 +1502,7 @@ def plan( execution_time=execution_time, create_from=create_from, skip_tests=skip_tests, + test_changed_only=test_changed_only, restate_models=restate_models, no_gaps=no_gaps, skip_backfill=skip_backfill, @@ -1553,6 +1557,7 @@ def plan_builder( execution_time: t.Optional[TimeLike] = None, create_from: t.Optional[str] = None, skip_tests: t.Optional[bool] = None, + test_changed_only: t.Optional[bool] = None, restate_models: t.Optional[t.Iterable[str]] = None, no_gaps: t.Optional[bool] = None, skip_backfill: t.Optional[bool] = None, @@ -1587,6 +1592,7 @@ def plan_builder( create_from: The environment to create the target environment from if it doesn't exist. If not specified, the "prod" environment will be used. skip_tests: Unit tests are run by default so this will skip them if enabled + test_changed_only: Run unit tests only for models included in the plan instead of all tests restate_models: A list of either internal or external models, or tags, that need to be restated for the given plan interval. If the target environment is a production environment, ALL snapshots that depended on these upstream tables will have their intervals deleted @@ -1628,6 +1634,7 @@ def plan_builder( "execution_time": execution_time, "create_from": create_from, "skip_tests": skip_tests, + "test_changed_only": test_changed_only, "restate_models": list(restate_models) if restate_models is not None else None, "no_gaps": no_gaps, "skip_backfill": skip_backfill, @@ -1657,6 +1664,11 @@ def plan_builder( } skip_tests = explain or skip_tests or False + test_changed_only = test_changed_only or False + + if skip_tests and test_changed_only: + raise PlanError("Cannot combine --skip-tests with --test-changed-only.") + no_gaps = no_gaps or False skip_backfill = skip_backfill or False empty_backfill = empty_backfill or False @@ -1683,8 +1695,6 @@ def plan_builder( if not skip_linter: self.lint_models() - self._run_plan_tests(skip_tests=skip_tests) - environment_ttl = ( self.environment_ttl if environment not in self.pinned_environments else None ) @@ -1767,6 +1777,19 @@ def plan_builder( *[s.name for s in context_diff.added], } + plan_test_model_names: t.Set[str] = { + *modified_model_names, + *(expanded_restate_models or set()), + } + if select_models and test_changed_only: + plan_test_model_names &= selected_fqns + + self._run_plan_tests( + skip_tests=skip_tests, + test_changed_only=test_changed_only, + model_names=plan_test_model_names, + ) + if ( is_dev and not include_unmodified @@ -2383,6 +2406,7 @@ def test( verbosity: Verbosity = Verbosity.DEFAULT, preserve_fixtures: bool = False, stream: t.Optional[t.TextIO] = None, + model_names: t.Optional[t.Collection[str]] = None, ) -> ModelTextTestResult: """Discover and run model tests""" if verbosity >= Verbosity.VERBOSE: @@ -2390,7 +2414,15 @@ def test( pd.set_option("display.max_columns", None) - test_meta = self.select_tests(tests=tests, patterns=match_patterns) + baseline_meta = self.select_tests(tests=tests, patterns=match_patterns, model_names=None) + if model_names is not None: + test_meta = self.select_tests( + tests=tests, patterns=match_patterns, model_names=model_names + ) + tests_skipped = len(baseline_meta) - len(test_meta) + else: + test_meta = baseline_meta + tests_skipped = 0 result = run_tests( model_test_metadata=test_meta, @@ -2404,6 +2436,7 @@ def test( default_catalog=self.default_catalog, default_catalog_dialect=self.config.dialect or "", ) + result.tests_skipped = tests_skipped self.console.log_test_results( result, @@ -2850,15 +2883,28 @@ def _run_tests( result = self.test(stream=test_output_io, verbosity=verbosity) return result, test_output_io.getvalue() - def _run_plan_tests(self, skip_tests: bool = False) -> t.Optional[ModelTextTestResult]: - if not skip_tests: - result = self.test() - if not result.wasSuccessful(): - raise PlanError( - "Cannot generate plan due to failing test(s). Fix test(s) and run again." - ) - return result - return None + def _run_plan_tests( + self, + skip_tests: bool = False, + test_changed_only: bool = False, + model_names: t.Optional[t.Collection[str]] = None, + ) -> t.Optional[ModelTextTestResult]: + if skip_tests: + return None + + effective_names: t.Optional[t.Set[str]] = None + + if test_changed_only: + effective_names = set(model_names or []) + if not effective_names: + return None + + result = self.test(model_names=effective_names) + if not result.wasSuccessful(): + raise PlanError( + "Cannot generate plan due to failing test(s). Fix test(s) and run again." + ) + return result def _warn_if_virtual_catalog_rematerialization(self, plan: "Plan") -> None: """Warn when ClickHouse models appear as new snapshots solely because a virtual catalog @@ -3569,6 +3615,7 @@ def select_tests( self, tests: t.Optional[t.List[str]] = None, patterns: t.Optional[t.List[str]] = None, + model_names: t.Optional[t.Collection[str]] = None, ) -> t.List[ModelTestMetadata]: """Filter pre-loaded test metadata based on tests and patterns.""" @@ -3592,6 +3639,14 @@ def select_tests( if patterns: test_meta = filter_tests_by_patterns(test_meta, patterns) + if model_names is not None: + test_meta = filter_tests_by_model_names( + test_meta, + set(model_names), + default_catalog=self.default_catalog, + dialect=self.default_dialect, + ) + return test_meta diff --git a/sqlmesh/core/test/__init__.py b/sqlmesh/core/test/__init__.py index 6353370f45..279c97b744 100644 --- a/sqlmesh/core/test/__init__.py +++ b/sqlmesh/core/test/__init__.py @@ -3,6 +3,7 @@ from sqlmesh.core.test.definition import ModelTest as ModelTest, generate_test as generate_test from sqlmesh.core.test.discovery import ( ModelTestMetadata as ModelTestMetadata, + filter_tests_by_model_names as filter_tests_by_model_names, filter_tests_by_patterns as filter_tests_by_patterns, ) from sqlmesh.core.test.result import ModelTextTestResult as ModelTextTestResult diff --git a/sqlmesh/core/test/discovery.py b/sqlmesh/core/test/discovery.py index 9afe3dd7fc..bfc67fd1e3 100644 --- a/sqlmesh/core/test/discovery.py +++ b/sqlmesh/core/test/discovery.py @@ -9,6 +9,7 @@ from sqlmesh.utils import unique from sqlmesh.utils.pydantic import PydanticModel +from sqlmesh.core.dialect import normalize_model_name class ModelTestMetadata(PydanticModel): @@ -46,3 +47,35 @@ def filter_tests_by_patterns( if ("*" in pattern and fnmatch.fnmatchcase(test.fully_qualified_test_name, pattern)) or pattern in test.fully_qualified_test_name ) + + +def filter_tests_by_model_names( + tests: list[ModelTestMetadata], + model_names: set[str], + *, + default_catalog: t.Optional[str] = None, + dialect: t.Optional[str] = None, +) -> list[ModelTestMetadata]: + """Keep tests whose YAML ``model:`` resolves to one of the given model names. + + Args: + tests: Loaded test metadata. + model_names: Model FQNs / names to keep (typically from a plan change set). + default_catalog: Catalog used when normalizing short model names. + dialect: Dialect used when normalizing model names. + + Returns: + Tests that target a model in ``model_names``. + """ + + normalized_models = { + normalize_model_name(name, default_catalog=default_catalog, dialect=dialect) + for name in model_names + } + return [ + test + for test in tests + if test.model_name + and normalize_model_name(test.model_name, default_catalog=default_catalog, dialect=dialect) + in normalized_models + ] diff --git a/sqlmesh/core/test/result.py b/sqlmesh/core/test/result.py index eefa0be513..9ed671b1c6 100644 --- a/sqlmesh/core/test/result.py +++ b/sqlmesh/core/test/result.py @@ -15,6 +15,7 @@ class ModelTextTestResult(unittest.TextTestResult): successes: t.List[unittest.TestCase] + tests_skipped: int def __init__(self, *args: t.Any, **kwargs: t.Any): self.console = kwargs.pop("console", None) @@ -24,6 +25,7 @@ def __init__(self, *args: t.Any, **kwargs: t.Any): self.failure_tables: t.List[t.Tuple[t.Any, ...]] = [] self.original_errors: t.List[t.Tuple[unittest.TestCase, ErrorType]] = [] self.duration: t.Optional[float] = None + self.tests_skipped = 0 def addSubTest( self, @@ -123,6 +125,7 @@ def merge(self, other: ModelTextTestResult) -> None: self.addSkip(skipped_args[0], skipped_args[1]) self.testsRun += other.testsRun + self.tests_skipped += other.tests_skipped def get_fail_and_error_tests(self) -> t.List[ModelTest]: # If tests contain failed subtests (e.g testing CTE outputs) we don't want diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index ed6a1b62de..90b44cd62c 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -407,6 +407,11 @@ def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None action="store_true", help="Skip the unit tests defined for the model.", ) + @argument( + "--test-changed-only", + action="store_true", + help="Run unit tests only for models included in the plan instead of all tests.", + ) @argument( "--skip-linter", action="store_true", @@ -533,6 +538,7 @@ def plan(self, context: Context, line: str) -> None: execution_time=args.execution_time, create_from=args.create_from, skip_tests=args.skip_tests, + test_changed_only=args.test_changed_only, restate_models=args.restate_model, backfill_models=args.backfill_model, no_gaps=args.no_gaps, @@ -1078,18 +1084,31 @@ def create_test(self, context: Context, line: str) -> None: action="store_true", help="Preserve the fixture tables in the testing database, useful for debugging.", ) + @argument( + "--select-model", + type=str, + nargs="*", + help="Select specific models to run unit tests for.", + ) @line_magic @pass_sqlmesh_context def run_test(self, context: Context, line: str) -> None: """Run unit test(s).""" args = parse_argstring(self.run_test, line) + model_names = ( + context._new_selector().expand_model_selections(args.select_model) + if args.select_model + else None + ) + context.test( match_patterns=args.pattern, tests=args.tests, verbosity=Verbosity(args.verbose), preserve_fixtures=args.preserve_fixtures, stream=StringIO(), # consume the output instead of redirecting to stdout + model_names=model_names, ) @magic_arguments() diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index a8ee1aaa39..da73952991 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -59,6 +59,31 @@ def create_example_project(temp_dir, template=ProjectTemplate.DEFAULT) -> None: ) +def add_incremental_model_test(temp_dir) -> None: + with open(temp_dir / "tests" / "test_incremental_model.yaml", "w", encoding="utf-8") as f: + f.write( + """ +test_example_incremental_model: + model: sqlmesh_example.incremental_model + vars: + start: 2020-01-01 + end: 2020-01-02 + inputs: + sqlmesh_example.seed_model: + rows: + - id: 1 + item_id: 1 + event_date: 2020-01-01 + outputs: + query: + rows: + - id: 1 + item_id: 1 + event_date: 2020-01-01 +""" + ) + + def update_incremental_model(temp_dir) -> None: with open(temp_dir / "models" / "incremental_model.sql", "w", encoding="utf-8") as f: f.write( @@ -193,6 +218,105 @@ def test_plan_skip_tests(runner, tmp_path): assert_backfill_success(result) +def test_plan_no_changes_runs_tests_by_default(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + add_incremental_model_test(tmp_path) + + result = runner.invoke( + cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "plan", "--no-prompts"], input="\n" + ) + assert result.exit_code == 0 + assert "Successfully Ran 2 tests against duckdb" in result.output + assert "No changes to plan" in result.output or "No changes" in result.output + + +def test_plan_test_changed_only_with_no_changes(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + result = runner.invoke( + cli, + [ + "--log-file-dir", + tmp_path, + "--paths", + tmp_path, + "plan", + "--test-changed-only", + "--no-prompts", + ], + input="\n", + ) + assert result.exit_code == 0 + assert "Successfully Ran" not in result.output + + +def test_plan_test_changed_only_runs_only_changed_model_tests(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + add_incremental_model_test(tmp_path) + + full_model_path = tmp_path / "models" / "full_model.sql" + full_model_path.write_text( + full_model_path.read_text().replace("COUNT(DISTINCT id)", "COUNT(id)") + ) + + result = runner.invoke( + cli, + [ + "--log-file-dir", + tmp_path, + "--paths", + tmp_path, + "plan", + "--test-changed-only", + "--no-prompts", + "--auto-apply", + ], + ) + assert result.exit_code == 0 + assert "Successfully Ran 1 tests against duckdb" in result.output + assert "Skipped 1 tests" in result.output + + +def test_plan_select_model_test_changed_only_scopes_tests(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + add_incremental_model_test(tmp_path) + + full_model_path = tmp_path / "models" / "full_model.sql" + full_model_path.write_text( + full_model_path.read_text().replace("COUNT(DISTINCT id)", "COUNT(id)") + ) + incremental_model_path = tmp_path / "models" / "incremental_model.sql" + incremental_model_path.write_text( + incremental_model_path.read_text().replace( + " item_id,\n event_date,", + " item_id,\n 'b' as new_col,\n event_date,", + ) + ) + + result = runner.invoke( + cli, + [ + "--log-file-dir", + tmp_path, + "--paths", + tmp_path, + "plan", + "--select-model", + "sqlmesh_example.full_model", + "--test-changed-only", + "--no-prompts", + "--auto-apply", + ], + ) + assert result.exit_code == 0 + assert "Successfully Ran 1 tests against duckdb" in result.output + assert "Skipped 1 tests" in result.output + + def test_plan_skip_linter(runner, tmp_path): create_example_project(tmp_path) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 75737f1edb..1d87e1db18 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -5,7 +5,7 @@ import re from datetime import date, timedelta, datetime from tempfile import TemporaryDirectory -from unittest.mock import PropertyMock, call, patch +from unittest.mock import ANY, PropertyMock, call, patch import time_machine import pytest @@ -3788,11 +3788,19 @@ def test_prompt_if_uncategorized_snapshot(mocker: MockerFixture, tmp_path: Path) assert context.config.plan.no_prompts == True +def test_plan_skip_tests_and_test_changed_only(sushi_context: Context) -> None: + with pytest.raises( + PlanError, + match="Cannot combine --skip-tests with --test-changed-only.", + ): + sushi_context.plan("dev", skip_tests=True, test_changed_only=True, no_prompts=True) + + def test_plan_explain_skips_tests(sushi_context: Context, mocker: MockerFixture) -> None: sushi_context.console = TerminalConsole() spy = mocker.spy(sushi_context, "_run_plan_tests") sushi_context.plan(environment="dev", explain=True, no_prompts=True, include_unmodified=True) - spy.assert_called_once_with(skip_tests=True) + spy.assert_called_once_with(skip_tests=True, test_changed_only=False, model_names=ANY) def test_dev_environment_virtual_update_with_environment_statements(tmp_path: Path) -> None: diff --git a/tests/core/test_test.py b/tests/core/test_test.py index d679f09393..6ac20cc2d8 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -3527,3 +3527,29 @@ def test_cte_failure(tmp_path: Path) -> None: assert "Ran 1 tests" in output assert "Failed tests (1)" in output + + +def test_filter_tests_by_model_names(): + from sqlmesh.core.test.discovery import ModelTestMetadata, filter_tests_by_model_names + + tests = [ + ModelTestMetadata(path=Path("a.yaml"), test_name="t1", body={"model": "sushi.a"}), + ModelTestMetadata(path=Path("b.yaml"), test_name="t2", body={"model": "sushi.b"}), + ModelTestMetadata(path=Path("c.yaml"), test_name="t3", body={"model": ""}), + ] + + filtered = filter_tests_by_model_names( + tests, + {'"memory"."sushi"."a"'}, + default_catalog="memory", + dialect="duckdb", + ) + assert [t.test_name for t in filtered] == ["t1"] + + filtered_short = filter_tests_by_model_names( + tests, + {"sushi.a"}, + default_catalog="memory", + dialect="duckdb", + ) + assert [t.test_name for t in filtered_short] == ["t1"] diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 786341d361..dbb09f4336 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -3,7 +3,7 @@ import os import pathlib from unittest import mock -from unittest.mock import PropertyMock, call +from unittest.mock import ANY, PropertyMock, call import pytest import time_machine @@ -256,7 +256,9 @@ def test_pr_plan(github_client, make_controller): assert controller.pr_plan.skip_backfill assert not controller.pr_plan.no_gaps assert not controller._context.apply.called - assert controller._context._run_plan_tests.call_args == call(skip_tests=True) + assert controller._context._run_plan_tests.call_args == call( + skip_tests=True, test_changed_only=False, model_names=ANY + ) assert ( controller._pr_plan_builder._categorizer_config == controller._context.auto_categorize_changes @@ -278,7 +280,9 @@ def test_pr_plan_auto_categorization(github_client, make_controller): assert controller.pr_plan.skip_backfill assert not controller.pr_plan.no_gaps assert not controller._context.apply.called - assert controller._context._run_plan_tests.call_args == call(skip_tests=True) + assert controller._context._run_plan_tests.call_args == call( + skip_tests=True, test_changed_only=False, model_names=ANY + ) assert controller._pr_plan_builder._categorizer_config == custom_categorizer_config assert controller.pr_plan.start == default_start_absolute assert not controller.pr_plan.start_override_per_model @@ -365,7 +369,9 @@ def test_prod_plan(github_client, make_controller): assert not controller.prod_plan.skip_backfill assert controller.prod_plan.no_gaps assert not controller._context.apply.called - assert controller._context._run_plan_tests.call_args == call(skip_tests=True) + assert controller._context._run_plan_tests.call_args == call( + skip_tests=True, test_changed_only=False, model_names=ANY + ) assert ( controller._prod_plan_builder._categorizer_config == controller._context.auto_categorize_changes @@ -387,7 +393,9 @@ def test_prod_plan_auto_categorization(github_client, make_controller): assert not controller.prod_plan.skip_backfill assert controller.prod_plan.no_gaps assert not controller._context.apply.called - assert controller._context._run_plan_tests.call_args == call(skip_tests=True) + assert controller._context._run_plan_tests.call_args == call( + skip_tests=True, test_changed_only=False, model_names=ANY + ) assert controller._prod_plan_builder._categorizer_config == custom_categorizer_config # default PR start should be ignored for prod plans assert controller.prod_plan.start != default_pr_start @@ -404,7 +412,9 @@ def test_prod_plan_with_gaps(github_client, make_controller): assert controller._prod_plan_with_gaps_builder._auto_categorization_enabled assert not controller.prod_plan_with_gaps.no_gaps assert not controller._context.apply.called - assert controller._context._run_plan_tests.call_args == call(skip_tests=True) + assert controller._context._run_plan_tests.call_args == call( + skip_tests=True, test_changed_only=False, model_names=ANY + ) def test_run_tests(github_client, make_controller): From 5a68ce67b66e09c01c6f8383e8c8b0d0247df49c Mon Sep 17 00:00:00 2001 From: BingEdward <1042653432@qq.com> Date: Sat, 5 Sep 2026 10:08:13 +0800 Subject: [PATCH 39/67] fix: write file cache entries atomically to avoid read races (#6011) Signed-off-by: 7487 <1042653432@qq.com> Co-authored-by: Claude Fable 5 --- sqlmesh/utils/cache.py | 23 +++++++++++++++++++++-- tests/utils/test_cache.py | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/sqlmesh/utils/cache.py b/sqlmesh/utils/cache.py index e1ff59a4a7..d7df2ef0d2 100644 --- a/sqlmesh/utils/cache.py +++ b/sqlmesh/utils/cache.py @@ -2,8 +2,10 @@ import gzip import logging +import os import pickle import shutil +import tempfile import typing as t from pathlib import Path @@ -125,8 +127,25 @@ def put(self, name: str, entry_id: str = "", *, value: T) -> None: if not self._path.is_dir(): raise SQLMeshError(f"Cache path '{self._path}' is not a directory.") - with gzip.open(self._cache_entry_path(name, entry_id), "wb", compresslevel=1) as fd: - pickle.dump(value, fd) + # Write to a temporary file and then atomically move it into place. Writing the + # entry in place ("wb" truncates the target file first) means that a concurrent + # reader (e.g. another pytest-xdist worker) could observe a partially written + # entry and fail to unpickle it. + tmp_fd, tmp_name = tempfile.mkstemp(dir=self._path, prefix=f"{self._cache_version}__tmp") + try: + with os.fdopen(tmp_fd, "wb") as raw_fd: + with gzip.open(raw_fd, "wb", compresslevel=1) as fd: + pickle.dump(value, fd) + try: + os.replace(tmp_name, self._cache_entry_path(name, entry_id)) + except OSError as ex: + # Windows os.replace fails if a concurrent reader still has the target file open. + logger.warning("Failed to store a cache entry '%s': %s", name, ex) + finally: + try: + os.unlink(tmp_name) + except OSError: + pass def exists(self, name: str, entry_id: str = "") -> bool: """Returns true if the cache entry with the given name and ID exists, false otherwise. diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py index e6e041e30a..aa11b6c846 100644 --- a/tests/utils/test_cache.py +++ b/tests/utils/test_cache.py @@ -42,6 +42,22 @@ def test_file_cache(tmp_path: Path, mocker: MockerFixture): assert "客户数据" in cache._cache_entry_path("客户数据").name +def test_file_cache_put_is_atomic(tmp_path: Path, mocker: MockerFixture) -> None: + cache: FileCache[_TestEntry] = FileCache(tmp_path) + + old_entry = _TestEntry(value="old") + cache.put("test_name", value=old_entry) + + # Simulate os.replace failing, e.g. on Windows when a concurrent reader still has the + # target file open. The existing entry must never be truncated / partially overwritten. + mocker.patch("sqlmesh.utils.cache.os.replace", side_effect=PermissionError("file in use")) + cache.put("test_name", value=_TestEntry(value="new")) + + assert cache.get("test_name") == old_entry + # The temporary file should have been cleaned up. + assert len(list(tmp_path.glob("*"))) == 1 + + def test_optimized_query_cache(tmp_path: Path, mocker: MockerFixture): model = SqlModel( name="test_model", From b1e36b96e729682b9b18060faaa0c5e02d5bd197 Mon Sep 17 00:00:00 2001 From: Ravi Kakadia <104999921+ravi-p-k-1@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:42:47 -0700 Subject: [PATCH 40/67] ci: add dbt 1.12 to test matrix (#6028) Signed-off-by: Ravi Kakadia Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d9e9552970..fdc649aa1e 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -457,7 +457,7 @@ jobs: strategy: fail-fast: false matrix: - dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11'] + dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11', '1.12'] steps: - uses: actions/checkout@v7 - name: Set up Python diff --git a/Makefile b/Makefile index 94cc8e492d..b9da58757a 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ install-dev-dbt-%: echo "Installing dbt version: $$version"; \ cp pyproject.toml pyproject.toml.backup; \ $(SED_INPLACE) 's/"pydantic>=2.0.0"/"pydantic"/g' pyproject.toml; \ - if [ "$$version" = "1.10.0" ] || [ "$$version" = "1.11.0" ]; then \ + if [ "$$version" = "1.10.0" ] || [ "$$version" = "1.11.0" ] || [ "$$version" = "1.12.0" ]; then \ echo "Applying special handling for dbt $$version"; \ $(SED_INPLACE) -E 's/"(dbt-core)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ $(SED_INPLACE) -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|redshift|trino))[^"]*"/"\1"/g' pyproject.toml; \ From 0c89897ac62ce280232a132fc8047d646704d0e0 Mon Sep 17 00:00:00 2001 From: davem-bis <68955845+davem-bis@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:54:58 +0100 Subject: [PATCH 41/67] fix: Resolve test collisions when capsys used (#6013) Signed-off-by: davem-bis <68955845+davem-bis@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- Makefile | 6 +++--- tests/conftest.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index b9da58757a..cc167212c0 100644 --- a/Makefile +++ b/Makefile @@ -132,13 +132,13 @@ engine-up: engine-clickhouse-up engine-mssql-up engine-mysql-up engine-postgres- engine-down: engine-clickhouse-down engine-mssql-down engine-mysql-down engine-postgres-down engine-spark-down engine-trino-down fast-test: - pytest -n auto -m "fast and not cicdonly" --junitxml=test-results/junit-fast-test.xml && pytest -m "isolated" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" + pytest -n auto -m "fast and not cicdonly and not isolated" --junitxml=test-results/junit-fast-test.xml && pytest -m "isolated and not slow" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" slow-test: - pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" + pytest -n auto -m "(fast or slow) and not cicdonly and not isolated" && pytest -m "isolated" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" cicd-test: - pytest -n auto -m "(fast or slow) and not pyspark" --junitxml=test-results/junit-cicd.xml && pytest -m "pyspark" && pytest -m "isolated" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" + pytest -n auto -m "(fast or slow) and not pyspark and not isolated" --junitxml=test-results/junit-cicd.xml && pytest -m "pyspark" && pytest -m "isolated and not pyspark" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" core-fast-test: pytest -n auto -m "fast and not web and not github and not dbt and not jupyter" diff --git a/tests/conftest.py b/tests/conftest.py index 4d1bb23577..1f7f6d9325 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -212,11 +212,13 @@ def pytest_collection_modifyitems(items, *args, **kwargs): "dialect_isolated", } for item in items: + if "capsys" in item.fixturenames: + # capsys is not threadsafe, so the test must be isolated. + item.add_marker("isolated") for marker in item.iter_markers(): if marker.name in test_type_markers: break else: - # if no test type marker is found, assume fast test item.add_marker("fast") From c0ae2db9bbd1bd819e47dafc2c1a8afec866dcb9 Mon Sep 17 00:00:00 2001 From: davem-bis <68955845+davem-bis@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:29:44 +0100 Subject: [PATCH 42/67] fix: Simplify Java Installation in DevContainer (#6012) Signed-off-by: davem-bis <68955845+davem-bis@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .devcontainer/devcontainer-lock.json | 5 ----- .devcontainer/devcontainer.json | 3 --- 2 files changed, 8 deletions(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index b33ff1a703..43be94a123 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -10,11 +10,6 @@ "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5", "integrity": "sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5" }, - "ghcr.io/devcontainers/features/java:1": { - "version": "1.8.1", - "resolved": "ghcr.io/devcontainers/features/java@sha256:8157bab2d8d71e40b2f3128c162fab763e2b11038fdd33784549290a5d386b48", - "integrity": "sha256:8157bab2d8d71e40b2f3128c162fab763e2b11038fdd33784549290a5d386b48" - }, "ghcr.io/devcontainers/features/python:1": { "version": "1.8.0", "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d0c250eb41..f9022bbbdd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,9 +6,6 @@ "ghcr.io/devcontainers/features/python:1": { "version": "3.12" }, - "ghcr.io/devcontainers/features/java:1": { - "version": "21" - }, "ghcr.io/devcontainers/features/docker-in-docker:4": {} }, "postCreateCommand": "bash .devcontainer/post-create-command.sh", From 847de8c09f3a5cb5d8d846fe5ffcce8e1b7419c2 Mon Sep 17 00:00:00 2001 From: avakili-voleon Date: Wed, 9 Sep 2026 19:27:04 -0400 Subject: [PATCH 43/67] fix(state_sync): skip interval removal when there is nothing to remove (#6027) Signed-off-by: Amir Vakili Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- sqlmesh/core/state_sync/db/interval.py | 3 ++ tests/core/integration/test_restatement.py | 38 ++++++++++++++++++++++ tests/core/state_sync/test_state_sync.py | 34 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index 8ccdc58fa0..227ee27d01 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -108,6 +108,9 @@ def remove_intervals( for snapshot in all_snapshots ] + if not intervals_to_remove: + return + if logger.isEnabledFor(logging.INFO): snapshot_ids = ", ".join(str(s.snapshot_id) for s, _ in intervals_to_remove) logger.info("Removing interval for snapshots: %s", snapshot_ids) diff --git a/tests/core/integration/test_restatement.py b/tests/core/integration/test_restatement.py index 3694efce31..af4f8965a6 100644 --- a/tests/core/integration/test_restatement.py +++ b/tests/core/integration/test_restatement.py @@ -83,6 +83,44 @@ def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): context.apply(plan) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_prod_restatement_with_unbackfilled_dev_version(init_and_plan_context: t.Callable): + """ + Scenario: + Prod is built. A breaking change is planned to `dev` with `--skip-backfill`, + so `dev` holds a different snapshot version with no interval rows. Prod is + then restated. + Outcome: + RestatementStage tries to clear `dev` intervals for that other version, finds + none in `_intervals`, and no-ops instead of crashing on an empty insert. Prod + restatement still applies; the un-backfilled `dev` snapshot still has no + intervals. + """ + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + prod_snapshot_id = context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id + + context.upsert_model( + add_projection_to_model(t.cast(SqlModel, context.get_model("sushi.waiter_revenue_by_day"))) + ) + context.plan("dev", skip_backfill=True, auto_apply=True, no_prompts=True) + + dev_snapshot_id = context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id + assert dev_snapshot_id != prod_snapshot_id + assert not context.state_sync.get_snapshots([dev_snapshot_id])[dev_snapshot_id].intervals + + context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + start="2023-01-07", + end="2023-01-08", + auto_apply=True, + no_prompts=True, + ) + + assert not context.state_sync.get_snapshots([dev_snapshot_id])[dev_snapshot_id].intervals + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_plan_across_environments_snapshot_with_shared_version( init_and_plan_context: t.Callable, diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 348a883fd5..562afc6ac7 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -400,6 +400,40 @@ def test_remove_interval_missing_snapshot( ] +def test_remove_interval_no_matching_intervals( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +) -> None: + snapshot = make_snapshot( + SqlModel( + name="a", + cron="@daily", + query=parse_one("select 1, ds"), + ), + version="a", + ) + state_sync.push_snapshots([snapshot]) + + # The snapshot has never been backfilled, so there are no rows to expand the shared versions from + state_sync.remove_intervals( + [(snapshot, snapshot.inclusive_exclusive("2020-01-15", "2020-01-17"))], + remove_shared_versions=True, + ) + + remove_records_count = state_sync.engine_adapter.fetchone( + "SELECT COUNT(*) FROM sqlmesh._intervals WHERE name = '\"a\"' AND version = 'a' AND is_removed" + )[0] # type: ignore + assert remove_records_count == 0 + + assert not state_sync.get_snapshots([snapshot])[snapshot.snapshot_id].intervals + + +def test_remove_interval_empty_input(state_sync: EngineAdapterStateSync) -> None: + state_sync.remove_intervals([]) + state_sync.remove_intervals([], remove_shared_versions=True) + + assert state_sync.engine_adapter.fetchone("SELECT COUNT(*) FROM sqlmesh._intervals")[0] == 0 # type: ignore + + def test_refresh_snapshot_intervals( state_sync: EngineAdapterStateSync, make_snapshot: t.Callable ) -> None: From 11ac31a0d7ecd20b9ea90e0ee6629e054f4715ba Mon Sep 17 00:00:00 2001 From: Dev M Date: Thu, 10 Sep 2026 06:10:58 +0530 Subject: [PATCH 44/67] fix(test): create ModelTests before concurrent pool workers (#6040) Signed-off-by: devtechedge Signed-off-by: Dev M Co-authored-by: devtechedge Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- sqlmesh/core/test/runner.py | 47 +++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/sqlmesh/core/test/runner.py b/sqlmesh/core/test/runner.py index 284558e1c8..17a4f55d69 100644 --- a/sqlmesh/core/test/runner.py +++ b/sqlmesh/core/test/runner.py @@ -125,25 +125,7 @@ def run_tests( # Ensure workers are not greater than the number of tests num_workers = min(len(model_test_metadata) or 1, default_test_connection.concurrent_tasks) - def _run_single_test( - metadata: ModelTestMetadata, engine_adapter: EngineAdapter - ) -> t.Optional[ModelTextTestResult]: - test = ModelTest.create_test( - body=metadata.body, - test_name=metadata.test_name, - models=models, - engine_adapter=engine_adapter, - dialect=dialect, - path=metadata.path, - default_catalog=default_catalog, - preserve_fixtures=preserve_fixtures, - concurrency=num_workers > 1, - verbosity=verbosity, - ) - - if not test: - return None - + def _run_single_test(test: ModelTest) -> ModelTextTestResult: result = t.cast( ModelTextTestResult, ModelTextTestRunner().run(t.cast(unittest.TestCase, test)), @@ -158,11 +140,30 @@ def _run_single_test( start_time = time.perf_counter() try: + # Build ModelTest instances on the calling thread before workers start. create_test() + # can call to_datetime() / ttl_cache (time.time()), which races with another worker's + # time_machine freeze when execution_time is set under concurrent_tasks > 1. + # NOTE: We can run create_tests in a separate parallel stage for a future optimization. + # We just can't overlap runs/creations. + tests: list[ModelTest] = [] + for metadata, engine_adapter in metadata_to_adapter.items(): + test = ModelTest.create_test( + body=metadata.body, + test_name=metadata.test_name, + models=models, + engine_adapter=engine_adapter, + dialect=dialect, + path=metadata.path, + default_catalog=default_catalog, + preserve_fixtures=preserve_fixtures, + concurrency=num_workers > 1, + verbosity=verbosity, + ) + if test: + tests.append(test) + with ThreadPoolExecutor(max_workers=num_workers) as pool: - futures = [ - pool.submit(_run_single_test, metadata=metadata, engine_adapter=engine_adapter) - for metadata, engine_adapter in metadata_to_adapter.items() - ] + futures = [pool.submit(_run_single_test, test) for test in tests] for future in concurrent.futures.as_completed(futures): test_results.append(future.result()) From 8d0b4de7dbdfabb5a57551bca3c8f21e3930e014 Mon Sep 17 00:00:00 2001 From: Maulik Patel Date: Wed, 9 Sep 2026 19:48:27 -0500 Subject: [PATCH 45/67] Fix: stale IntervalUnit docstring to match enum members (#5957) Signed-off-by: Maulik Patel <21225464+maupatel@users.noreply.github.com> Co-authored-by: Maulik Patel <21225464+maupatel@users.noreply.github.com> Co-authored-by: mday-io Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- sqlmesh/core/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index 3ee97becc9..73e4a31940 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -28,7 +28,8 @@ class IntervalUnit(str, Enum): """IntervalUnit is the inferred granularity of an incremental node. - IntervalUnit can be one of 5 types, YEAR, MONTH, DAY, HOUR, MINUTE. The unit is inferred + IntervalUnit can be one of 7 types: YEAR, MONTH, DAY, HOUR, HALF_HOUR, QUARTER_HOUR, + FIVE_MINUTE. The unit is inferred based on the cron schedule of a node. The minimum time delta between a sample set of dates is used to determine which unit a node's schedule is. From 68f5b95f28a36deaab16195b5f86096118e52e01 Mon Sep 17 00:00:00 2001 From: MJSHANG Date: Fri, 11 Sep 2026 16:25:24 -0700 Subject: [PATCH 46/67] fix(test): support out-of-nanosecond-range timestamps in unit test comparisons (#6009) Signed-off-by: mokashang Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- sqlmesh/core/test/definition.py | 37 ++++++++++++++++++++++++----- tests/core/test_test.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 136de947a3..ee6ce26b71 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -263,12 +263,9 @@ def assert_equal( for col, value in object_sentinel_values.items(): try: # can't use `isinstance()` here - https://stackoverflow.com/a/68743663/1707525 - if type(value) is datetime.date: - expected[col] = pd.to_datetime(expected[col]).dt.date - elif type(value) is datetime.time: - expected[col] = pd.to_datetime(expected[col]).dt.time - elif type(value) is datetime.datetime: - expected[col] = pd.to_datetime(expected[col]).dt.to_pydatetime() + value_type = type(value) + if value_type in (datetime.date, datetime.time, datetime.datetime): + expected[col] = _parse_expected_datetime_column(expected[col], value_type) except Exception as e: from sqlmesh.core.console import get_console @@ -1014,6 +1011,34 @@ def _raise_error(msg: str, path: Path | None = None) -> None: raise TestError(f"Failed to run test:\n{msg}") +def _parse_expected_datetime_column(series: pd.Series, target_type: type) -> pd.Series: + """Convert a series of expected values to python ``date``/``time``/``datetime``. + + Falls back to microsecond resolution when pandas' default nanosecond + parsing overflows. SQL ``TIMESTAMP`` columns can carry values outside + pandas' default ``datetime64[ns]`` range (1677-09-21..2262-04-11), so + unit tests may compare against values like ``0001-01-01`` which are + valid in the database but overflow the default resolution. + """ + import pandas as pd + from pandas.errors import OutOfBoundsDatetime + + try: + parsed = pd.to_datetime(series) + except OutOfBoundsDatetime: + parsed = series.astype("datetime64[us]") + + if target_type is datetime.date: + return parsed.dt.date + if target_type is datetime.time: + return parsed.dt.time + # `Series.dt.to_pydatetime()` returns an `ndarray` in pandas 2.x. Wrap it in a + # Series with ``dtype=object`` so pandas does not coerce the values back to + # ``pd.Timestamp`` (which would reintroduce the nanosecond overflow this + # function exists to avoid). + return pd.Series(parsed.dt.to_pydatetime(), index=parsed.index, dtype="object") + + def _normalize_df_value(value: t.Any) -> t.Any: """Normalize data in a pandas dataframe so ruamel and sqlglot can deal with it.""" import numpy as np diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 6ac20cc2d8..7c67192a14 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2931,6 +2931,48 @@ def test_timestamp_normalization() -> None: ) +def test_out_of_bounds_nanosecond_timestamp_comparison(mocker: MockerFixture) -> None: + # https://github.com/TobikoData/sqlmesh/issues/5929 + # Engines like Redshift may return a TIMESTAMP column as an object-dtype + # series of python `datetime.datetime` instances. Values outside pandas' + # default `datetime64[ns]` range (1677-09-21..2262-04-11) - which SQL + # `TIMESTAMP` fully supports - previously raised `OutOfBoundsDatetime` + # while parsing the expected values, producing a "Failed to convert + # expected value into `datetime`" warning and a false mismatch on values + # whose repr survives str-coercion (the values below happen to compare + # equal via `str()`, so the mismatch was silent). + test = _create_test( + body=load_yaml( + """ +test_foo: + model: sushi.foo + outputs: + query: + - ts_col: "0001-01-01 00:00:00" + - ts_col: "9999-12-31 23:59:59" + """ + ), + test_name="test_foo", + model=_create_model("SELECT ts_col FROM raw"), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ) + actual = pd.DataFrame( + { + "ts_col": pd.Series( + [datetime.datetime(1, 1, 1), datetime.datetime(9999, 12, 31, 23, 59, 59)], + dtype=object, + ) + } + ) + # Use T separator so a string-only comparison (broken path) would mismatch + # against str(datetime.datetime(1, 1, 1)) == "0001-01-01 00:00:00". + expected = pd.DataFrame({"ts_col": ["0001-01-01T00:00:00", "9999-12-31T23:59:59"]}) + log_warning = mocker.spy(get_console(), "log_warning") + test.assert_equal(expected=expected, actual=actual, sort=False) + for call_args in log_warning.call_args_list: + assert "Failed to convert expected value" not in call_args.args[0] + + @use_terminal_console def test_disable_test_logging_if_no_tests_found(mocker: MockerFixture, tmp_path: Path) -> None: init_example_project(tmp_path, engine_type="duckdb") From 7e4f83e463af89bb108427d052ffbb3b4c56334c Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Sat, 12 Sep 2026 01:16:22 +0100 Subject: [PATCH 47/67] ci: turn on and fix `ui-style` and `test-vscode` stages (#6054) Signed-off-by: Adegbite Ayoade --- .devcontainer/devcontainer.json | 5 +-- .github/workflows/pr.yaml | 41 ++++++++++++++++--- vscode/extension/tests/fixtures.ts | 4 +- .../extension/tests/lineage_settings.spec.ts | 2 +- .../library/components/graph/ModelNode.tsx | 11 ++++- .../src/library/components/graph/help.ts | 5 ++- 6 files changed, 52 insertions(+), 16 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f9022bbbdd..6e794c42a7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,10 +11,7 @@ "postCreateCommand": "bash .devcontainer/post-create-command.sh", "customizations": { "vscode": { - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance" - ] + "extensions": ["ms-python.python", "ms-python.vscode-pylance"] } }, "remoteUser": "vscode" diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fdc649aa1e..0758d7f7ff 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -16,6 +16,7 @@ jobs: outputs: python: ${{ steps.filter.outputs.python }} client: ${{ steps.filter.outputs.client }} + vscode: ${{ steps.filter.outputs.vscode }} ci: ${{ steps.filter.outputs.ci }} steps: - uses: actions/checkout@v7 @@ -34,6 +35,8 @@ jobs: - 'pyproject.toml' client: - 'web/client/**' + vscode: + - 'vscode/**' ci: - '.github/**' - 'Makefile' @@ -188,9 +191,10 @@ jobs: ui-style: needs: [changes] - if: false - # needs.changes.outputs.client == 'true' || needs.changes.outputs.ci == - # 'true' || github.ref == 'refs/heads/main' + if: + needs.changes.outputs.client == 'true' || needs.changes.outputs.vscode == + 'true' || needs.changes.outputs.ci == 'true' || github.ref == + 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -252,7 +256,17 @@ jobs: fail-fast: false matrix: engine: - [duckdb, postgres, mysql, mssql, trino, spark, clickhouse, risingwave, starrocks] + [ + duckdb, + postgres, + mysql, + mssql, + trino, + spark, + clickhouse, + risingwave, + starrocks, + ] env: PYTEST_XDIST_AUTO_NUM_WORKERS: 2 SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: '1' @@ -393,10 +407,13 @@ jobs: retention-days: 7 test-vscode: + needs: changes + if: + needs.changes.outputs.vscode == 'true' || needs.changes.outputs.ci == + 'true' || github.ref == 'refs/heads/main' env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 runs-on: ubuntu-latest - if: false steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 @@ -457,7 +474,19 @@ jobs: strategy: fail-fast: false matrix: - dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11', '1.12'] + dbt-version: + [ + '1.3', + '1.4', + '1.5', + '1.6', + '1.7', + '1.8', + '1.9', + '1.10', + '1.11', + '1.12', + ] steps: - uses: actions/checkout@v7 - name: Set up Python diff --git a/vscode/extension/tests/fixtures.ts b/vscode/extension/tests/fixtures.ts index 7a10d811fa..6ce92c3266 100644 --- a/vscode/extension/tests/fixtures.ts +++ b/vscode/extension/tests/fixtures.ts @@ -60,5 +60,5 @@ export const test = base.extend< ], }) -// Export expect and Page from Playwright for convenience -export { expect, Page } from '@playwright/test' +// Export expect and commonly used Playwright types for convenience +export { expect, FrameLocator, Page } from '@playwright/test' diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index c2bd4a1e17..d98ab53dff 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from './fixtures' -import type { FrameLocator, Page } from '@playwright/test' +import type { FrameLocator, Page } from './fixtures' import fs from 'fs-extra' import { openLineageView, diff --git a/web/client/src/library/components/graph/ModelNode.tsx b/web/client/src/library/components/graph/ModelNode.tsx index 0fe2f27d51..bb01ffe05b 100644 --- a/web/client/src/library/components/graph/ModelNode.tsx +++ b/web/client/src/library/components/graph/ModelNode.tsx @@ -198,7 +198,11 @@ export default function ModelNode({ style={{ width: '100%', ...(tagColor != null - ? { borderColor: tagColor, backgroundColor: tagColor, color: tagColor } + ? { + borderColor: tagColor, + backgroundColor: tagColor, + color: tagColor, + } : {}), }} > @@ -246,7 +250,10 @@ export default function ModelNode({ /> {showColumns && ( -
+
t.trim()).filter(Boolean) + ? tagsStr + .split(',') + .map(t => t.trim()) + .filter(Boolean) : undefined const node = createGraphNode(modelName, { label: model?.displayName ?? modelName, From 80731defe62a5a3f5978af152eb48b251b3e64dc Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Sat, 12 Sep 2026 02:34:11 +0100 Subject: [PATCH 48/67] fix(web): resolve model paths against their own project root (#6052) Signed-off-by: Adegbite Ayoade Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- tests/web/test_models.py | 25 +++++++++++++++++++++++++ web/server/api/endpoints/models.py | 21 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/web/test_models.py diff --git a/tests/web/test_models.py b/tests/web/test_models.py new file mode 100644 index 0000000000..5ff495ccc4 --- /dev/null +++ b/tests/web/test_models.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from sqlmesh.core.context import Context +from web.server.api.endpoints.models import get_models + +pytestmark = pytest.mark.web + + +def test_get_models_multi_repo() -> None: + """Models of every project are serialized, not just those of the first one. + + `context.path` is the first configured project, so it is not an ancestor of the models + defined in any of the others. + """ + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") + + paths_by_name = {model.name: model.path for model in get_models(context)} + + # Each model is reported relative to the project that defines it. + assert paths_by_name["bronze.a"] == "models/a.sql" + assert paths_by_name["silver.c"] == "models/c.sql" diff --git a/web/server/api/endpoints/models.py b/web/server/api/endpoints/models.py index 21a7b93eb0..d671b35908 100644 --- a/web/server/api/endpoints/models.py +++ b/web/server/api/endpoints/models.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing as t +from pathlib import Path from fastapi import APIRouter, Depends, HTTPException from sqlglot import exp @@ -12,6 +13,7 @@ from sqlmesh.utils.date import now, to_datetime from web.server import models from web.server.settings import get_loaded_context +from web.server.utils import is_relative_to router = APIRouter() @@ -125,7 +127,7 @@ def serialize_model(context: Context, model: Model, render_query: bool = False) return models.Model( name=model.name, fqn=model.fqn, - path=str(path.absolute().relative_to(context.path).as_posix()) if path else None, + path=_path_relative_to_project(context, path) if path else None, full_path=str(path.absolute().as_posix()) if path else None, dialect=dialect, columns=columns, @@ -138,6 +140,23 @@ def serialize_model(context: Context, model: Model, render_query: bool = False) ) +def _path_relative_to_project(context: Context, path: Path) -> str: + """Returns a model's path relative to the project root that defines it. + + `context.path` is only the first of the configured projects, so it isn't necessarily an + ancestor of every model when the context is loaded with more than one of them. + """ + absolute_path = path.absolute() + project_roots = [root for root in context.configs if is_relative_to(absolute_path, root)] + if not project_roots: + # The model lives outside of every configured project, so there is nothing to be + # relative to. + return absolute_path.as_posix() + # The deepest root wins so that nested projects report the closest one. + closest_root = max(project_roots, key=lambda root: len(root.parts)) + return absolute_path.relative_to(closest_root).as_posix() + + def _get_model_type(model: Model) -> str: if model.is_sql: return models.ModelType.SQL From ee57c615ff0dacecbfd69db57757b93fa42e7bca Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:24:13 -0600 Subject: [PATCH 49/67] fix(ci): minio image switch to chainguard (#6055) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .../integration/docker/_common-hive.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/core/engine_adapter/integration/docker/_common-hive.yaml b/tests/core/engine_adapter/integration/docker/_common-hive.yaml index 59d3cd80a0..3c9c716b34 100644 --- a/tests/core/engine_adapter/integration/docker/_common-hive.yaml +++ b/tests/core/engine_adapter/integration/docker/_common-hive.yaml @@ -12,22 +12,22 @@ services: # S3-style object storage minio: - image: 'minio/minio:RELEASE.2022-05-26T05-48-41Z' + image: 'cgr.dev/chainguard/minio:latest@sha256:039800e64ec7247d2fde7cff3697e964f6fe20b6d7d2c46aa7d82cc63355d512' ports: - '9000:9000' - '9001:9001' environment: - MINIO_ACCESS_KEY: minio - MINIO_SECRET_KEY: minio123 + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: minio123 command: server /data --console-address ":9001" # Set up minio with default buckets / paths mc-job: - image: 'minio/mc:RELEASE.2022-05-09T04-08-26Z' + image: 'cgr.dev/chainguard/minio-client:latest-dev@sha256:fb635b967f5f32424391150dac4985cc1aea78fb0a1ce031c01952cf661a13ec' entrypoint: | /bin/bash -c " sleep 5; - /usr/bin/mc config --quiet host add myminio http://minio:9000 minio minio123; + /usr/bin/mc alias --quiet set myminio http://minio:9000 minio minio123; /usr/bin/mc mb --quiet myminio/trino/datalake; /usr/bin/mc mb --quiet myminio/trino/datalake_iceberg; /usr/bin/mc mb --quiet myminio/trino/datalake_delta; @@ -39,4 +39,4 @@ services: /usr/bin/mc mb --quiet myminio/nessie/warehouse; " depends_on: - - minio \ No newline at end of file + - minio From 2c30f831c95d08c48249dc8a30fe1b69ae7deb49 Mon Sep 17 00:00:00 2001 From: sineline <9558720+sineline@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:45:26 +0200 Subject: [PATCH 50/67] Fix/snowflake iceberg clone dialect error (#5722) Signed-off-by: Guillem G --- sqlmesh/core/engine_adapter/base.py | 14 ++++++- sqlmesh/core/engine_adapter/bigquery.py | 1 + sqlmesh/core/engine_adapter/clickhouse.py | 1 + sqlmesh/core/engine_adapter/databricks.py | 4 ++ sqlmesh/core/engine_adapter/fabric.py | 4 +- sqlmesh/core/engine_adapter/snowflake.py | 32 ++++++++++++++++ sqlmesh/core/snapshot/evaluator.py | 3 +- tests/core/engine_adapter/test_snowflake.py | 42 +++++++++++++++++++++ tests/core/test_snapshot_evaluator.py | 6 ++- 9 files changed, 102 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index bd435db76f..930cdf7cd4 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1094,6 +1094,8 @@ def clone_table( replace: bool = False, exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, + table_format: t.Optional[str] = None, + table_kind: t.Optional[str] = None, **kwargs: t.Any, ) -> None: """Creates a table with the target name by cloning the source table. @@ -1103,6 +1105,10 @@ def clone_table( source_table_name: The name of the source table that should be cloned. replace: Whether or not to replace an existing table. exists: Indicates whether to include the IF NOT EXISTS check. + clone_kwargs: Additional arguments for the CLONE clause. + table_format: The table format of the source table, if any. Engines that require + format-specific DDL to clone a table use it to derive `table_kind`. + table_kind: The kind of table to create. Defaults to `TABLE`. """ if not self.SUPPORTS_CLONING: raise NotImplementedError(f"Engine does not support cloning: {type(self)}") @@ -1111,7 +1117,7 @@ def clone_table( self.execute( exp.Create( this=exp.to_table(target_table_name), - kind="TABLE", + kind=table_kind or "TABLE", replace=replace, exists=exists, clone=exp.Clone( @@ -1214,9 +1220,15 @@ def get_alter_operations( def alter_table( self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], + table_format: t.Optional[str] = None, ) -> None: """ Performs the alter statements to change the current table into the structure of the target table. + + Args: + alter_expressions: The alter operations to apply. + table_format: The table format of the target table, if any. Engines that require + format-specific DDL to alter a table use it to adjust the generated statements. """ with self.transaction(): for alter_expression in [ diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index d136445114..7917aaa149 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -405,6 +405,7 @@ def create_mapping_schema( def alter_table( self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], + table_format: t.Optional[str] = None, ) -> None: """ Performs the alter statements to change the current table into the structure of the target table, diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 92e3c33e6f..16ae5b5b51 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -699,6 +699,7 @@ def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expr]) -> N def alter_table( self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], + table_format: t.Optional[str] = None, ) -> None: """ Performs the alter statements to change the current table into the structure of the target table. diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 098825bc2d..8d47cdfd61 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -386,6 +386,8 @@ def clone_table( replace: bool = False, exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, + table_format: t.Optional[str] = None, + table_kind: t.Optional[str] = None, **kwargs: t.Any, ) -> None: clone_kwargs = clone_kwargs or {} @@ -395,6 +397,8 @@ def clone_table( source_table_name, replace=replace, clone_kwargs=clone_kwargs, + table_format=table_format, + table_kind=table_kind, **kwargs, ) diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py index 7b2f1acd73..427a04d087 100644 --- a/sqlmesh/core/engine_adapter/fabric.py +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -225,7 +225,9 @@ def set_current_catalog(self, catalog_name: t.Optional[str]) -> None: self._target_catalog = target_catalog def alter_table( - self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]] + self, + alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], + table_format: t.Optional[str] = None, ) -> None: """ Applies alter expressions to a table. Fabric has limited support for ALTER TABLE, diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index d589b5d15b..37ca6eee22 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -24,6 +24,7 @@ SourceQuery, set_catalog, ) +from sqlmesh.core.schema_diff import TableAlterOperation from sqlmesh.utils import optional_import, get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.pandas import columns_to_types_from_dtypes @@ -667,6 +668,8 @@ def clone_table( replace: bool = False, exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, + table_format: t.Optional[str] = None, + table_kind: t.Optional[str] = None, **kwargs: t.Any, ) -> None: # The Snowflake adapter should use the transient property to clone transient tables @@ -675,14 +678,43 @@ def clone_table( if isinstance(table_type, exp.TransientProperty): kwargs["properties"] = exp.Properties(expressions=[table_type]) + # Snowflake rejects `CREATE TABLE ... CLONE` for Iceberg tables, it requires + # `CREATE ICEBERG TABLE ... CLONE` instead + if table_format and not table_kind: + table_kind = f"{table_format.upper()} TABLE" + super().clone_table( target_table_name, source_table_name, replace=replace, clone_kwargs=clone_kwargs, + table_kind=table_kind, **kwargs, ) + def alter_table( + self, + alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], + table_format: t.Optional[str] = None, + ) -> None: + # Snowflake rejects `ALTER TABLE` for Iceberg tables, it requires + # `ALTER ICEBERG TABLE` instead + if table_format: + table_kind = f"{table_format.upper()} TABLE" + resolved_expressions = [] + for alter_expression in alter_expressions: + resolved_expression = ( + alter_expression.expression + if isinstance(alter_expression, TableAlterOperation) + else alter_expression.copy() + ) + resolved_expression.set("kind", table_kind) + resolved_expressions.append(resolved_expression) + + super().alter_table(resolved_expressions) + else: + super().alter_table(alter_expressions) + @t.overload def _columns_to_types( self, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 11b3fd1f33..ad935310f4 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1104,6 +1104,7 @@ def _clone_snapshot_in_dev( target_table_name, snapshot.table_name(), rendered_physical_properties=rendered_physical_properties, + table_format=snapshot.model.table_format, ) self._migrate_target_table( target_table_name=target_table_name, @@ -2161,7 +2162,7 @@ def migrate( _check_additive_schema_change( snapshot, alter_operations, kwargs["allow_additive_snapshots"] ) - self.adapter.alter_table(alter_operations) + self.adapter.alter_table(alter_operations, table_format=snapshot.model.table_format) # Apply grants after schema migration deployability_index = kwargs.get("deployability_index") diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 085c51098b..05f0133ec8 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -1007,6 +1007,48 @@ def test_table_format_iceberg(snowflake_mocked_engine_adapter: SnowflakeEngineAd ] +def test_clone_table_iceberg(mocker: MockerFixture, make_mocked_engine_adapter: t.Callable): + mocker.patch("sqlmesh.core.engine_adapter.snowflake.SnowflakeEngineAdapter.set_current_catalog") + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter, default_catalog="test_catalog") + + # Snowflake rejects `CREATE TABLE ... CLONE` for Iceberg tables + adapter.clone_table("target_table", "source_table", table_format="iceberg") + adapter.cursor.execute.assert_called_once_with( + 'CREATE ICEBERG TABLE IF NOT EXISTS "target_table" CLONE "source_table"' + ) + + # Engines that don't need format-specific DDL are unaffected + adapter = make_mocked_engine_adapter(EngineAdapter, default_catalog="test_catalog") + adapter.SUPPORTS_CLONING = True + adapter.clone_table("target_table", "source_table", table_format="iceberg") + adapter.cursor.execute.assert_called_once_with( + 'CREATE TABLE IF NOT EXISTS "target_table" CLONE "source_table"' + ) + + +def test_alter_table_iceberg(mocker: MockerFixture, make_mocked_engine_adapter: t.Callable): + mocker.patch("sqlmesh.core.engine_adapter.snowflake.SnowflakeEngineAdapter.set_current_catalog") + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter, default_catalog="test_catalog") + + current_table = {"a": "INT"} + target_table = {"a": "INT", "b": "INT"} + adapter.columns = lambda table_name, **kwargs: { + k: exp.DataType.build(v) + for k, v in (current_table if table_name == "test_table" else target_table).items() + } + + alter_operations = adapter.get_alter_operations("test_table", "target_table") + + # Snowflake rejects `ALTER TABLE` for Iceberg tables + adapter.alter_table(alter_operations, table_format="iceberg") + assert to_sql_calls(adapter) == ['ALTER ICEBERG TABLE "test_table" ADD "b" INT'] + + # Without a table format the regular `ALTER TABLE` is used + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter, default_catalog="test_catalog") + adapter.alter_table(alter_operations) + assert to_sql_calls(adapter) == ['ALTER TABLE "test_table" ADD "b" INT'] + + def test_create_view_with_schema_and_grants( snowflake_mocked_engine_adapter: SnowflakeEngineAdapter, ): diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 27bcbe05ae..3950e0d53f 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1943,6 +1943,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", rendered_physical_properties={}, + table_format=None, ) adapter_mock.get_alter_operations.assert_called_once_with( @@ -1952,7 +1953,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) ignore_additive=False, ) - adapter_mock.alter_table.assert_called_once_with([]) + adapter_mock.alter_table.assert_called_once_with([], table_format=None) adapter_mock.drop_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp" @@ -1992,6 +1993,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", rendered_physical_properties={}, + table_format=None, ) adapter_mock.get_alter_operations.assert_called_once_with( @@ -2001,7 +2003,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m ignore_additive=False, ) - adapter_mock.alter_table.assert_called_once_with([]) + adapter_mock.alter_table.assert_called_once_with([], table_format=None) adapter_mock.drop_table.assert_has_calls( [ From d3e5f0b9c703faee42749064760ee9ca3297c96d Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:44:21 -0600 Subject: [PATCH 51/67] chore(lsp): limiting prettier formatting scope (#6056) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .prettierignore | 1 + package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.prettierignore b/.prettierignore index 78cf56de58..2c652486dc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -30,6 +30,7 @@ docs examples posts .circleci +.github/ README.md mkdocs.yml .readthedocs.yaml diff --git a/package.json b/package.json index 65fda17618..98734ffd3c 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ }, "scripts": { "ci": "pnpm run lint && pnpm run -r ci", - "fmt": "prettier --write .", - "fmt:check": "prettier --check .", + "fmt": "prettier --write vscode web/client web/common", + "fmt:check": "prettier --check vscode web/client web/common", "lint": "pnpm run fmt:check && pnpm run -r lint", "lint:fix": "pnpm run fmt && pnpm run -r lint:fix" }, From 15c60306fd44af827c33dec169fa02d7e9af7b19 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:12:26 -0600 Subject: [PATCH 52/67] feature(ci): filter dbt tests to same as python tests (#6005) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0758d7f7ff..0c26d39a2f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -470,6 +470,10 @@ jobs: path: vscode/extension/playwright-report/ retention-days: 30 test-dbt-versions: + needs: changes + if: + needs.changes.outputs.python == 'true' || needs.changes.outputs.ci == + 'true' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest strategy: fail-fast: false From 17fb177cd82c6803165dcbcc9613aac7cf3a6b13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:18:20 -0600 Subject: [PATCH 53/67] Chore(deps-dev): Bump yaml from 2.8.0 to 2.9.0 (#5186) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- pnpm-lock.yaml | 1217 +++++++++++++++++++++------------ vscode/extension/package.json | 2 +- 2 files changed, 778 insertions(+), 441 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aeacb362d0..31b664c053 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,31 +71,31 @@ importers: version: 0.0.10 '@vscode/test-electron': specifier: ^2.5.2 - version: 2.5.2 + version: 2.5.2(supports-color@8.1.1) '@vscode/vsce': specifier: ^3.6.0 - version: 3.6.0 + version: 3.6.0(supports-color@8.1.1) esbuild: specifier: ^0.25.8 version: 0.25.8 eslint: specifier: ^9.31.0 - version: 9.31.0(jiti@2.4.2) + version: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) ts-loader: specifier: ^9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.8)) + version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)) typescript: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: specifier: ^8.38.0 - version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@8.1.1))(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) yaml: - specifier: ^2.8.0 - version: 2.8.0 + specifier: ^2.9.0 + version: 2.9.0 vscode/react: dependencies: @@ -113,7 +113,7 @@ importers: version: 4.1.11 '@tailwindcss/vite': specifier: ^4.1.11 - version: 4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@18.3.1) @@ -122,13 +122,13 @@ importers: version: 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': specifier: ^1.131.26 - version: 1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.129.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) + version: 1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.171.27)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) '@tanstack/react-virtual': specifier: ^3.13.12 version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.129.8 - version: 1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) + version: 1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@9.4.0)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -140,7 +140,7 @@ importers: version: 0.8.2 orval: specifier: ^7.10.0 - version: 7.10.0(openapi-types@12.1.3) + version: 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) react: specifier: ^18.3.1 version: 18.3.1 @@ -162,22 +162,22 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: ^4.0.1 - version: 4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + version: 4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) '@storybook/addon-a11y': specifier: ^9.0.18 - version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) '@storybook/addon-docs': specifier: ^9.0.18 - version: 9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + version: 9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) '@storybook/addon-onboarding': specifier: ^9.0.18 - version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) '@storybook/addon-vitest': specifier: ^9.0.18 - version: 9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4) + version: 9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(vitest@3.2.4) '@storybook/react-vite': specifier: ^9.0.18 - version: 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -192,31 +192,31 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.7.0(supports-color@9.4.0)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/browser': specifier: 3.2.3 - version: 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + version: 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: 3.2.3 - version: 3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4) + version: 3.2.3(@vitest/browser@3.2.3)(supports-color@8.1.1)(vitest@3.2.4) jsdom: specifier: ^26.1.0 - version: 26.1.0 + version: 26.1.0(supports-color@9.4.0) playwright: specifier: ^1.54.1 version: 1.54.1 storybook: specifier: ^9.0.18 - version: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + version: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -309,7 +309,7 @@ importers: version: 18.3.1(react@18.3.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@18.3.23)(react@18.3.1) + version: 10.1.0(@types/react@18.3.23)(react@18.3.1)(supports-color@9.4.0) react-router: specifier: ^7.7.0 version: 7.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -355,7 +355,7 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': specifier: ^3.11.0 - version: 3.11.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 3.11.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -364,13 +364,13 @@ importers: version: 10.4.21(postcss@8.5.6) eslint: specifier: ^9.31.0 - version: 9.31.0(jiti@2.4.2) + version: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) jsdom: specifier: ^26.1.0 - version: 26.1.0 + version: 26.1.0(supports-color@9.4.0) orval: specifier: ^7.10.0 - version: 7.10.0(openapi-types@12.1.3) + version: 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) postcss: specifier: ^8.5.6 version: 8.5.6 @@ -382,16 +382,16 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.38.0 - version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.13.2 @@ -410,10 +410,10 @@ importers: version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-docs': specifier: 9.1.5 - version: 9.1.5(@types/react@18.3.23)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))) + version: 9.1.5(@types/react@18.3.23)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))) '@storybook/react-vite': specifier: 9.1.5 - version: 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@tailwindcss/typography': specifier: 0.5.16 version: 0.5.16(tailwindcss@3.4.17) @@ -446,10 +446,10 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': specifier: 4.7.0 - version: 4.7.0(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.7.0(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/browser': specifier: 3.2.4 - version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@xyflow/react': specifier: 12.8.4 version: 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -479,13 +479,13 @@ importers: version: 4.3.1 eslint: specifier: 9.31.0 - version: 9.31.0(jiti@2.4.2) + version: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) eslint-plugin-react-hooks: specifier: 5.2.0 - version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) + version: 5.2.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0)) eslint-plugin-storybook: specifier: 9.1.5 - version: 9.1.5(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + version: 9.1.5(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(supports-color@9.4.0)(typescript@5.8.3) fuse.js: specifier: 7.1.0 version: 7.1.0 @@ -509,7 +509,7 @@ importers: version: 18.3.1(react@18.3.1) storybook: specifier: 9.1.5 - version: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) syncpack: specifier: 13.0.4 version: 13.0.4(typescript@5.8.3) @@ -527,19 +527,19 @@ importers: version: 5.8.3 typescript-eslint: specifier: 8.38.0 - version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) vite: specifier: 6.3.5 - version: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(@types/node@20.11.25)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.5.4(@types/node@20.11.25)(rollup@4.45.1)(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) vite-plugin-static-copy: specifier: 3.1.1 - version: 3.1.1(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 3.1.1(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) packages: @@ -1197,9 +1197,6 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - '@jridgewell/trace-mapping@0.3.30': - resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -1807,56 +1804,67 @@ packages: resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.45.1': resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.45.1': resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.45.1': resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.45.1': resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.45.1': resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.45.1': resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.45.1': resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.45.1': resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.45.1': resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.45.1': resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} @@ -2184,24 +2192,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.13.2': resolution: {integrity: sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.13.2': resolution: {integrity: sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.13.2': resolution: {integrity: sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.13.2': resolution: {integrity: sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==} @@ -2282,24 +2294,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.11': resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.11': resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.11': resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.11': resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} @@ -2346,6 +2362,10 @@ packages: resolution: {integrity: sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==} engines: {node: '>=12'} + '@tanstack/history@1.162.1': + resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} + engines: {node: '>=20.19'} + '@tanstack/query-core@5.83.0': resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} @@ -2392,6 +2412,10 @@ packages: resolution: {integrity: sha512-Izqf5q8TzJv0DJURynitJioPJT3dPAefrzHi2wlY/Q5+7nEG41SkjYMotTX2Q9i/Pjl91lW8gERCHpksszRdRw==} engines: {node: '>=12'} + '@tanstack/router-core@1.171.27': + resolution: {integrity: sha512-wDwSLvoLwIaNcnx9UNcN9Mb7Y8QwCYq1U1RQZwyN186gnkIoIYI2SOxy8VqH1vFigbkHkk4FmwMAQlghPgDK2g==} + engines: {node: '>=20.19'} + '@tanstack/router-devtools-core@1.131.26': resolution: {integrity: sha512-TGHmRDQpYphuRbDH+jJp418vQuIydzITaUx7MiPk5U1ZZ+2O/GxcF/ycXmyYR0IHTpSky35I83X3bKTiv+thyw==} engines: {node: '>=12'} @@ -2809,6 +2833,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-react-swc@3.11.0': resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} @@ -2922,6 +2947,11 @@ packages: '@volar/typescript@2.4.23': resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true '@vscode/python-extension@1.0.5': resolution: {integrity: sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==} @@ -3542,6 +3572,9 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -3932,6 +3965,7 @@ packages: eslint@9.31.0: resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -4189,21 +4223,23 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -4756,24 +4792,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -5485,6 +5525,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -5831,10 +5872,20 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.6.4: + resolution: {integrity: sha512-R0f1U9hmn38+dFMz6b6ab8lwucmw4AtiY7St+JPWudy1dm+Bs3g884nyrsH9Cy6rKpZKLYayXuMda9GZ/fl8JQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + seroval@1.6.4: + resolution: {integrity: sha512-LErWMNS2RRFdu2RMA5u/PA59/IWs0XsikyEXGQ2/36iEWFrdG0ABmg17E17cikrv76891kOAMq3TkTFXpwAHXw==} + engines: {node: '>=10'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -6180,6 +6231,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terminal-link@4.0.0: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} @@ -6329,6 +6381,7 @@ packages: tsconfck@2.1.2: resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} engines: {node: ^14.13.1 || ^16 || >=18} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^4.3.5 || ^5.0.0 @@ -6532,6 +6585,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -6714,6 +6768,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -6823,8 +6878,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -6955,34 +7010,34 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-auth@1.10.0': + '@azure/core-auth@1.10.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.0 + '@azure/core-util': 1.13.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-client@1.10.0': + '@azure/core-client@1.10.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 - '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-auth': 1.10.0(supports-color@8.1.1) + '@azure/core-rest-pipeline': 1.22.0(supports-color@8.1.1) '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 - '@azure/logger': 1.3.0 + '@azure/core-util': 1.13.0(supports-color@8.1.1) + '@azure/logger': 1.3.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-rest-pipeline@1.22.0': + '@azure/core-rest-pipeline@1.22.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 + '@azure/core-auth': 1.10.0(supports-color@8.1.1) '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 - '@azure/logger': 1.3.0 - '@typespec/ts-http-runtime': 0.3.0 + '@azure/core-util': 1.13.0(supports-color@8.1.1) + '@azure/logger': 1.3.0(supports-color@8.1.1) + '@typespec/ts-http-runtime': 0.3.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -6991,23 +7046,23 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-util@1.13.0': + '@azure/core-util@1.13.0(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.0 + '@typespec/ts-http-runtime': 0.3.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/identity@4.10.2': + '@azure/identity@4.10.2(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.0 - '@azure/core-client': 1.10.0 - '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-auth': 1.10.0(supports-color@8.1.1) + '@azure/core-client': 1.10.0(supports-color@8.1.1) + '@azure/core-rest-pipeline': 1.22.0(supports-color@8.1.1) '@azure/core-tracing': 1.3.0 - '@azure/core-util': 1.13.0 - '@azure/logger': 1.3.0 + '@azure/core-util': 1.13.0(supports-color@8.1.1) + '@azure/logger': 1.3.0(supports-color@8.1.1) '@azure/msal-browser': 4.15.0 '@azure/msal-node': 3.6.3 open: 10.2.0 @@ -7015,9 +7070,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/logger@1.3.0': + '@azure/logger@1.3.0(supports-color@8.1.1)': dependencies: - '@typespec/ts-http-runtime': 0.3.0 + '@typespec/ts-http-runtime': 0.3.0(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -7042,20 +7097,20 @@ snapshots: '@babel/compat-data@7.28.0': {} - '@babel/core@7.28.0': + '@babel/core@7.28.0(supports-color@9.4.0)': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) '@babel/helpers': 7.27.6 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7082,41 +7137,41 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.27.1(supports-color@9.4.0) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@9.4.0) + '@babel/traverse': 7.28.0(supports-color@9.4.0) semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.27.1': + '@babel/helper-member-expression-to-functions@7.27.1(supports-color@9.4.0)': dependencies: - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.27.1': + '@babel/helper-module-imports@7.27.1(supports-color@9.4.0)': dependencies: - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/helper-module-imports': 7.27.1(supports-color@9.4.0) '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -7126,18 +7181,18 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/helper-member-expression-to-functions': 7.27.1(supports-color@9.4.0) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + '@babel/helper-skip-transparent-expression-wrappers@7.27.1(supports-color@9.4.0)': dependencies: - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -7157,53 +7212,53 @@ snapshots: dependencies: '@babel/types': 7.28.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@9.4.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -7215,7 +7270,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/types': 7.28.1 - '@babel/traverse@7.28.0': + '@babel/traverse@7.28.0(supports-color@9.4.0)': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.0 @@ -7223,7 +7278,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -7236,13 +7291,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@chromatic-com/storybook@4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.1.0 - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -7470,17 +7525,30 @@ snapshots: '@esbuild/win32-x64@0.25.8': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))': + dependencies: + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))': dependencies: - eslint: 9.31.0(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.21.0': + '@eslint/config-array@0.21.0(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-array@0.21.0(supports-color@9.4.0)': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1(supports-color@9.4.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7491,10 +7559,24 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.1(supports-color@8.1.1)': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.1(supports-color@9.4.0)': + dependencies: + ajv: 6.12.6 + debug: 4.4.1(supports-color@9.4.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -7615,21 +7697,21 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: glob: 10.4.5 magic-string: 0.30.17 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) optionalDependencies: typescript: 5.8.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: glob: 10.4.5 magic-string: 0.30.17 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) optionalDependencies: typescript: 5.8.3 @@ -7641,7 +7723,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -7659,11 +7741,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping@0.3.30': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -7760,23 +7837,23 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@orval/angular@7.10.0(openapi-types@12.1.3)': + '@orval/angular@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/axios@7.10.0(openapi-types@12.1.3)': + '@orval/axios@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/core@7.10.0(openapi-types@12.1.3)': + '@orval/core@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) '@ibm-cloud/openapi-ruleset': 1.31.1 @@ -7784,7 +7861,7 @@ snapshots: ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@9.4.0) esbuild: 0.25.8 esutils: 2.0.3 fs-extra: 11.3.0 @@ -7801,64 +7878,64 @@ snapshots: - openapi-types - supports-color - '@orval/fetch@7.10.0(openapi-types@12.1.3)': + '@orval/fetch@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/hono@7.10.0(openapi-types@12.1.3)': + '@orval/hono@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/zod': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mcp@7.10.0(openapi-types@12.1.3)': + '@orval/mcp@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/zod': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mock@7.10.0(openapi-types@12.1.3)': + '@orval/mock@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) openapi3-ts: 4.4.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/query@7.10.0(openapi-types@12.1.3)': + '@orval/query@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/fetch': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) lodash.omitby: 4.6.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/swr@7.10.0(openapi-types@12.1.3)': + '@orval/swr@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/fetch': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/zod@7.10.0(openapi-types@12.1.3)': + '@orval/zod@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding @@ -8463,18 +8540,18 @@ snapshots: dependencies: '@secretlint/types': 10.2.1 - '@secretlint/config-loader@10.2.1': + '@secretlint/config-loader@10.2.1(supports-color@8.1.1)': dependencies: '@secretlint/profiler': 10.2.1 '@secretlint/resolver': 10.2.1 '@secretlint/types': 10.2.1 ajv: 8.17.1 debug: 4.4.1(supports-color@8.1.1) - rc-config-loader: 4.1.3 + rc-config-loader: 4.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@secretlint/core@10.2.1': + '@secretlint/core@10.2.1(supports-color@8.1.1)': dependencies: '@secretlint/profiler': 10.2.1 '@secretlint/types': 10.2.1 @@ -8483,11 +8560,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@secretlint/formatter@10.2.1': + '@secretlint/formatter@10.2.1(supports-color@8.1.1)': dependencies: '@secretlint/resolver': 10.2.1 '@secretlint/types': 10.2.1 - '@textlint/linter-formatter': 15.2.0 + '@textlint/linter-formatter': 15.2.0(supports-color@8.1.1) '@textlint/module-interop': 15.2.0 '@textlint/types': 15.2.0 chalk: 5.4.1 @@ -8499,11 +8576,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@secretlint/node@10.2.1': + '@secretlint/node@10.2.1(supports-color@8.1.1)': dependencies: - '@secretlint/config-loader': 10.2.1 - '@secretlint/core': 10.2.1 - '@secretlint/formatter': 10.2.1 + '@secretlint/config-loader': 10.2.1(supports-color@8.1.1) + '@secretlint/core': 10.2.1(supports-color@8.1.1) + '@secretlint/formatter': 10.2.1(supports-color@8.1.1) '@secretlint/profiler': 10.2.1 '@secretlint/source-creator': 10.2.1 '@secretlint/types': 10.2.1 @@ -8725,79 +8802,79 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@storybook/addon-a11y@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@storybook/addon-a11y@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) - '@storybook/addon-docs@9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@storybook/addon-docs@9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-docs@9.1.5(@types/react@18.3.23)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-docs@9.1.5(@types/react@18.3.23)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) - '@storybook/addon-vitest@9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4)': + '@storybook/addon-vitest@9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prompts: 2.4.2 - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - '@storybook/builder-vite@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/builder-vite@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))) - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/csf-plugin@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))': dependencies: - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -8807,75 +8884,75 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-dom-shim@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': + '@storybook/react-dom-shim@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) - '@storybook/react-dom-shim@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/react-dom-shim@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) - '@storybook/react-vite@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/react-vite@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@storybook/builder-vite': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/react': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(typescript@5.8.3) + '@storybook/builder-vite': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) + '@storybook/react': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 - react-docgen: 8.0.0 + react-docgen: 8.0.0(supports-color@9.4.0) react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react-vite@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/react-vite@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@storybook/builder-vite': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/react': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + '@storybook/builder-vite': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) + '@storybook/react': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 - react-docgen: 8.0.0 + react-docgen: 8.0.0(supports-color@9.4.0) react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(typescript@5.8.3)': + '@storybook/react@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0) optionalDependencies: typescript: 5.8.3 - '@storybook/react@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)': + '@storybook/react@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) optionalDependencies: typescript: 5.8.3 @@ -9020,15 +9097,17 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) '@tanstack/history@1.129.7': {} + '@tanstack/history@1.162.1': {} + '@tanstack/query-core@5.83.0': {} '@tanstack/react-query@5.83.0(react@18.3.1)': @@ -9036,10 +9115,10 @@ snapshots: '@tanstack/query-core': 5.83.0 react: 18.3.1 - '@tanstack/react-router-devtools@1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.129.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.171.27)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: '@tanstack/react-router': 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.131.26(@tanstack/router-core@1.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/router-devtools-core': 1.131.26(@tanstack/router-core@1.171.27)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -9088,9 +9167,16 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.131.26(@tanstack/router-core@1.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-core@1.171.27': dependencies: - '@tanstack/router-core': 1.129.8 + '@tanstack/history': 1.162.1 + cookie-es: 3.1.1 + seroval: 1.6.4 + seroval-plugins: 1.6.4(seroval@1.6.4) + + '@tanstack/router-devtools-core@1.131.26(@tanstack/router-core@1.171.27)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + dependencies: + '@tanstack/router-core': 1.171.27 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.7 @@ -9098,10 +9184,10 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.129.8': + '@tanstack/router-generator@1.129.8(supports-color@9.4.0)': dependencies: '@tanstack/router-core': 1.129.8 - '@tanstack/router-utils': 1.129.7 + '@tanstack/router-utils': 1.129.7(supports-color@9.4.0) '@tanstack/virtual-file-routes': 1.129.7 prettier: 3.6.2 recast: 0.23.11 @@ -9111,35 +9197,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8))': + '@tanstack/router-plugin@1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@9.4.0)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 '@tanstack/router-core': 1.129.8 - '@tanstack/router-generator': 1.129.8 - '@tanstack/router-utils': 1.129.7 + '@tanstack/router-generator': 1.129.8(supports-color@9.4.0) + '@tanstack/router-utils': 1.129.7(supports-color@9.4.0) '@tanstack/virtual-file-routes': 1.129.7 - babel-dead-code-elimination: 1.0.10 + babel-dead-code-elimination: 1.0.10(supports-color@9.4.0) chokidar: 3.6.0 unplugin: 2.3.5 zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - webpack: 5.99.8(esbuild@0.25.8) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + webpack: 5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.129.7': + '@tanstack/router-utils@1.129.7(supports-color@9.4.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0))(supports-color@9.4.0) ansis: 4.1.0 diff: 8.0.2 transitivePeerDependencies: @@ -9190,7 +9276,7 @@ snapshots: '@textlint/ast-node-types@15.2.0': {} - '@textlint/linter-formatter@15.2.0': + '@textlint/linter-formatter@15.2.0(supports-color@8.1.1)': dependencies: '@azu/format-text': 1.0.2 '@azu/style-format': 1.0.1 @@ -9468,15 +9554,32 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/type-utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/type-utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.38.0 - eslint: 9.31.0(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -9485,23 +9588,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.38.0 '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@8.1.1)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 - eslint: 9.31.0(jiti@2.4.2) + debug: 4.4.1(supports-color@8.1.1) + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@9.4.0) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.38.0(supports-color@8.1.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.38.0(supports-color@9.4.0)(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -9515,13 +9639,25 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + debug: 4.4.1(supports-color@8.1.1) + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.31.0(jiti@2.4.2) + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + debug: 4.4.1(supports-color@9.4.0) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -9529,13 +9665,29 @@ snapshots: '@typescript-eslint/types@8.38.0': {} - '@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.38.0(supports-color@8.1.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.38.0(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@8.1.1) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.38.0(supports-color@9.4.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.38.0(typescript@5.8.3) + '@typescript-eslint/project-service': 8.38.0(supports-color@9.4.0)(typescript@5.8.3) '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -9545,13 +9697,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.38.0 '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) - eslint: 9.31.0(jiti@2.4.2) + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@8.1.1)(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0)) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@9.4.0)(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -9561,10 +9724,10 @@ snapshots: '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@typespec/ts-http-runtime@0.3.0': + '@typespec/ts-http-runtime@0.3.0(supports-color@8.1.1)': dependencies: - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -9603,48 +9766,48 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.13.2(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitejs/plugin-react@4.7.0(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitejs/plugin-react@4.7.0(supports-color@9.4.0)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0(supports-color@9.4.0)) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/utils': 3.2.3 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9654,16 +9817,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@8.1.1))(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9673,16 +9836,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9693,7 +9856,7 @@ snapshots: - vite optional: true - '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4)': + '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(supports-color@8.1.1)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9701,16 +9864,16 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) istanbul-reports: 3.1.7 magic-string: 0.30.17 magicast: 0.3.5 std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -9722,29 +9885,29 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -9783,7 +9946,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@8.1.1))(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) '@vitest/utils@3.2.3': dependencies: @@ -9803,11 +9966,13 @@ snapshots: '@volar/source-map@2.4.23': {} - '@volar/typescript@2.4.23': + '@volar/typescript@2.4.23(typescript@5.8.3)': dependencies: '@volar/language-core': 2.4.23 path-browserify: 1.0.1 vscode-uri: 3.1.0 + optionalDependencies: + typescript: 5.8.3 '@vscode/python-extension@1.0.5': {} @@ -9823,10 +9988,10 @@ snapshots: supports-color: 9.4.0 yargs: 17.7.2 - '@vscode/test-electron@2.5.2': + '@vscode/test-electron@2.5.2(supports-color@8.1.1)': dependencies: - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) jszip: 3.10.1 ora: 8.2.0 semver: 7.7.2 @@ -9872,10 +10037,10 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.5 '@vscode/vsce-sign-win32-x64': 2.0.5 - '@vscode/vsce@3.6.0': + '@vscode/vsce@3.6.0(supports-color@8.1.1)': dependencies: - '@azure/identity': 4.10.2 - '@secretlint/node': 10.2.1 + '@azure/identity': 4.10.2(supports-color@8.1.1) + '@secretlint/node': 10.2.1(supports-color@8.1.1) '@secretlint/secretlint-formatter-sarif': 10.2.1 '@secretlint/secretlint-rule-no-dotenv': 10.2.1 '@secretlint/secretlint-rule-preset-recommend': 10.2.1 @@ -9895,7 +10060,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 10.2.1 + secretlint: 10.2.1(supports-color@8.1.1) semver: 7.7.2 tmp: 0.2.3 typed-rest-client: 1.8.11 @@ -10230,11 +10395,11 @@ snapshots: tunnel: 0.0.6 typed-rest-client: 1.8.11 - babel-dead-code-elimination@1.0.10: + babel-dead-code-elimination@1.0.10(supports-color@9.4.0): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) '@babel/parser': 7.28.0 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -10523,6 +10688,8 @@ snapshots: cookie-es@1.2.2: {} + cookie-es@3.1.1: {} + cookie@1.0.2: {} core-util-is@1.0.3: {} @@ -10633,15 +10800,17 @@ snapshots: de-indent@1.0.2: {} - debug@4.4.1: + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 - debug@4.4.1(supports-color@8.1.1): + debug@4.4.1(supports-color@9.4.0): dependencies: ms: 2.1.3 optionalDependencies: - supports-color: 8.1.1 + supports-color: 9.4.0 decamelize@4.0.0: {} @@ -10907,9 +11076,9 @@ snapshots: es6-promise@3.3.1: {} - esbuild-register@3.6.0(esbuild@0.25.8): + esbuild-register@3.6.0(esbuild@0.25.8)(supports-color@9.4.0): dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -10947,15 +11116,15 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0)): dependencies: - eslint: 9.31.0(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) - eslint-plugin-storybook@9.1.5(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3): + eslint-plugin-storybook@9.1.5(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)))(supports-color@9.4.0)(typescript@5.8.3): dependencies: - '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.31.0(jiti@2.4.2) - storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) + storybook: 9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - typescript @@ -10974,14 +11143,14 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.31.0(jiti@2.4.2): + eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 + '@eslint/config-array': 0.21.0(supports-color@8.1.1) '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.15.1 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.1(supports-color@8.1.1) '@eslint/js': 9.31.0 '@eslint/plugin-kit': 0.3.4 '@humanfs/node': 0.16.6 @@ -10992,7 +11161,49 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0(supports-color@9.4.0) + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1(supports-color@9.4.0) + '@eslint/js': 9.31.0 + '@eslint/plugin-kit': 0.3.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1(supports-color@9.4.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -11343,7 +11554,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@9.4.0): dependencies: '@types/estree': 1.0.8 '@types/hast': 3.0.4 @@ -11352,9 +11563,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@9.4.0) + mdast-util-mdx-jsx: 3.2.0(supports-color@9.4.0) + mdast-util-mdxjs-esm: 2.0.1(supports-color@9.4.0) property-information: 7.1.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.17 @@ -11400,19 +11611,33 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2(supports-color@9.4.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@9.4.0) transitivePeerDependencies: - supports-color http2-client@1.3.5: {} - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@9.4.0): dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -11638,7 +11863,7 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@5.0.6(supports-color@8.1.1): dependencies: '@jridgewell/trace-mapping': 0.3.29 debug: 4.4.1(supports-color@8.1.1) @@ -11692,14 +11917,42 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: + jsdom@26.1.0(supports-color@8.1.1): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsdom@26.1.0(supports-color@9.4.0): dependencies: cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@9.4.0) + https-proxy-agent: 7.0.6(supports-color@9.4.0) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.20 parse5: 7.3.0 @@ -11962,7 +12215,7 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 magicast@0.3.5: dependencies: @@ -11985,14 +12238,14 @@ snapshots: math-intrinsics@1.1.0: {} - mdast-util-from-markdown@2.0.2: + mdast-util-from-markdown@2.0.2(supports-color@9.4.0): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@9.4.0) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -12002,18 +12255,18 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@9.4.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.2(supports-color@9.4.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@9.4.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 @@ -12021,7 +12274,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.2(supports-color@9.4.0) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -12030,13 +12283,13 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@9.4.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.2(supports-color@9.4.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -12191,10 +12444,10 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@9.4.0): dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@9.4.0) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -12462,11 +12715,11 @@ snapshots: openapi3-ts@4.2.2: dependencies: - yaml: 2.8.0 + yaml: 2.9.0 openapi3-ts@4.4.0: dependencies: - yaml: 2.8.0 + yaml: 2.9.0 optionator@0.9.4: dependencies: @@ -12489,19 +12742,19 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 - orval@7.10.0(openapi-types@12.1.3): + orval@7.10.0(openapi-types@12.1.3)(supports-color@9.4.0): dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@orval/angular': 7.10.0(openapi-types@12.1.3) - '@orval/axios': 7.10.0(openapi-types@12.1.3) - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) - '@orval/hono': 7.10.0(openapi-types@12.1.3) - '@orval/mcp': 7.10.0(openapi-types@12.1.3) - '@orval/mock': 7.10.0(openapi-types@12.1.3) - '@orval/query': 7.10.0(openapi-types@12.1.3) - '@orval/swr': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/angular': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/axios': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/core': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/fetch': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/hono': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/mcp': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/mock': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/query': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/swr': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) + '@orval/zod': 7.10.0(openapi-types@12.1.3)(supports-color@9.4.0) ajv: 8.17.1 cac: 6.7.14 chalk: 4.1.2 @@ -12679,7 +12932,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 - yaml: 2.8.0 + yaml: 2.9.0 optionalDependencies: postcss: 8.5.6 @@ -12773,7 +13026,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - rc-config-loader@4.1.3: + rc-config-loader@4.1.3(supports-color@8.1.1): dependencies: debug: 4.4.1(supports-color@8.1.1) js-yaml: 4.1.0 @@ -12810,10 +13063,10 @@ snapshots: dependencies: typescript: 5.8.3 - react-docgen@8.0.0: + react-docgen@8.0.0(supports-color@9.4.0): dependencies: - '@babel/core': 7.28.0 - '@babel/traverse': 7.28.0 + '@babel/core': 7.28.0(supports-color@9.4.0) + '@babel/traverse': 7.28.0(supports-color@9.4.0) '@babel/types': 7.28.1 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 @@ -12835,17 +13088,17 @@ snapshots: react-is@17.0.2: {} - react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1): + react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1)(supports-color@9.4.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/react': 18.3.23 devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@9.4.0) html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 react: 18.3.1 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@9.4.0) remark-rehype: 11.1.2 unified: 11.0.5 unist-util-visit: 5.0.0 @@ -12997,10 +13250,10 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@9.4.0): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.2(supports-color@9.4.0) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -13113,11 +13366,11 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - secretlint@10.2.1: + secretlint@10.2.1(supports-color@8.1.1): dependencies: '@secretlint/config-creator': 10.2.1 - '@secretlint/formatter': 10.2.1 - '@secretlint/node': 10.2.1 + '@secretlint/formatter': 10.2.1(supports-color@8.1.1) + '@secretlint/node': 10.2.1(supports-color@8.1.1) '@secretlint/profiler': 10.2.1 debug: 4.4.1(supports-color@8.1.1) globby: 14.1.0 @@ -13147,8 +13400,14 @@ snapshots: dependencies: seroval: 1.3.2 + seroval-plugins@1.6.4(seroval@1.6.4): + dependencies: + seroval: 1.6.4 + seroval@1.3.2: {} + seroval@1.6.4: {} + set-cookie-parser@2.7.1: {} set-function-length@1.2.2: @@ -13323,7 +13582,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2): + storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 @@ -13332,7 +13591,7 @@ snapshots: '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.8 - esbuild-register: 3.6.0(esbuild@0.25.8) + esbuild-register: 3.6.0(esbuild@0.25.8)(supports-color@9.4.0) recast: 0.23.11 semver: 7.7.2 ws: 8.18.3 @@ -13344,17 +13603,17 @@ snapshots: - supports-color - utf-8-validate - storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)): + storybook@9.1.5(@testing-library/dom@10.4.1)(prettier@3.6.2)(supports-color@9.4.0)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.8 - esbuild-register: 3.6.0(esbuild@0.25.8) + esbuild-register: 3.6.0(esbuild@0.25.8)(supports-color@9.4.0) recast: 0.23.11 semver: 7.7.2 ws: 8.18.3 @@ -13622,15 +13881,16 @@ snapshots: ansi-escapes: 7.0.0 supports-hyperlinks: 3.2.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)): + terser-webpack-plugin@5.3.14(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.44.0 - webpack: 5.99.8(esbuild@0.25.8) + webpack: 5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8) optionalDependencies: + '@swc/core': 1.13.2(@swc/helpers@0.5.17) esbuild: 0.25.8 terser@5.44.0: @@ -13729,7 +13989,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.8)): + ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.2 @@ -13737,7 +13997,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.8(esbuild@0.25.8) + webpack: 5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8) ts-toolbelt@9.6.0: {} @@ -13825,15 +14085,26 @@ snapshots: markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 - yaml: 2.8.0 + yaml: 2.9.0 - typescript-eslint@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.31.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@8.1.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2)(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript-eslint@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.38.0(supports-color@9.4.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2)(supports-color@9.4.0))(supports-color@9.4.0)(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2)(supports-color@9.4.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -13982,13 +14253,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -14003,13 +14274,34 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@9.4.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@9.4.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -14024,39 +14316,39 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - vite-plugin-dts@4.5.4(@types/node@20.11.25)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@20.11.25)(rollup@4.45.1)(supports-color@9.4.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@20.11.25) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@volar/typescript': 2.4.23 + '@volar/typescript': 2.4.23(typescript@5.8.3) '@vue/language-core': 2.2.0(typescript@5.8.3) compare-versions: 6.1.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@9.4.0) kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 typescript: 5.8.3 optionalDependencies: - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-static-copy@3.1.1(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)): + vite-plugin-static-copy@3.1.1(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: chokidar: 3.6.0 fs-extra: 11.3.0 p-map: 7.0.3 picocolors: 1.1.1 tinyglobby: 0.2.14 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) - vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -14071,9 +14363,9 @@ snapshots: lightningcss: 1.30.1 terser: 5.44.0 tsx: 4.20.3 - yaml: 2.8.0 + yaml: 2.9.0 - vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -14088,20 +14380,20 @@ snapshots: lightningcss: 1.30.1 terser: 5.44.0 tsx: 4.20.3 - yaml: 2.8.0 + yaml: 2.9.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@8.1.1))(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 @@ -14112,15 +14404,15 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@8.1.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 20.11.25 - '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 + jsdom: 26.1.0(supports-color@8.1.1) transitivePeerDependencies: - jiti - less @@ -14135,18 +14427,18 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@9.4.0) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 @@ -14157,15 +14449,60 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.11.25 + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 26.1.0(supports-color@9.4.0) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1(supports-color@9.4.0) + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.1.0 - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 + jsdom: 26.1.0(supports-color@9.4.0) transitivePeerDependencies: - jiti - less @@ -14180,18 +14517,18 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@9.4.0) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 @@ -14202,15 +14539,15 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.1.0 - '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 + jsdom: 26.1.0(supports-color@9.4.0) transitivePeerDependencies: - jiti - less @@ -14265,7 +14602,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.99.8(esbuild@0.25.8): + webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -14288,7 +14625,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.3 - terser-webpack-plugin: 5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)) + terser-webpack-plugin: 5.3.14(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)(webpack@5.99.8(@swc/core@1.13.2(@swc/helpers@0.5.17))(esbuild@0.25.8)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -14409,7 +14746,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.9.0: {} yargs-parser@20.2.9: {} diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 342096731f..24199e3f7d 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -180,6 +180,6 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", "vitest": "^3.2.4", - "yaml": "^2.8.0" + "yaml": "^2.9.0" } } From c4524e571a10ee904c2df1eebfe74aef2dcc7498 Mon Sep 17 00:00:00 2001 From: codetalker-ai Date: Tue, 15 Sep 2026 04:26:45 +0300 Subject: [PATCH 54/67] Minor fix for Windows web UI interface (#4752) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> Co-authored-by: Ben <9087625+benfdking@users.noreply.github.com> --- web/server/api/endpoints/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/server/api/endpoints/files.py b/web/server/api/endpoints/files.py index 5be9032c6a..83f590438b 100644 --- a/web/server/api/endpoints/files.py +++ b/web/server/api/endpoints/files.py @@ -181,6 +181,6 @@ def _get_file_with_content(file_path: Path, relative_path: str) -> models.File: return models.File( name=file_path.name, - path=relative_path, + path=str(Path(relative_path).as_posix()), content=content, ) From e30fe61bc982b86a05098ca4f4d6c1a2cd39e576 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:38:09 -0600 Subject: [PATCH 55/67] chore(md): noting pre-commit command (#6050) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Co-authored-by: Cursor --- CONTRIBUTING.md | 4 +++- docs/development.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c764255a7..4533069d5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,10 +65,12 @@ See [docs/development.md](docs/development.md) for full setup instructions. Key python -m venv .venv source .venv/bin/activate make install-dev -make style # Run before submitting +make style # Run before submitting make fast-test # Quick test suite ``` +Optionally, `make install-pre-commit` installs git hooks so ruff and mypy run on `git commit`. Hooks do not replace `make style`: they run on staged files, while CI runs `make style` across the tree. + ## Coding Standards - Run `make style` before submitting a pull request diff --git a/docs/development.md b/docs/development.md index d80ef60689..1f0e111f02 100644 --- a/docs/development.md +++ b/docs/development.md @@ -42,7 +42,7 @@ Once you have activated your virtual environment, you can install the dependenci make install-dev ``` -Optionally, you can use pre-commit to automatically run linters/formatters: +Optionally, `make install-pre-commit` installs git hooks so ruff and mypy run on `git commit`. Hooks do not replace `make style`: they run on staged files, while CI runs `make style` across the tree. ```bash make install-pre-commit From 18074a8b953e85fe863adb5790427feaec7cb811 Mon Sep 17 00:00:00 2001 From: cookieark7 <67856015+cookieark7@users.noreply.github.com> Date: Wed, 16 Sep 2026 06:11:34 +0530 Subject: [PATCH 56/67] fix(tests): prevent schema collisions between integration test params (#6072) Signed-off-by: cookieark Co-authored-by: cookieark --- tests/core/engine_adapter/integration/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 11bf95f3d6..bb5bc2c850 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -1,7 +1,9 @@ from __future__ import annotations +import hashlib import os import pathlib +import secrets import sys import typing as t import time @@ -20,7 +22,7 @@ from sqlmesh.core.engine_adapter import SparkEngineAdapter, TrinoEngineAdapter, AthenaEngineAdapter from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.core.model.definition import SqlModel, load_sql_based_model -from sqlmesh.utils import random_id +from sqlmesh.utils import ALPHANUMERIC, random_id from sqlmesh.utils.date import to_ds from sqlmesh.utils.pydantic import PydanticModel from tests.utils.pandas import compare_dataframes @@ -205,7 +207,13 @@ def __init__( self.mark = mark self.gateway = gateway self._columns_to_types = columns_to_types - self.test_id = random_id(short=True) + # The id is appended to schema names, so it has to keep concurrent test params apart + # while staying short: temp table names built from these schemas are already close to + # Postgres's 63 character identifier limit. A tag derived from the param guarantees two + # params never share a schema, and the random part separates concurrent runs and retries. + param_tag = hashlib.sha1(mark.encode()).hexdigest()[:3] + random_part = "".join(secrets.choice(ALPHANUMERIC) for _ in range(5)) + self.test_id = f"{param_tag}{random_part}" self._context: t.Optional[Context] = None self.is_remote = is_remote self._schemas: t.List[ From ad2377ea9f065b26d957207958a024aaebc0b70f Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Wed, 16 Sep 2026 04:13:24 +0100 Subject: [PATCH 57/67] feat(test): add `--local` to run tests without loading state (#6060) Signed-off-by: Adegbite Ayoade Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- docs/concepts/tests.md | 17 +++++ docs/reference/cli.md | 3 + sqlmesh/cli/main.py | 16 +++- tests/cli/test_cli.py | 165 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index a293237687..a9bd45a25a 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -463,6 +463,23 @@ You can also run tests that match a pattern or substring using a glob pathname e $ sqlmesh test tests/test_* ``` +You can pass `--local` to run tests without loading state from the configured state connection: + +``` bash +$ sqlmesh test --local +``` + +This keeps offline runs and commit hooks from opening a connection to the state backend. + +In multi-repository setups, or when running tests for only a subset of projects, models that exist only in remote state are not loaded under `--local`. Unlike [`sqlmesh lint --local`](../guides/linter.md), which reports additional errors in that situation, a test whose model is missing is **skipped with a warning and the run still succeeds**: + +``` +[WARNING] Model '"memory"."bronze"."a"' was not found at tests/test_a.yaml +.**Successfully Ran `1` Tests Against `duckdb`** +``` + +So a passing exit code alone does not mean every test you expected actually ran. Watch the output for these warnings, and keep in mind that a hook using `--local` will not fail on them. + ### Testing using notebooks You can execute tests on demand using the `%run_test` notebook magic as follows: diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1367f8551b..1ed03535b9 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -630,6 +630,9 @@ Options: useful for debugging. --select-model TEXT Select specific models to run unit tests for. Can be specified multiple times. + --local Run tests using only locally loaded project files + without loading state. Tests whose model is not loaded + are skipped with a warning rather than failing. --help Show this message and exit. ``` diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index b6678136f0..c2c81ada9c 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -42,6 +42,8 @@ ) SKIP_CONTEXT_COMMANDS = ("init", "ui") LOCAL_ONLY_COMMANDS = ("format",) +# Commands that are local-only when they're passed --local. +OPTIONAL_LOCAL_COMMANDS = ("lint", "test") class _SQLMeshGroup(click.Group): @@ -129,8 +131,12 @@ def cli( load = True # Local-only gating must hold for any number of --paths, so it stays outside the block below. load_state = ctx.invoked_subcommand not in LOCAL_ONLY_COMMANDS - # The parent callback constructs Context before Click invokes `lint`, so inspect its parsed args here. - if ctx.invoked_subcommand == "lint" and "--local" in ctx.meta["subcommand_args"]: + # The parent callback constructs Context before Click invokes the subcommand, so inspect its + # parsed args here. + if ( + ctx.invoked_subcommand in OPTIONAL_LOCAL_COMMANDS + and "--local" in ctx.meta["subcommand_args"] + ): load_state = False if len(paths) == 1: @@ -811,6 +817,12 @@ def create_test( multiple=True, help="Select specific models to run unit tests for.", ) +@click.option( + "--local", + is_flag=True, + expose_value=False, + help="Run tests using only locally loaded project files without loading state. Tests whose model is not loaded are skipped with a warning rather than failing.", +) @click.argument("tests", nargs=-1) @click.pass_obj @error_handler diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index da73952991..b52df5394a 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -2530,6 +2530,59 @@ def test_lint_local_runs_without_state(runner: CliRunner, tmp_path: Path, mocker mock.assert_not_called() +def test_test_still_loads_state(runner: CliRunner, tmp_path: Path, mocker): + """Guard that `test` explicitly passes `load_state=True` and still reaches state sync.""" + mock = _setup_local_only_project(tmp_path, mocker) + init_spy = mocker.spy(Context, "__init__") + + runner.invoke(cli, ["--paths", str(tmp_path), "test"]) + + assert init_spy.called, "Context was never constructed" + for call in init_spy.call_args_list: + assert "load_state" in call.kwargs, ( + "CLI didn't pass load_state= explicitly; missing kwarg defaults to True silently" + ) + assert call.kwargs["load_state"] is True, ( + f"Context was constructed with load_state={call.kwargs['load_state']} for `test`" + ) + assert mock.called, "state-sync was never accessed during `test`" + + +def test_test_local_runs_without_state(runner: CliRunner, tmp_path: Path, mocker): + mock = _setup_local_only_project(tmp_path, mocker) + init_spy = mocker.spy(Context, "__init__") + + result = runner.invoke(cli, ["--paths", str(tmp_path), "test", "--local"]) + + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + assert init_spy.called, "Context was never constructed" + for call in init_spy.call_args_list: + assert "load_state" in call.kwargs, ( + "CLI didn't pass load_state= explicitly; missing kwarg defaults to True silently" + ) + assert call.kwargs["load_state"] is False, ( + f"Context was constructed with load_state={call.kwargs['load_state']} for `test --local`" + ) + mock.assert_not_called() + + +def test_test_local_runs_without_state_multiple_paths( + runner: CliRunner, tmp_path: Path, mocker +) -> None: + """`--local` gating must hold for any number of --paths, matching `lint --local`.""" + project_a = tmp_path / "a" + project_b = tmp_path / "b" + _create_local_only_project(project_a, "proj_a") + _create_local_only_project(project_b, "proj_b") + mock = _patch_state_access(mocker) + + result = runner.invoke( + cli, ["--paths", str(project_a), "--paths", str(project_b), "test", "--local"] + ) + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + mock.assert_not_called() + + @pytest.mark.parametrize("command", ["format"]) def test_local_only_commands_skip_state_multiple_paths( runner: CliRunner, tmp_path: Path, mocker, command: str @@ -2611,3 +2664,115 @@ def test_format_does_not_open_state_connection( result = runner.invoke(cli, ["--paths", str(tmp_path), "format"]) assert result.exit_code == 0, f"Format failed: {result.output}\nException: {result.exception}" mock.assert_not_called() + + +def test_test_local_runs_project_unit_tests(runner: CliRunner, tmp_path: Path, mocker) -> None: + """A real unit test from the project's YAML runs under `--local` without touching state.""" + create_example_project(tmp_path) + mock = _patch_state_access(mocker) + + result = runner.invoke(cli, ["--paths", str(tmp_path), "test", "--local"]) + + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + assert "Successfully Ran 1 tests" in " ".join(result.output.split()) + mock.assert_not_called() + + +def test_test_local_does_not_open_state_connection( + runner: CliRunner, tmp_path: Path, mocker, monkeypatch +) -> None: + """`test --local` must not open a configured remote Postgres state connection.""" + pytest.importorskip("psycopg2") + + for var in ("PG_HOST", "PG_USER", "PG_PASSWORD", "PG_DATABASE"): + monkeypatch.delenv(var, raising=False) + + create_example_project(tmp_path) + (tmp_path / "config.yaml").write_text( + """project: cli_test + +gateways: + prod: + state_connection: + type: postgres + host: "{{ env_var('PG_HOST', 'postgres.internal.example.com') }}" + port: 5432 + user: "{{ env_var('PG_USER') }}" + password: "{{ env_var('PG_PASSWORD') }}" + database: "{{ env_var('PG_DATABASE', 'sqlmesh_state') }}" + connection: + type: duckdb + database: "warehouse.db" + +default_gateway: prod + +model_defaults: + dialect: duckdb +""", + encoding="utf-8", + ) + + mock = _patch_state_access(mocker) + + result = runner.invoke(cli, ["--paths", str(tmp_path), "test", "--local"]) + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + mock.assert_not_called() + + +def test_test_local_multi_repo_partial(runner: CliRunner, copy_to_temp_path, mocker) -> None: + """Run tests for one repo of a multi-repo project whose upstream models live only in state. + + Pins the behavioral difference against `lint --local`: a model that isn't loaded produces a + warning and its test is skipped, rather than turning into an error. + """ + repo_2 = copy_to_temp_path("examples/multi")[0] / "repo_2" + + # silver.c lives in repo_2 and its upstream bronze.a is supplied as a test input. + (repo_2 / "tests" / "test_c.yaml").write_text( + """test_silver_c: + model: silver.c + inputs: + bronze.a: + rows: + - col_a: 1 + - col_a: 1 + - col_a: 2 + outputs: + query: + rows: + - col_a: 1 + - col_a: 2 +""", + encoding="utf-8", + ) + # bronze.a itself is defined in repo_1, so it is not loaded when only repo_2 is given. + (repo_2 / "tests" / "test_a.yaml").write_text( + """test_bronze_a: + model: bronze.a + outputs: + query: + rows: + - col_a: 1 +""", + encoding="utf-8", + ) + + mock = _patch_state_access(mocker) + args = ["--gateway", "memory", "--paths", str(repo_2), "test"] + + # Without --local the same run reaches the state backend. + runner.invoke(cli, args) + assert mock.called, "state-sync was never accessed during `test`" + + mock.reset_mock() + + result = runner.invoke(cli, [*args, "--local"]) + + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + # Console output wraps, so compare against whitespace-normalized text. + output = " ".join(result.output.split()) + assert 'Model \'"memory"."bronze"."a"\' was not found' in output, ( + "the unloaded model should warn rather than fail" + ) + assert "Successfully Ran 1 tests" in output, "the repo_2 test should still run" + mock.assert_not_called() From 76225c61905c618b4f1bc7e616553df6039e0f88 Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Sun, 20 Sep 2026 21:08:26 +0100 Subject: [PATCH 58/67] feat(test): treat model file paths as test selectors (#6061) Signed-off-by: Adegbite Ayoade --- docs/concepts/tests.md | 14 ++++ docs/reference/cli.md | 4 ++ sqlmesh/cli/main.py | 8 ++- sqlmesh/core/context.py | 122 ++++++++++++++++++++++++++++---- tests/cli/test_cli.py | 95 +++++++++++++++++++++++++ tests/core/test_test.py | 151 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 16 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index a9bd45a25a..8bf0fe0cbf 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -463,6 +463,20 @@ You can also run tests that match a pattern or substring using a glob pathname e $ sqlmesh test tests/test_* ``` +Passing the path of a model file runs the tests for that model, which is useful for commit hooks and other tools that work with changed files rather than test names: + +``` +$ sqlmesh test models/full_model.sql +``` + +Model files and test files can be mixed, and the results are unioned. A test selected by more than one argument still runs only once, so the following runs each of `full_model`'s tests a single time even though both arguments cover them: + +``` +$ sqlmesh test models/full_model.sql tests/test_full_model.yaml +``` + +An argument that is neither a known model file nor a known test file is an error, so a mistyped or stale path fails instead of quietly running no tests. A model that simply has no tests is not an error. + You can pass `--local` to run tests without loading state from the configured state connection: ``` bash diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1ed03535b9..f7943e2f71 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -623,6 +623,10 @@ Usage: sqlmesh test [OPTIONS] [TESTS]... Run model unit tests. + TESTS are test files, `file.yaml::test_name` selectors, or model files, in + which case the tests for those models are run. They are unioned, and a test + selected more than once still only runs once. + Options: -k TEXT Only run tests that match the pattern of substring. -v, --verbose Verbose output. diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index c2c81ada9c..fc6cad8011 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -835,7 +835,12 @@ def test( select_model: t.List[str], tests: t.List[str], ) -> None: - """Run model unit tests.""" + """Run model unit tests. + + TESTS are test files, `file.yaml::test_name` selectors, or model files, in which case the + tests for those models are run. They are unioned, and a test selected more than once still + only runs once. + """ model_names = ( obj._new_selector().expand_model_selections(select_model) if select_model else None ) @@ -845,6 +850,7 @@ def test( verbosity=Verbosity(verbose), preserve_fixtures=preserve_fixtures, model_names=model_names, + raise_on_unknown_paths=True, ) if not result.wasSuccessful(): exit(1) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index b70ccae141..64dd6d67da 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -36,6 +36,7 @@ import abc import collections import logging +import os.path import sys import time import traceback @@ -119,7 +120,7 @@ filter_tests_by_patterns, ) from sqlmesh.core.user import User -from sqlmesh.utils import CorrelationId, UniqueKeyDict, Verbosity +from sqlmesh.utils import CorrelationId, UniqueKeyDict, Verbosity, unique from sqlmesh.utils.concurrency import concurrent_apply_to_values from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( @@ -2407,6 +2408,7 @@ def test( preserve_fixtures: bool = False, stream: t.Optional[t.TextIO] = None, model_names: t.Optional[t.Collection[str]] = None, + raise_on_unknown_paths: bool = False, ) -> ModelTextTestResult: """Discover and run model tests""" if verbosity >= Verbosity.VERBOSE: @@ -2414,10 +2416,18 @@ def test( pd.set_option("display.max_columns", None) - baseline_meta = self.select_tests(tests=tests, patterns=match_patterns, model_names=None) + baseline_meta = self.select_tests( + tests=tests, + patterns=match_patterns, + model_names=None, + raise_on_unknown_paths=raise_on_unknown_paths, + ) if model_names is not None: test_meta = self.select_tests( - tests=tests, patterns=match_patterns, model_names=model_names + tests=tests, + patterns=match_patterns, + model_names=model_names, + raise_on_unknown_paths=raise_on_unknown_paths, ) tests_skipped = len(baseline_meta) - len(test_meta) else: @@ -3611,30 +3621,112 @@ def lint_models( return all_violations + def _tests_by_absolute_model_path(self) -> t.Dict[str, t.List[ModelTestMetadata]]: + """Map each model file to the tests that target the model(s) defined in it.""" + tests_by_model_name: t.Dict[str, t.List[ModelTestMetadata]] = collections.defaultdict(list) + for metadata in self._model_test_metadata: + if metadata.model_name: + tests_by_model_name[ + normalize_model_name( + metadata.model_name, + default_catalog=self.default_catalog, + dialect=self.default_dialect, + ) + ].append(metadata) + + # A path is made absolute rather than resolved, so this costs no syscalls per model. + tests_by_path: t.Dict[str, t.List[ModelTestMetadata]] = {} + for fqn, model in self._models.items(): + if model._path is not None: + tests_by_path.setdefault(os.path.abspath(model._path), []).extend( + tests_by_model_name.get(fqn, []) + ) + + return tests_by_path + + def _select_tests_by_test_path(self, selector: str) -> t.Optional[t.List[ModelTestMetadata]]: + """Resolve a selector against the test files, or return None if it matches none of them. + + The selector is a test file path or a `path::test_name`. Paths are matched as given + first, so an unchanged selector never pays for normalization. + """ + if "::" in selector: + metadata = self._model_test_metadata_fully_qualified_name_index.get(selector) + if metadata is None: + path, _, test_name = selector.rpartition("::") + metadata = self._model_test_metadata_fully_qualified_name_index.get( + f"{os.path.abspath(path)}::{test_name}" + ) + return [metadata] if metadata is not None else None + + for candidate in (Path(selector), Path(os.path.abspath(selector))): + matched = self._model_test_metadata_path_index.get(candidate) + if matched is not None: + return list(matched) + + return None + + def _unknown_test_selector_error(self, selector: str) -> str: + """Explains why a selector matched nothing. + + A `path::test_name` whose file is a known test file failed on the test name, not the + path, so the message says so rather than claiming the file is unknown. + """ + if "::" in selector: + path, _, _ = selector.rpartition("::") + if any( + candidate in self._model_test_metadata_path_index + for candidate in (Path(path), Path(os.path.abspath(path))) + ): + return f"'{selector}' is not a known test in '{path}'." + + return f"'{selector}' is not a known model or test file." + def select_tests( self, tests: t.Optional[t.List[str]] = None, patterns: t.Optional[t.List[str]] = None, model_names: t.Optional[t.Collection[str]] = None, + raise_on_unknown_paths: bool = False, ) -> t.List[ModelTestMetadata]: - """Filter pre-loaded test metadata based on tests and patterns.""" + """Filter pre-loaded test metadata based on tests and patterns. + + Args: + tests: Test selectors. Each one is a test file path, a `path::test_name`, or the path + of a model file, in which case that model's tests are selected. Selectors are + unioned and the result is deduplicated, so a model file and a test file that + resolve to the same test run it once rather than twice. + patterns: Patterns matched against fully qualified test names. + model_names: If given, narrows the selection to tests targeting these models. + raise_on_unknown_paths: Whether to raise when a selector matches neither a known test + nor a known model file. Off by default so that callers which probe arbitrary + documents, such as the LSP, keep getting an empty result instead of an error. + """ test_meta = self._model_test_metadata if tests: - filtered_tests = [] + filtered_tests: t.List[ModelTestMetadata] = [] + # Built at most once, and only if a selector turns out not to be a test file. + tests_by_model_path: t.Optional[t.Dict[str, t.List[ModelTestMetadata]]] = None + for test in tests: - if "::" in test: - if test in self._model_test_metadata_fully_qualified_name_index: - filtered_tests.append( - self._model_test_metadata_fully_qualified_name_index[test] - ) - else: - test_path = Path(test) - if test_path in self._model_test_metadata_path_index: - filtered_tests.extend(self._model_test_metadata_path_index[test_path]) + matched = self._select_tests_by_test_path(test) + if matched is None and "::" not in test: + if tests_by_model_path is None: + tests_by_model_path = self._tests_by_absolute_model_path() + # A known model with no tests matches an empty list, which is not the same + # as a selector that resolves to nothing at all. + matched = tests_by_model_path.get(os.path.abspath(test)) + if matched is None: + if raise_on_unknown_paths: + raise SQLMeshError(self._unknown_test_selector_error(test)) + continue + filtered_tests.extend(matched) - test_meta = filtered_tests + # Selectors can overlap, e.g. a model file and the test file holding its tests, so + # the union is deduplicated to avoid running the same test more than once. + test_meta = unique(filtered_tests) if patterns: test_meta = filter_tests_by_patterns(test_meta, patterns) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index b52df5394a..251ea54fa3 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -2666,6 +2666,27 @@ def test_format_does_not_open_state_connection( mock.assert_not_called() +def test_test_accepts_model_paths(runner: CliRunner, tmp_path: Path) -> None: + create_example_project(tmp_path) + + result = runner.invoke( + cli, ["--paths", str(tmp_path), "test", str(tmp_path / "models" / "full_model.sql")] + ) + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + assert "Ran 1 test" in result.output + + +def test_test_unknown_path_fails(runner: CliRunner, tmp_path: Path) -> None: + """A staged file that resolves to nothing must fail rather than silently run no tests.""" + create_example_project(tmp_path) + + result = runner.invoke( + cli, ["--paths", str(tmp_path), "test", str(tmp_path / "models" / "nope.sql")] + ) + assert result.exit_code != 0 + assert "is not a known model or test file" in result.output + + def test_test_local_runs_project_unit_tests(runner: CliRunner, tmp_path: Path, mocker) -> None: """A real unit test from the project's YAML runs under `--local` without touching state.""" create_example_project(tmp_path) @@ -2776,3 +2797,77 @@ def test_test_local_multi_repo_partial(runner: CliRunner, copy_to_temp_path, moc ) assert "Successfully Ran 1 tests" in output, "the repo_2 test should still run" mock.assert_not_called() + + +def test_test_local_with_model_paths(runner: CliRunner, tmp_path: Path, mocker) -> None: + """`--local` and model path selectors compose, which is the pre-commit hook case in #6020.""" + create_example_project(tmp_path) + mock = _patch_state_access(mocker) + + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "test", "--local", str(tmp_path / "models" / "full_model.sql")], + ) + + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + assert "Successfully Ran 1 tests" in " ".join(result.output.split()) + mock.assert_not_called() + + # An unresolvable path still fails loudly, without reaching state. + result = runner.invoke( + cli, ["--paths", str(tmp_path), "test", "--local", str(tmp_path / "models" / "nope.sql")] + ) + assert result.exit_code != 0 + assert "is not a known model or test file" in result.output + mock.assert_not_called() + + +def test_test_local_with_python_model_paths(runner: CliRunner, tmp_path: Path, mocker) -> None: + """The `--local` + path-selector combination works for Python models too.""" + create_example_project(tmp_path) + + (tmp_path / "models" / "py_model.py").write_text( + """ +import pandas as pd # noqa: TID253 +from sqlmesh import model, ExecutionContext +import typing as t + +@model( + name="sqlmesh_example.py_model", + columns={"id": "int"}, +) +def execute(context: ExecutionContext, **kwargs: t.Any) -> pd.DataFrame: + return pd.DataFrame([{"id": 1}]) +""", + encoding="utf-8", + ) + (tmp_path / "tests" / "test_py_model.yaml").write_text( + """ +test_py_model: + model: sqlmesh_example.py_model + outputs: + query: + rows: + - id: 1 +""", + encoding="utf-8", + ) + + mock = _patch_state_access(mocker) + + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "test", "--local", str(tmp_path / "models" / "py_model.py")], + ) + + assert result.exit_code == 0, f"Test failed: {result.output}\nException: {result.exception}" + assert "Successfully Ran 1 tests" in " ".join(result.output.split()) + mock.assert_not_called() + + # A Python file that is not a model is still an error rather than a silent no-op. + result = runner.invoke( + cli, ["--paths", str(tmp_path), "test", "--local", str(tmp_path / "models" / "nope.py")] + ) + assert result.exit_code != 0 + assert "is not a known model or test file" in result.output + mock.assert_not_called() diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 7c67192a14..0fbe8e926c 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import re import typing as t import io from pathlib import Path @@ -2687,6 +2688,156 @@ def test_number_of_tests_found(tmp_path: Path) -> None: assert len(results.successes) == 1 +def test_model_path_selects_its_tests(tmp_path: Path) -> None: + """A model file path selects that model's tests, even though the YAML path wasn't given.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + results = context.test(tests=[str(tmp_path / "models" / "full_model.sql")]) + assert len(results.successes) == 1 + assert results.testsRun == 1 + + +def test_python_model_path_selects_its_tests(tmp_path: Path) -> None: + """Selection is by file path, so a Python model works the same way a SQL one does.""" + init_example_project(tmp_path, engine_type="duckdb") + + py_model = tmp_path / "models" / "py_model.py" + py_model.write_text( + """ +import pandas as pd # noqa: TID253 +from sqlmesh import model, ExecutionContext +import typing as t + +@model( + name="sqlmesh_example.py_model", + columns={"id": "int"}, +) +def execute(context: ExecutionContext, **kwargs: t.Any) -> pd.DataFrame: + return pd.DataFrame([{"id": 1}]) +""" + ) + (tmp_path / "tests" / "test_py_model.yaml").write_text( + """ +test_py_model: + model: sqlmesh_example.py_model + outputs: + query: + rows: + - id: 1 +""" + ) + + context = Context(paths=tmp_path) + + results = context.test(tests=[str(py_model)]) + assert results.testsRun == 1 + assert len(results.successes) == 1 + + # The SQL model's own test is not pulled in by selecting the Python model. + assert context.test(tests=[str(tmp_path / "models" / "full_model.sql")]).testsRun == 1 + + +def test_model_path_without_tests_selects_nothing(tmp_path: Path) -> None: + """A known model that simply has no tests is not an error.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + results = context.test(tests=[str(tmp_path / "models" / "incremental_model.sql")]) + assert results.testsRun == 0 + assert results.wasSuccessful() + + +def test_model_and_test_paths_are_unioned_without_duplicates(tmp_path: Path) -> None: + """Overlapping selectors must not run the same test twice.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + model_path = str(tmp_path / "models" / "full_model.sql") + test_path = str(tmp_path / "tests" / "test_full_model.yaml") + + # The YAML holds full_model's only test, so both selectors resolve to the same test. + assert context.test(tests=[model_path]).testsRun == 1 + assert context.test(tests=[test_path]).testsRun == 1 + assert context.test(tests=[model_path, test_path]).testsRun == 1 + + +def test_overlapping_yaml_and_named_test_are_deduplicated(tmp_path: Path) -> None: + """`file.yaml::name` is a subset of `file.yaml`, so together they're still one run.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + test_path = str(tmp_path / "tests" / "test_full_model.yaml") + results = context.test(tests=[f"{test_path}::test_example_full_model", test_path]) + assert results.testsRun == 1 + + +def test_relative_paths_select_tests(tmp_path: Path, monkeypatch) -> None: + """Pre-commit passes paths relative to the repo root, not absolute ones.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + monkeypatch.chdir(tmp_path) + assert context.test(tests=["models/full_model.sql"]).testsRun == 1 + assert context.test(tests=["tests/test_full_model.yaml"]).testsRun == 1 + + +def test_unknown_path_is_ignored_by_default(tmp_path: Path) -> None: + """Default behavior is unchanged, so the LSP can keep probing arbitrary documents.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + assert context.select_tests(tests=[str(tmp_path / "models" / "nope.sql")]) == [] + + +def test_unknown_path_errors_when_requested(tmp_path: Path) -> None: + """A path that is neither a known model nor a known test file must not pass silently.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + missing = tmp_path / "models" / "nope.sql" + with pytest.raises(SQLMeshError, match="is not a known model or test file"): + context.select_tests(tests=[str(missing)], raise_on_unknown_paths=True) + + with pytest.raises(SQLMeshError, match="is not a known model or test file"): + context.test(tests=[str(missing)], raise_on_unknown_paths=True) + + +def test_unknown_test_name_errors_when_requested(tmp_path: Path) -> None: + """A known YAML file with an unknown `::test_name` reports the test, not the file.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + test_path = tmp_path / "tests" / "test_full_model.yaml" + with pytest.raises(SQLMeshError, match=f"is not a known test in '{re.escape(str(test_path))}'"): + context.select_tests(tests=[f"{test_path}::nope"], raise_on_unknown_paths=True) + + +def test_unknown_test_name_in_unknown_file_reports_the_file(tmp_path: Path) -> None: + """A `::test_name` on a file that isn't a test file is a path problem, not a name one.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + missing = tmp_path / "tests" / "test_nope.yaml" + with pytest.raises(SQLMeshError, match="is not a known model or test file"): + context.select_tests(tests=[f"{missing}::nope"], raise_on_unknown_paths=True) + + +def test_select_model_still_filters_path_selection(tmp_path: Path) -> None: + """`--select-model` keeps narrowing the selection rather than adding to it.""" + init_example_project(tmp_path, engine_type="duckdb") + context = Context(paths=tmp_path) + + model_path = str(tmp_path / "models" / "full_model.sql") + assert ( + context.test(tests=[model_path], model_names=["sqlmesh_example.full_model"]).testsRun == 1 + ) + assert ( + context.test(tests=[model_path], model_names=["sqlmesh_example.incremental_model"]).testsRun + == 0 + ) + + def test_freeze_time_concurrent(tmp_path: Path) -> None: tests_dir = tmp_path / "tests" tests_dir.mkdir() From a6ba67dc87715ab74555db81cc992f8ea5037c78 Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Tue, 22 Sep 2026 02:35:06 +0100 Subject: [PATCH 59/67] fix(vscode): scope render tab assertions to the editor tab (#6076) Signed-off-by: Adegbite Ayoade Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- vscode/extension/tests/render.spec.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index db660daae1..0fba008834 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -38,7 +38,9 @@ test('Render works correctly', async ({ page, sharedCodeServer, tempDir }) => { // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window await expect(page.locator('text="marketing"."customer_id" AS')).toBeVisible() - await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible() + await expect( + page.getByRole('tab', { name: 'sushi.customers (rendered)' }), + ).toBeVisible() }) test('Render works correctly with model without a description', async ({ @@ -74,7 +76,9 @@ test('Render works correctly with model without a description', async ({ // Check if the model is rendered correctly await expect(page.locator('text="orders"."id" AS "id",')).toBeVisible() - await expect(page.locator('text=sushi.latest_order (rendered)')).toBeVisible() + await expect( + page.getByRole('tab', { name: 'sushi.latest_order (rendered)' }), + ).toBeVisible() }) test('Render works correctly with every rendered model opening a new tab', async ({ @@ -104,7 +108,9 @@ test('Render works correctly with every rendered model opening a new tab', async await runCommand(page, 'Render Model') // Check if the model is rendered correctly - await expect(page.locator('text=sushi.latest_order (rendered)')).toBeVisible() + await expect( + page.getByRole('tab', { name: 'sushi.latest_order (rendered)' }), + ).toBeVisible() // Open the customers model await page @@ -117,8 +123,12 @@ test('Render works correctly with every rendered model opening a new tab', async await runCommand(page, 'Render Model') // Assert both tabs exist - await expect(page.locator('text=sushi.latest_order (rendered)')).toBeVisible() - await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible() + await expect( + page.getByRole('tab', { name: 'sushi.latest_order (rendered)' }), + ).toBeVisible() + await expect( + page.getByRole('tab', { name: 'sushi.customers (rendered)' }), + ).toBeVisible() }) test('Render shows model picker when no active editor is open', async ({ @@ -146,7 +156,9 @@ test('Render shows model picker when no active editor is open', async ({ await page.locator('text=sushi.customers').click() // Verify the rendered model is shown - await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible({ + await expect( + page.getByRole('tab', { name: 'sushi.customers (rendered)' }), + ).toBeVisible({ timeout: 2_000, }) }) From df6e3a58d6707f9d5b3dfc05c1b6bf4b5f9d8f27 Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Thu, 24 Sep 2026 02:41:15 +0100 Subject: [PATCH 60/67] fix(vscode): stop the LSP restarting on unrelated configuration changes (#6057) Signed-off-by: Adegbite Ayoade --- vscode/extension/src/extension.ts | 57 ++++++- .../src/utilities/coalesceAsync.test.ts | 159 ++++++++++++++++++ .../extension/src/utilities/coalesceAsync.ts | 51 ++++++ .../common/configurationChange.test.ts | 60 +++++++ .../utilities/common/configurationChange.ts | 35 ++++ 5 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 vscode/extension/src/utilities/coalesceAsync.test.ts create mode 100644 vscode/extension/src/utilities/coalesceAsync.ts create mode 100644 vscode/extension/src/utilities/common/configurationChange.test.ts create mode 100644 vscode/extension/src/utilities/common/configurationChange.ts diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index cfea8c2228..db72414389 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -24,8 +24,10 @@ import { traceError, } from './utilities/common/log' import { onDidChangePythonInterpreter } from './utilities/common/python' +import { requiresLspRestart } from './utilities/common/configurationChange' +import { coalesceAsync } from './utilities/coalesceAsync' import { sleep } from './utilities/sleep' -import { handleError } from './utilities/errors' +import { ErrorType, handleError } from './utilities/errors' import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' @@ -65,7 +67,24 @@ export async function activate(context: vscode.ExtensionContext) { ), ) - const restartLsp = async (invokedByUser = false): Promise => { + /** + * Set when a restart was asked for explicitly, so that a user-invoked restart + * coalesced together with an automatic one is still treated as user-invoked. + */ + let restartInvokedByUser = false + + /** + * Set by a failed run and handled by the caller once the run has finished. + * Handling it inside the run would deadlock: the not_signed_in handler waits + * on a sign-in that restarts the client again, and that restart would wait on + * the run that is still waiting on the handler. + */ + let restartError: ErrorType | undefined + + const runRestart = async (): Promise => { + const invokedByUser = restartInvokedByUser + restartInvokedByUser = false + if (!lspClient) { lspClient = new LSPClient() } @@ -73,12 +92,7 @@ export async function activate(context: vscode.ExtensionContext) { traceVerbose('Restarting SQLMesh LSP client') const result = await lspClient.restart(invokedByUser) if (isErr(result)) { - await handleError( - authProvider, - restartLsp, - result.error, - 'LSP restart failed', - ) + restartError = result.error return } @@ -95,6 +109,25 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(testControllerDisposable) } + /** + * Restarts are serialized: a client disposing while the next one starts up + * leaves colliding command registrations and requests aimed at a disposed + * client, which is what surfaced as constant crashes. + */ + const restartLspSerialized = coalesceAsync(runRestart) + + const restartLsp = async (invokedByUser = false): Promise => { + restartInvokedByUser = restartInvokedByUser || invokedByUser + await restartLspSerialized() + + // Claimed so that callers coalesced into the same run don't each report it. + const error = restartError + restartError = undefined + if (error) { + await handleError(authProvider, restartLsp, error, 'LSP restart failed') + } + } + // commands needing the restart helper context.subscriptions.push( vscode.commands.registerCommand( @@ -191,7 +224,13 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( onDidChangePythonInterpreter(() => restartLsp()), - onDidChangeConfiguration(() => restartLsp()), + // Only restart for settings the server actually reads. This event fires for + // every setting in the editor, including ones written by other extensions. + onDidChangeConfiguration(event => { + if (requiresLspRestart(event)) { + void restartLsp() + } + }), ) if (!lspClient.hasCompletionCapability()) { diff --git a/vscode/extension/src/utilities/coalesceAsync.test.ts b/vscode/extension/src/utilities/coalesceAsync.test.ts new file mode 100644 index 0000000000..f40aba0aaa --- /dev/null +++ b/vscode/extension/src/utilities/coalesceAsync.test.ts @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest' +import { coalesceAsync } from './coalesceAsync' + +/** + * Let pending microtasks run. A rerun is scheduled off the promise of the run + * that precedes it, so it starts a few hops after that run settles. + */ +const flush = () => new Promise(resolve => setTimeout(resolve, 0)) + +/** A task that only settles when the test releases it. */ +function deferredTask() { + let release: (() => void) | undefined + let rejectWith: ((error: Error) => void) | undefined + let calls = 0 + + const run = () => { + calls += 1 + return new Promise((resolve, reject) => { + release = resolve + rejectWith = reject + }) + } + + return { + run, + get calls() { + return calls + }, + release: () => release?.(), + reject: (error: Error) => rejectWith?.(error), + } +} + +describe('coalesceAsync', () => { + it('runs the task immediately when idle', async () => { + const task = deferredTask() + const run = coalesceAsync(task.run) + + const first = run() + expect(task.calls).toBe(1) + + task.release() + await first + }) + + // A burst of triggers used to start a restart per event, which left clients + // disposing and starting concurrently. See #5642. + it('collapses every call made while running into a single rerun', async () => { + const task = deferredTask() + const run = coalesceAsync(task.run) + + const first = run() + expect(task.calls).toBe(1) + + const queued = [run(), run(), run(), run()] + expect(task.calls).toBe(1) + + task.release() + await first + await flush() + + // Exactly one rerun is scheduled, no matter how many calls arrived. + expect(task.calls).toBe(2) + + task.release() + await Promise.all(queued) + await flush() + expect(task.calls).toBe(2) + }) + + it('runs again for calls made after the previous run finished', async () => { + const task = deferredTask() + const run = coalesceAsync(task.run) + + const first = run() + task.release() + await first + expect(task.calls).toBe(1) + + const second = run() + expect(task.calls).toBe(2) + task.release() + await second + }) + + it('resolves the callers that were coalesced together', async () => { + const task = deferredTask() + const run = coalesceAsync(task.run) + + const first = run() + const queued = [run(), run()] + + task.release() + await first + // The rerun has to be in flight before it can be released. + await flush() + task.release() + + await expect(Promise.all(queued)).resolves.toEqual([undefined, undefined]) + }) + + it('surfaces a failure without wedging later calls', async () => { + const task = deferredTask() + const run = coalesceAsync(task.run) + + const first = run() + task.reject(new Error('restart failed')) + await expect(first).rejects.toThrow('restart failed') + + const second = run() + expect(task.calls).toBe(2) + task.release() + await second + }) + + // A failed restart is reported to the caller, which handles it after the run + // has finished. The not_signed_in handler signs the user in and restarts the + // client again, so that handler must not run inside the task: the restart it + // triggers would wait on the run that is still waiting on the handler, and + // neither would ever settle. See #5920. + it('lets a failure handler trigger another run without deadlocking', async () => { + let failNextRun = true + let reportedError: string | undefined + let runs = 0 + + const runRestart = async (): Promise => { + runs += 1 + // The real run awaits the client restart before it knows the outcome. + // Without that await the re-entrant call below lands in the synchronous + // prefix of the run, before it has been recorded as in flight, and the + // deadlock this covers cannot happen. + const failed = await Promise.resolve(failNextRun) + if (failed) { + failNextRun = false + reportedError = 'not_signed_in' + } + } + + const runRestartSerialized = coalesceAsync(runRestart) + + const restart = async (): Promise => { + await runRestartSerialized() + + const error = reportedError + reportedError = undefined + if (error === 'not_signed_in') { + // Stands in for the sign-in flow, which restarts once signed in. + await restart() + } + } + + await restart() + + expect(runs).toBe(2) + expect(reportedError).toBeUndefined() + }, 2_000) +}) diff --git a/vscode/extension/src/utilities/coalesceAsync.ts b/vscode/extension/src/utilities/coalesceAsync.ts new file mode 100644 index 0000000000..46259f9d79 --- /dev/null +++ b/vscode/extension/src/utilities/coalesceAsync.ts @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Serialize an async task so it never overlaps with itself, collapsing any + * calls that arrive while it is running into a single rerun. + * + * Restarting the language server means stopping a client and starting another. + * Letting two of those interleave leaves commands registered by the outgoing + * client colliding with the incoming one, and leaves requests addressed to a + * client that has already been disposed. Queueing one run per trigger would + * only spread the same problem out over time, so queued triggers collapse into + * one rerun: all the caller wants is for the task to have run after its + * request, not for it to run once per request. + * + * @param task The task to serialize. + * @returns A function that resolves once the task has run for that call. + */ +export function coalesceAsync(task: () => Promise): () => Promise { + let running: Promise | undefined + let queued: Promise | undefined + + const start = async (): Promise => { + try { + await task() + } finally { + running = undefined + } + } + + return (): Promise => { + if (!running) { + running = start() + return running + } + + // A rerun is already scheduled, so this call is satisfied by that one. + if (!queued) { + queued = running + // A failed run must not stop the rerun that was asked for; the caller + // waiting on the failed run is the one that sees the error. + .catch(() => undefined) + .then(() => { + queued = undefined + running = start() + return running + }) + } + + return queued + } +} diff --git a/vscode/extension/src/utilities/common/configurationChange.test.ts b/vscode/extension/src/utilities/common/configurationChange.test.ts new file mode 100644 index 0000000000..1a9c633ead --- /dev/null +++ b/vscode/extension/src/utilities/common/configurationChange.test.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest' +import { requiresLspRestart } from './configurationChange' + +/** + * Build a stand-in for `vscode.ConfigurationChangeEvent` from the settings that + * changed. VS Code reports a section as affected when the changed key is the + * section itself or sits underneath it. + */ +const changed = (...keys: string[]) => ({ + affectsConfiguration: (section: string) => + keys.some(key => key === section || key.startsWith(`${section}.`)), +}) + +describe('requiresLspRestart', () => { + it('restarts when a sqlmesh setting changes', () => { + expect(requiresLspRestart(changed('sqlmesh.projectPaths'))).toBe(true) + expect(requiresLspRestart(changed('sqlmesh.lspEntrypoint'))).toBe(true) + }) + + it('restarts when the python interpreter changes', () => { + expect(requiresLspRestart(changed('python.defaultInterpreterPath'))).toBe( + true, + ) + }) + + // The LSP used to restart on every configuration change in the editor, so + // anything that wrote a setting took the extension down with it. See #5920. + it('ignores settings the language server does not read', () => { + expect(requiresLspRestart(changed('editor.fontSize'))).toBe(false) + expect(requiresLspRestart(changed('workbench.colorTheme'))).toBe(false) + expect(requiresLspRestart(changed('files.autoSave'))).toBe(false) + }) + + // Running any python command in a VS Code terminal makes the Python + // extension touch its own terminal settings, which is what made the + // extension crash whenever sqlmesh was run in the terminal. See #5642. + it('ignores python settings unrelated to the interpreter', () => { + expect( + requiresLspRestart(changed('python.terminal.activateEnvironment')), + ).toBe(false) + expect( + requiresLspRestart(changed('python.analysis.typeCheckingMode')), + ).toBe(false) + expect(requiresLspRestart(changed('terminal.integrated.env.linux'))).toBe( + false, + ) + }) + + it('does not restart when nothing relevant changed', () => { + expect(requiresLspRestart(changed())).toBe(false) + }) + + it('restarts when a relevant setting changes alongside irrelevant ones', () => { + expect( + requiresLspRestart(changed('editor.fontSize', 'sqlmesh.projectPaths')), + ).toBe(true) + }) +}) diff --git a/vscode/extension/src/utilities/common/configurationChange.ts b/vscode/extension/src/utilities/common/configurationChange.ts new file mode 100644 index 0000000000..7e3ab1f3e0 --- /dev/null +++ b/vscode/extension/src/utilities/common/configurationChange.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Configuration sections the language server reads, so a change to any of them + * needs the server restarted to take effect. + * + * `sqlmesh` covers `sqlmesh.projectPaths` and `sqlmesh.lspEntrypoint`, both of + * which decide how the server is launched. The interpreter path matters because + * the server runs inside that interpreter. + */ +export const RESTART_CONFIGURATION_SECTIONS = [ + 'sqlmesh', + 'python.defaultInterpreterPath', +] + +/** + * The part of `vscode.ConfigurationChangeEvent` this module needs. Declared + * structurally so the check stays unit testable without the VS Code runtime. + */ +export interface ConfigurationChange { + affectsConfiguration(section: string): boolean +} + +/** + * Whether a configuration change affects a setting the language server reads. + * + * `workspace.onDidChangeConfiguration` fires for every setting in the editor, + * including ones written by other extensions, so the event has to be filtered + * before it triggers a restart. + */ +export function requiresLspRestart(event: ConfigurationChange): boolean { + return RESTART_CONFIGURATION_SECTIONS.some(section => + event.affectsConfiguration(section), + ) +} From 9bd35b774e2eb4775841ef5e37278f2b10900829 Mon Sep 17 00:00:00 2001 From: Adegbite Ayoade Abel Date: Thu, 24 Sep 2026 03:46:02 +0100 Subject: [PATCH 61/67] feat(cli): report state backend versions on rollback and info (#6088) Signed-off-by: Adegbite Ayoade --- sqlmesh/core/context.py | 36 +++++++- sqlmesh/core/state_sync/db/migrator.py | 9 ++ tests/cli/test_cli.py | 105 ++++++++++++++++++++++ tests/core/state_sync/test_state_sync.py | 32 +++++++ tests/integrations/jupyter/test_magics.py | 51 ++++++++--- 5 files changed, 217 insertions(+), 16 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 64dd6d67da..a6420f3ec1 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -108,6 +108,7 @@ CachingStateSync, StateReader, StateSync, + Versions, ) from sqlmesh.core.janitor import cleanup_expired_views, delete_expired_snapshots from sqlmesh.core.table_diff import TableDiff @@ -2610,8 +2611,10 @@ def migrate(self) -> None: """ self.notification_target_manager.notify(NotificationEvent.MIGRATION_START) self._load_materializations() + state_sync = self._new_state_sync() + previous_versions = self._state_versions(state_sync) try: - self._new_state_sync().migrate( + state_sync.migrate( promoted_snapshots_only=self.config.migration.promoted_snapshots_only, ) except Exception as e: @@ -2619,6 +2622,7 @@ def migrate(self) -> None: NotificationEvent.MIGRATION_FAILURE, traceback.format_exc() ) raise e + self._print_state_versions(self._state_versions(state_sync), previous_versions) self.notification_target_manager.notify(NotificationEvent.MIGRATION_END) @python_api_analytics @@ -2627,7 +2631,10 @@ def rollback(self) -> None: Please contact your SQLMesh administrator before doing this. This action cannot be undone. """ - self._new_state_sync().rollback() + state_sync = self._new_state_sync() + previous_versions = self._state_versions(state_sync) + state_sync.rollback() + self._print_state_versions(self._state_versions(state_sync), previous_versions) @python_api_analytics def create_external_models(self, strict: bool = False) -> None: @@ -2701,6 +2708,12 @@ def print_info( if state_connection: self._try_connection("state backend", state_connection.connection_validator()) + if verbosity >= Verbosity.VERBOSE: + try: + self._print_state_versions(self._state_versions()) + except Exception as ex: + self.console.log_error(f"Failed to fetch the state backend versions. {ex}") + @python_api_analytics def print_environment_names(self) -> None: """Prints all environment names along with expiry datetime.""" @@ -3290,6 +3303,25 @@ def _try_connection(self, connection_name: str, validator: t.Callable[[], None]) except Exception as ex: self.console.log_error(f"{connection_name} connection failed. {ex}") + def _state_versions(self, state_sync: t.Optional[StateSync] = None) -> Versions: + """Returns the versions recorded in the state backend without validating them.""" + return (state_sync or self._new_state_sync()).get_versions(validate=False) + + def _print_state_versions( + self, versions: Versions, previous_versions: t.Optional[Versions] = None + ) -> None: + """Prints the state backend versions, optionally alongside the ones they replaced.""" + self.console.log_status_update("\nState backend versions:") + for label, attribute in ( + ("Schema version", "schema_version"), + ("SQLGlot version", "sqlglot_version"), + ("SQLMesh version", "sqlmesh_version"), + ): + version = getattr(versions, attribute) + if previous_versions is not None: + version = f"{getattr(previous_versions, attribute)} -> {version}" + self.console.log_status_update(f"{label}: {version}") + def _new_state_sync(self) -> StateSync: return self._provided_state_sync or self._scheduler.create_state_sync(self) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 8d73e1d395..aaabeac5d9 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -96,6 +96,15 @@ def migrate( migrate_rows = self._apply_migrations(schema, skip_backup) if not migrate_rows and major_minor(SQLMESH_VERSION) == versions.minor_sqlmesh_version: + # Nothing to migrate, but a patch-level bump still leaves the recorded versions + # behind what is actually running, so they are brought up to date here. The + # schema version is carried over rather than defaulted, since no migration ran + # and moving it could hide one that is genuinely needed later. + if ( + versions.sqlmesh_version != SQLMESH_VERSION + or versions.sqlglot_version != SQLGLOT_VERSION + ): + self.version_state.update_versions(schema_version=versions.schema_version) return if migrate_rows: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 251ea54fa3..f1540727b1 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -10,11 +10,15 @@ from click import ClickException from click.testing import CliRunner +from sqlglot import __version__ as SQLGLOT_VERSION from sqlmesh import RuntimeEnv +from sqlmesh._version import __version__ as SQLMESH_VERSION from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.cli.main import cli from sqlmesh.core.context import Context +from sqlmesh.core.state_sync.base import SCHEMA_VERSION from sqlmesh.integrations.dlt import generate_dlt_models +from sqlmesh.utils import major_minor from sqlmesh.utils.date import now_ds, time_like_to_str, timedelta, to_datetime, yesterday_ds from sqlmesh.core.config.connection import DIALECT_TO_TYPE @@ -1024,6 +1028,107 @@ def test_info_on_new_project_does_not_create_state_sync(runner, tmp_path): assert not context.engine_adapter.table_exists("sqlmesh._versions") +def test_info_state_versions(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "info"]) + assert result.exit_code == 0 + assert "State backend versions" not in result.output + + result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "info", "-v"]) + assert result.exit_code == 0 + assert "State backend versions" in result.output + assert f"Schema version: {SCHEMA_VERSION}" in result.output + assert f"SQLGlot version: {SQLGLOT_VERSION}" in result.output + assert f"SQLMesh version: {SQLMESH_VERSION}" in result.output + + +def test_rollback_state_versions(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + context = Context(paths=tmp_path) + state_sync = context._new_state_sync() + # Back up the current state, then pretend the state was migrated by a newer SQLMesh. + state_sync.migrator._backup_state() + state_sync.version_state.update_versions( + schema_version=SCHEMA_VERSION + 1, + sqlglot_version="9999.0.0", + sqlmesh_version="9999.0.0", + ) + context.close() + + result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "rollback"]) + assert result.exit_code == 0 + assert "State backend versions" in result.output + assert f"Schema version: {SCHEMA_VERSION + 1} -> {SCHEMA_VERSION}" in result.output + assert f"SQLGlot version: 9999.0.0 -> {SQLGLOT_VERSION}" in result.output + assert f"SQLMesh version: 9999.0.0 -> {SQLMESH_VERSION}" in result.output + + +def test_migrate_state_versions(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + context = Context(paths=tmp_path) + # Pretend the state was written by an older patch release of the same minor version, which + # is the case `migrate` used to leave untouched. + major, minor = major_minor(SQLMESH_VERSION) + older_sqlmesh = f"{major}.{minor}.dev0" + context._new_state_sync().version_state.update_versions( + sqlglot_version="0.0.1", + sqlmesh_version=older_sqlmesh, + ) + context.close() + + result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "migrate"]) + assert result.exit_code == 0 + assert "State backend versions" in result.output + assert f"SQLGlot version: 0.0.1 -> {SQLGLOT_VERSION}" in result.output + assert f"SQLMesh version: {older_sqlmesh} -> {SQLMESH_VERSION}" in result.output + + +def test_migrate_updates_versions_after_a_patch_bump(runner, tmp_path): + """A patch bump leaves the minor version equal, but the recorded versions must still move. + + Both minor versions have to match the installed ones, otherwise `_apply_migrations` reports + rows to migrate and the early return this covers is never reached. + """ + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + sqlmesh_major, sqlmesh_minor = major_minor(SQLMESH_VERSION) + sqlglot_major, sqlglot_minor = major_minor(SQLGLOT_VERSION) + context = Context(paths=tmp_path) + context._new_state_sync().version_state.update_versions( + sqlglot_version=f"{sqlglot_major}.{sqlglot_minor}.dev0", + sqlmesh_version=f"{sqlmesh_major}.{sqlmesh_minor}.dev0", + ) + context.close() + + assert ( + runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "migrate"]).exit_code + == 0 + ) + + context = Context(paths=tmp_path) + versions = context._new_state_sync().get_versions(validate=False) + context.close() + assert versions.sqlmesh_version == SQLMESH_VERSION + assert versions.sqlglot_version == SQLGLOT_VERSION + + +def test_rollback_without_backup_does_not_print_state_versions(runner, tmp_path): + create_example_project(tmp_path) + init_prod_and_backfill(runner, tmp_path) + + result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "rollback"]) + assert result.exit_code == 1 + assert "There are no prior migrations to roll back to." in result.output + assert "State backend versions" not in result.output + + def test_dlt_pipeline_errors(runner, tmp_path): # Error if no pipeline is provided result = runner.invoke(cli, ["--paths", tmp_path, "init", "-t", "dlt", "duckdb"]) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 562afc6ac7..32a359f667 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -4254,3 +4254,35 @@ def test_state_version_is_too_old( match="The current state belongs to an old version of SQLMesh that is no longer supported. Please upgrade to 0.134.0 first before upgrading to.*", ): state_sync.migrate(skip_backup=True) + + +def test_migrate_patch_bump_preserves_schema_version( + state_sync: EngineAdapterStateSync, mocker: MockerFixture +) -> None: + """A run with nothing to migrate must not move the recorded schema version. + + `_apply_migrations` is forced to report no rows so the patch-bump branch is reached with a + schema version that differs from the current one. A real state can't get into that shape, + which is the point: the schema version is carried over rather than defaulted so that a + migration which is genuinely still needed can't be masked. + """ + from sqlmesh import __version__ as SQLMESH_VERSION + from sqlmesh.core.state_sync.base import SCHEMA_VERSION + + stale_schema_version = SCHEMA_VERSION - 1 + state_sync.version_state.update_versions( + schema_version=stale_schema_version, + sqlglot_version="0.0.1", + sqlmesh_version=SQLMESH_VERSION, + ) + + mocker.patch( + "sqlmesh.core.state_sync.db.migrator.StateMigrator._apply_migrations", + return_value=False, + ) + + state_sync.migrate() + + versions = state_sync.get_versions(validate=False) + assert versions.schema_version == stale_schema_version + assert versions.sqlglot_version == SQLGLOT_VERSION diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index c849dcbfc7..8449a64b69 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -12,8 +12,11 @@ from IPython.utils.capture import CapturedIO, capture_output from pytest_mock.plugin import MockerFixture from rich.console import Console as RichConsole +from sqlglot import __version__ as SQLGLOT_VERSION from sqlmesh import Context, RuntimeEnv +from sqlmesh._version import __version__ as SQLMESH_VERSION +from sqlmesh.core.state_sync.base import SCHEMA_VERSION, Versions from sqlmesh.magics import register_magics from pathlib import Path @@ -740,7 +743,8 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ assert not output.stdout assert not output.stderr - assert len(output.outputs) == 6 + assert len(output.outputs) == 10 + # No plan has been applied, so the state backend is still empty and reports the defaults. assert convert_all_html_output_to_text(output) == [ "Models: 20", "Macros: 8", @@ -748,6 +752,10 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ "Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []\n shared_connection: true", "Test Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []\n shared_connection: true", "Data warehouse connection succeeded", + "State backend versions:", + "Schema version: 0", + "SQLGlot version: 0.0.0", + "SQLMesh version: 0.0.0", ] assert get_all_html_output(output) == [ "
Models: 20
", @@ -756,6 +764,10 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ '
Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None  filesystems: []  shared_connection: true
', '
Test Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None  filesystems: []  shared_connection: true
', "
Data warehouse connection succeeded
", + "
State backend versions:
", + "
Schema version: 0
", + '
SQLGlot version: 0.0.0
', + '
SQLMesh version: 0.0.0
', ] @@ -768,25 +780,36 @@ def test_migrate( assert not output.stdout assert not output.stderr - assert len(output.outputs) == 1 + assert len(output.outputs) == 5 + # The sushi state lives in an in-memory DuckDB database, so the state sync that `migrate` + # opens starts empty and the versions move from the defaults to the running ones. + empty = Versions() assert convert_all_html_output_to_text(output) == [ + "State backend versions:", + f"Schema version: {empty.schema_version} -> {SCHEMA_VERSION}", + f"SQLGlot version: {empty.sqlglot_version} -> {SQLGLOT_VERSION}", + f"SQLMesh version: {empty.sqlmesh_version} -> {SQLMESH_VERSION}", "Migration complete", ] - assert get_all_html_output(output) == [ - str( + # Rich highlights the numbers inside the version lines, and that markup depends on the + # running versions, so only the fixed lines are compared as HTML. + html_output = get_all_html_output(output) + assert html_output[0] == str( + h("pre", {"style": RICH_PRE_STYLE}, "State backend versions:", autoescape=False) + ) + assert html_output[-1] == str( + h( + "pre", + {"style": RICH_PRE_STYLE}, h( - "pre", - {"style": RICH_PRE_STYLE}, - h( - "span", - {"style": SUCCESS_STYLE}, - "Migration complete", - autoescape=False, - ), + "span", + {"style": SUCCESS_STYLE}, + "Migration complete", autoescape=False, - ) + ), + autoescape=False, ) - ] + ) # TODO: Add test for rollback From 11d2e628d39eed354c8869869509b146969712d2 Mon Sep 17 00:00:00 2001 From: mday-io Date: Thu, 24 Sep 2026 16:19:33 +0000 Subject: [PATCH 62/67] fix(format): render header list properties as dialect-native arrays Dialect-agnostic header properties that hold a list (`tags`, `ignored_rules`) were rendered by the base generator as `ARRAY(...)`. On BigQuery `ARRAY(` is a subquery constructor, so a multi-element `tags ['a', 'b']` reformatted to `ARRAY('a', 'b')` failed to reload. Render these as `[...]` when the model dialect itself spells arrays with brackets; dialects that use `[`/`]` for identifier quoting (tsql, sqlite, ...) keep `ARRAY(...)`. Elements stay dialect-agnostic. Also give `grain` and `table_properties` the render policy of `grains` / `physical_properties`: they are renamed in `ModelMeta._pre_root_validator` rather than via Pydantic aliases, so reflection missed them and they lost dialect-specific rendering. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 55 +++++++++++++ tests/core/test_dialect.py | 161 +++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index ecd43a566f..d9c2bea938 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -804,9 +804,44 @@ def _meta_render_policy() -> t.Dict[str, bool]: for source in sources: for name, field in source.model_fields.items(): policy.setdefault((field.alias or name).lower(), _holds_expression(field.annotation)) + + # `ModelMeta._pre_root_validator` (sqlmesh/core/model/meta.py) renames these two + # user-facing property names to their target field before Pydantic validation, so + # they never surface as a `Field(alias=...)` for the reflection above to find. Give + # each the render policy of the field it is renamed to. + pre_validator_aliases = { + "grain": "grains", + "table_properties": "physical_properties", + } + for alias, target in pre_validator_aliases.items(): + if target in policy: + policy[alias] = policy[target] + return policy +@functools.lru_cache(maxsize=None) +def _dialect_renders_array_as_brackets(dialect_name: t.Optional[str]) -> bool: + """Whether `dialect_name`'s own generator spells an array literal as `[a, b]`. + + Checked by actually rendering a sample `exp.Array` with that dialect, rather than + inspecting `Dialect.ARRAY_SIZE_NAME` or similar generator flags, because the + generator is the single source of truth for what a dialect's array syntax looks + like and there is no single shared flag for it across dialects. This also covers + dialects (tsql, sqlite, tableau, exasol, fabric) that reuse `[`/`]` for identifier + quoting and therefore render arrays as `ARRAY(...)` instead: rewriting their + `tags`/`ignored_rules` value to `[a, b]` would not be an array literal in their + grammar at all, so it silently reparses as one bracket-quoted identifier and + corrupts the value. An unrecognized dialect name renders with the generic + generator, which itself does not use brackets, so it falls back to `False`. + """ + try: + sample = exp.Array(expressions=[exp.Literal.string("x")]) + return sample.sql(dialect=dialect_name).startswith("[") + except Exception: + return False + + def _props_sql(self: Generator, expressions: t.List[exp.Expr]) -> str: props = [] size = len(expressions) @@ -855,6 +890,26 @@ def render_with_model_dialect(node: exp.Expr, **overrides: t.Any) -> str: and _meta_render_policy().get(prop.name.lower()) ): value_sql = render_with_model_dialect(value) + elif ( + meta_dialect + and isinstance(value, exp.Array) + and _dialect_renders_array_as_brackets(meta_dialect) + ): + # Dialect-agnostic properties (e.g. `tags`, `ignored_rules`) that hold a + # list still go through the base (dialect=None) generator, which renders + # an `exp.Array` as `ARRAY(...)`. On BigQuery `ARRAY(` is parsed as a + # subquery constructor, so a multi-element `ARRAY('a', 'b')` fails to + # reparse ("Required keyword: 'value' missing for Property"). Render it + # as a bracketed list literal instead -- but only for dialects that + # actually spell arrays that way; dialects that reuse `[`/`]` for + # identifier quoting (tsql, sqlite, ...) keep the generic `ARRAY(...)` + # form, which they parse back correctly. The elements themselves stay on + # the dialect-agnostic path (`self.expressions`, not + # `render_with_model_dialect`): these are SQLMesh's own scalar values + # (tag/rule name strings), not user warehouse SQL, so they must not be + # transpiled with the model dialect (e.g. tsql boolean literals turning + # into `(1 = 1)`). + value_sql = f"[{self.expressions(value, flat=True)}]" else: value_sql = self.sql(prop, "value") diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index c57ed7e8c4..23c903a66d 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -617,6 +617,167 @@ def test_format_model_expressions_macro_property_comments_preserved_with_dialect assert formatted == twice +@pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake"]) +@pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"]) +def test_format_model_expressions_list_property_array_literal(dialect: str, prop_name: str): + """Dialect-agnostic header properties that hold a list (`tags`, `ignored_rules`) must + render as a bracketed list literal (`[a, b]`) on dialects that spell arrays that way, + not the base generator's `ARRAY(a, b)`. + + On BigQuery, `ARRAY(` is parsed as a subquery constructor, so a multi-element + `ARRAY('C1', 'c2')` fails to reparse with `Required keyword: 'value' missing for + Property`. This previously affected any dialect using this generator, since these + properties render generically (the `dialect=None` path) regardless of the model's + own dialect. Only bigquery/duckdb/snowflake-like dialects are covered here; + dialects whose own array syntax is not brackets (postgres' `ARRAY[...]`, + databricks' `ARRAY(...)`) or that reuse `[`/`]` for identifier quoting (tsql, + sqlite, ...) are covered by + `test_format_model_expressions_list_property_dialects_without_bracket_arrays`. + """ + source = f"""MODEL ( + name a.b, + dialect {dialect}, + {prop_name} ['C1', 'c2'] +); +SELECT 1 AS x""" + + formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) + + assert f"{prop_name} ['C1', 'c2']" in formatted + + # Reparses cleanly with the model's own dialect. + reparsed = parse(formatted, default_dialect=dialect) + + # Idempotent: formatting an already-formatted model is a no-op. + twice = format_model_expressions(reparsed, dialect=dialect) + assert formatted == twice + + model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) + if prop_name == "tags": + assert model.tags == ["C1", "c2"] + else: + assert model.ignored_rules == {"c1", "c2"} + + +def test_format_model_expressions_array_property_no_dialect_unchanged(): + """Regression guard: with no model dialect, list-valued header properties must keep + rendering through the base generator (`ARRAY(...)`), exactly as pinned by + `test_format_model_expressions`. The `[...]` rewrite only applies once a model + dialect is present (gated on `meta_dialect`).""" + formatted = format_model_expressions( + parse("MODEL (name a.b, tags ['C1', 'c2']); SELECT 1 AS x") + ) + + assert "tags ARRAY('C1', 'c2')" in formatted + + +@pytest.mark.parametrize("dialect", ["tsql", "sqlite", "postgres", "databricks"]) +@pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"]) +def test_format_model_expressions_list_property_dialects_without_bracket_arrays( + dialect: str, prop_name: str +): + """Regression: dialects whose own generator does not spell an `exp.Array` as + `[a, b]` must NOT get the bracket-list rewrite from + `test_format_model_expressions_list_property_array_literal`, and must keep the + generic `ARRAY(...)` form. + + This matters most for tsql and sqlite (also true of tableau, exasol, fabric), which + reuse `[`/`]` for identifier quoting: `['a', 'b']` is not an array literal in their + grammar at all, so rewriting `tags` or `ignored_rules` to that form reparses as a + single bracket-quoted identifier, silently collapsing two values into one and + corrupting the tag/rule names -- even though this exact source formatted correctly + on `main` before bracket rendering was introduced. postgres (`ARRAY[...]`) and + databricks (`ARRAY(...)`) are not corrupted by the bracket form, but should still + keep rendering with their own generator's spelling rather than a generic bracket + literal that is not how either dialect writes arrays. + """ + source = f"""MODEL ( + name a.b, + dialect {dialect}, + {prop_name} ARRAY('C1', 'c2') +); +SELECT 1 AS x""" + + formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) + + prop_line = formatted.split(f"{prop_name} ")[1].split("\n")[0] + assert prop_line.startswith("ARRAY") + assert "[" not in prop_line + + reparsed = parse(formatted, default_dialect=dialect) + twice = format_model_expressions(reparsed, dialect=dialect) + assert formatted == twice + + model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) + if prop_name == "tags": + assert model.tags == ["C1", "c2"] + else: + assert model.ignored_rules == {"c1", "c2"} + + +@pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake", "postgres"]) +def test_format_model_expressions_grain_alias_render_policy(dialect: str): + """`grain` is renamed to `grains` in `ModelMeta._pre_root_validator`, not via a + Pydantic alias, so `_meta_render_policy` must special-case it to inherit `grains`' + render policy (warehouse SQL). Otherwise a multi-column `grain [id, id2]` falls back + to the generic, dialect-agnostic path and (via the base generator) becomes + `ARRAY(id, id2)`, which fails to reparse on BigQuery. + """ + source = f"""MODEL ( + name a.b, + dialect {dialect}, + grain [id, id2] +); +SELECT 1 AS x, 2 AS id, 3 AS id2""" + + formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) + + # Rendered with the model's own dialect (e.g. postgres' native `ARRAY[...]`), never + # the base generator's `ARRAY(id, id2)`, which fails to reparse on BigQuery. + assert "ARRAY(id, id2)" not in formatted + + twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect) + assert formatted == twice + + # `grain [id, id2]` parses to a single composite grain wrapping both columns + # (independent of this fix); what matters here is that it survives a dialect- + # specific round trip rather than being flattened to the generic `ARRAY(...)`. + model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) + assert len(model.grains) == 1 + assert {c.name for c in model.grains[0].find_all(exp.Column)} == {"id", "id2"} + + +def test_format_model_expressions_table_properties_alias_render_policy(): + """`table_properties` is the deprecated alias for `physical_properties`, renamed in + `ModelMeta._pre_root_validator`, not via a Pydantic alias. It must inherit + `physical_properties`' render policy (warehouse SQL) so dialect-specific values + inside it, such as tsql's `DATETIME2`, are not flattened to the generic generator's + `TIMESTAMP` spelling. + """ + formatted = format_model_expressions( + parse( + """ + MODEL ( + name a.b, + dialect tsql, + table_properties ( + x = CAST('2024-01-01' AS DATETIME2) + ) + ); + + SELECT 1 AS x + """, + default_dialect="tsql", + ), + dialect="tsql", + ) + + assert "x = '2024-01-01'::DATETIME2" in formatted + + twice = format_model_expressions(parse(formatted, default_dialect="tsql"), dialect="tsql") + assert formatted == twice + + def test_format_model_expressions_normalize_functions(): """Regression: formatter function-name casing behavior. From f7de34b3af1b20e8bd6ba53e9d3547a06e8072f2 Mon Sep 17 00:00:00 2001 From: mday-io Date: Thu, 24 Sep 2026 16:36:38 +0000 Subject: [PATCH 63/67] test(format): cover `sqlmesh format` on BigQuery header lists Add a Context-level test that formats a BigQuery model with `tags`, `ignored_rules`, `grain` and `partitioned_by`, then reloads it. Trim the header-list unit tests and drop the postgres/databricks cases, which passed with or without the fix. Signed-off-by: mday-io --- tests/core/test_dialect.py | 79 +++++++------------------------------- tests/core/test_format.py | 34 ++++++++++++++++ 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 23c903a66d..d2c4358cb0 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -620,20 +620,8 @@ def test_format_model_expressions_macro_property_comments_preserved_with_dialect @pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake"]) @pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"]) def test_format_model_expressions_list_property_array_literal(dialect: str, prop_name: str): - """Dialect-agnostic header properties that hold a list (`tags`, `ignored_rules`) must - render as a bracketed list literal (`[a, b]`) on dialects that spell arrays that way, - not the base generator's `ARRAY(a, b)`. - - On BigQuery, `ARRAY(` is parsed as a subquery constructor, so a multi-element - `ARRAY('C1', 'c2')` fails to reparse with `Required keyword: 'value' missing for - Property`. This previously affected any dialect using this generator, since these - properties render generically (the `dialect=None` path) regardless of the model's - own dialect. Only bigquery/duckdb/snowflake-like dialects are covered here; - dialects whose own array syntax is not brackets (postgres' `ARRAY[...]`, - databricks' `ARRAY(...)`) or that reuse `[`/`]` for identifier quoting (tsql, - sqlite, ...) are covered by - `test_format_model_expressions_list_property_dialects_without_bracket_arrays`. - """ + """List-valued header properties render as `[a, b]`, not `ARRAY(a, b)`, which + BigQuery parses as a subquery and fails to load.""" source = f"""MODEL ( name a.b, dialect {dialect}, @@ -642,14 +630,9 @@ def test_format_model_expressions_list_property_array_literal(dialect: str, prop SELECT 1 AS x""" formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) - assert f"{prop_name} ['C1', 'c2']" in formatted - # Reparses cleanly with the model's own dialect. - reparsed = parse(formatted, default_dialect=dialect) - - # Idempotent: formatting an already-formatted model is a no-op. - twice = format_model_expressions(reparsed, dialect=dialect) + twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect) assert formatted == twice model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) @@ -660,10 +643,6 @@ def test_format_model_expressions_list_property_array_literal(dialect: str, prop def test_format_model_expressions_array_property_no_dialect_unchanged(): - """Regression guard: with no model dialect, list-valued header properties must keep - rendering through the base generator (`ARRAY(...)`), exactly as pinned by - `test_format_model_expressions`. The `[...]` rewrite only applies once a model - dialect is present (gated on `meta_dialect`).""" formatted = format_model_expressions( parse("MODEL (name a.b, tags ['C1', 'c2']); SELECT 1 AS x") ) @@ -671,26 +650,13 @@ def test_format_model_expressions_array_property_no_dialect_unchanged(): assert "tags ARRAY('C1', 'c2')" in formatted -@pytest.mark.parametrize("dialect", ["tsql", "sqlite", "postgres", "databricks"]) +@pytest.mark.parametrize("dialect", ["tsql", "sqlite"]) @pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"]) -def test_format_model_expressions_list_property_dialects_without_bracket_arrays( +def test_format_model_expressions_list_property_bracket_identifier_dialects( dialect: str, prop_name: str ): - """Regression: dialects whose own generator does not spell an `exp.Array` as - `[a, b]` must NOT get the bracket-list rewrite from - `test_format_model_expressions_list_property_array_literal`, and must keep the - generic `ARRAY(...)` form. - - This matters most for tsql and sqlite (also true of tableau, exasol, fabric), which - reuse `[`/`]` for identifier quoting: `['a', 'b']` is not an array literal in their - grammar at all, so rewriting `tags` or `ignored_rules` to that form reparses as a - single bracket-quoted identifier, silently collapsing two values into one and - corrupting the tag/rule names -- even though this exact source formatted correctly - on `main` before bracket rendering was introduced. postgres (`ARRAY[...]`) and - databricks (`ARRAY(...)`) are not corrupted by the bracket form, but should still - keep rendering with their own generator's spelling rather than a generic bracket - literal that is not how either dialect writes arrays. - """ + """These dialects quote identifiers with `[...]`, so a bracketed list would reload + as a single identifier. List properties must keep the `ARRAY(...)` form.""" source = f"""MODEL ( name a.b, dialect {dialect}, @@ -699,13 +665,9 @@ def test_format_model_expressions_list_property_dialects_without_bracket_arrays( SELECT 1 AS x""" formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) + assert f"{prop_name} ARRAY('C1', 'c2')" in formatted - prop_line = formatted.split(f"{prop_name} ")[1].split("\n")[0] - assert prop_line.startswith("ARRAY") - assert "[" not in prop_line - - reparsed = parse(formatted, default_dialect=dialect) - twice = format_model_expressions(reparsed, dialect=dialect) + twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect) assert formatted == twice model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) @@ -717,12 +679,8 @@ def test_format_model_expressions_list_property_dialects_without_bracket_arrays( @pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake", "postgres"]) def test_format_model_expressions_grain_alias_render_policy(dialect: str): - """`grain` is renamed to `grains` in `ModelMeta._pre_root_validator`, not via a - Pydantic alias, so `_meta_render_policy` must special-case it to inherit `grains`' - render policy (warehouse SQL). Otherwise a multi-column `grain [id, id2]` falls back - to the generic, dialect-agnostic path and (via the base generator) becomes - `ARRAY(id, id2)`, which fails to reparse on BigQuery. - """ + """`grain` is renamed to `grains` before validation, so it must share the + `grains` render policy instead of falling back to `ARRAY(...)`.""" source = f"""MODEL ( name a.b, dialect {dialect}, @@ -731,29 +689,18 @@ def test_format_model_expressions_grain_alias_render_policy(dialect: str): SELECT 1 AS x, 2 AS id, 3 AS id2""" formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect) - - # Rendered with the model's own dialect (e.g. postgres' native `ARRAY[...]`), never - # the base generator's `ARRAY(id, id2)`, which fails to reparse on BigQuery. assert "ARRAY(id, id2)" not in formatted twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect) assert formatted == twice - # `grain [id, id2]` parses to a single composite grain wrapping both columns - # (independent of this fix); what matters here is that it survives a dialect- - # specific round trip rather than being flattened to the generic `ARRAY(...)`. model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) - assert len(model.grains) == 1 assert {c.name for c in model.grains[0].find_all(exp.Column)} == {"id", "id2"} def test_format_model_expressions_table_properties_alias_render_policy(): - """`table_properties` is the deprecated alias for `physical_properties`, renamed in - `ModelMeta._pre_root_validator`, not via a Pydantic alias. It must inherit - `physical_properties`' render policy (warehouse SQL) so dialect-specific values - inside it, such as tsql's `DATETIME2`, are not flattened to the generic generator's - `TIMESTAMP` spelling. - """ + """`table_properties` is renamed to `physical_properties` before validation, so it + must keep dialect-specific types such as tsql's `DATETIME2`.""" formatted = format_model_expressions( parse( """ diff --git a/tests/core/test_format.py b/tests/core/test_format.py index 5a44e1b381..481c5c65b5 100644 --- a/tests/core/test_format.py +++ b/tests/core/test_format.py @@ -161,3 +161,37 @@ def test_format_without_state_load(tmp_path: pathlib.Path, mocker: MockerFixture context = Context(paths=tmp_path, config=Config(project="local_only"), load_state=False) context.format(check=True) mock.assert_not_called() + + +def test_format_bigquery_header_list_properties(tmp_path: pathlib.Path): + # A BigQuery model must survive `sqlmesh format` and still load: list-valued header + # properties were rewritten to `ARRAY(...)`, which BigQuery parses as a subquery. + model_file = create_temp_file( + tmp_path, + pathlib.Path("models/model.sql"), + """MODEL ( + name test.model, + kind INCREMENTAL_BY_TIME_RANGE (time_column ds), + tags ['C1', 'c2'], + ignored_rules ['noselectstar', 'ambiguousorinvalidcolumn'], + grain [id], + partitioned_by DATE_TRUNC(ds, MONTH) +); +SELECT 1 AS id, CURRENT_DATE() AS ds""", + ) + config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) + + Context(paths=tmp_path, config=config).format() + + formatted = model_file.read_text(encoding="utf-8") + assert "tags ['C1', 'c2']" in formatted + assert "ignored_rules ['noselectstar', 'ambiguousorinvalidcolumn']" in formatted + assert "grain [id]" in formatted + assert "partitioned_by DATE_TRUNC(ds, MONTH)" in formatted + + context = Context(paths=tmp_path, config=config) + assert context.format(check=True) + model = context.get_model("test.model") + assert model.tags == ["C1", "c2"] + assert model.ignored_rules == {"noselectstar", "ambiguousorinvalidcolumn"} + assert [p.sql("bigquery") for p in model.partitioned_by] == ["DATE_TRUNC(`ds`, MONTH)"] From 1e0c8594c8883daf0ce4ca8e1fc42eadc4f9bbcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:48:12 -0400 Subject: [PATCH 64/67] Chore(deps): Bump @swc/core-linux-x64-gnu from 1.13.2 to 1.16.2 (#6080) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 14 ++++++++++++-- web/client/package.json | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31b664c053..ec077396c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,8 +394,8 @@ importers: version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0(supports-color@9.4.0))(lightningcss@1.30.1)(supports-color@9.4.0)(terser@5.44.0)(tsx@4.20.3)(yaml@2.9.0) optionalDependencies: '@swc/core-linux-x64-gnu': - specifier: ^1.13.2 - version: 1.13.2 + specifier: ^1.16.2 + version: 1.16.2 web/common: devDependencies: @@ -2208,6 +2208,13 @@ packages: os: [linux] libc: [glibc] + '@swc/core-linux-x64-gnu@1.16.2': + resolution: {integrity: sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@swc/core-linux-x64-musl@1.13.2': resolution: {integrity: sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==} engines: {node: '>=10'} @@ -8974,6 +8981,9 @@ snapshots: '@swc/core-linux-x64-gnu@1.13.2': optional: true + '@swc/core-linux-x64-gnu@1.16.2': + optional: true + '@swc/core-linux-x64-musl@1.13.2': optional: true diff --git a/web/client/package.json b/web/client/package.json index b6c99153f9..23305fc039 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -75,6 +75,6 @@ "vitest": "^3.2.4" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "^1.13.2" + "@swc/core-linux-x64-gnu": "^1.16.2" } } From 68da7f440b42bb6dc456ea74f1f2ad4117484133 Mon Sep 17 00:00:00 2001 From: mday-io Date: Fri, 25 Sep 2026 14:49:18 +0000 Subject: [PATCH 65/67] fix(format): keep boolean literals stable in dialect-rendered header values On tsql, a boolean inside a header value rendered with the model dialect, such as `audits (my_audit(flag := true))`, was written as `(1 = 1)` and then wrapped across lines on the next run, so `sqlmesh format --check` flagged an already-formatted model. Keep boolean literals as `TRUE`/`FALSE` there, as before, since the value is transpiled with the model dialect when it is used. Also add a regression test that repeated formatting keeps a model's declared column types (BigQuery `DATETIME`, tsql `DATETIME2`), and merge the `time_column` kind test into the equivalent scalar-sibling test. Signed-off-by: mday-io --- sqlmesh/core/dialect.py | 14 ++++++- tests/core/test_dialect.py | 81 +++++++++++++++----------------------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index d9c2bea938..bcfb300ba1 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -864,7 +864,19 @@ def render_with_model_dialect(node: exp.Expr, **overrides: t.Any) -> str: "comments": self.comments, } opts.update(overrides) - return node.sql(**opts) + + # Keep boolean literals anywhere in the value (audit args, physical_properties, + # merge_filter, ...) as `TRUE`/`FALSE`: tsql would otherwise emit `(1 = 1)`, + # which reformats differently on the next pass. The value is transpiled with + # the model dialect anyway when it is used, e.g. in the rendered audit query. + def keep_boolean_literal(n: exp.Expr) -> exp.Expr: + if not isinstance(n, exp.Boolean): + return n + literal = exp.var("TRUE" if n.this else "FALSE") + literal.comments = n.comments + return literal + + return node.transform(keep_boolean_literal).sql(**opts) if isinstance(prop, MacroFunc): # A macro in property position wraps user-authored arguments, so it carries diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index d2c4358cb0..dc55571f21 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -422,8 +422,8 @@ def test_format_model_expressions_meta_render_policy(dialect: str, audit_type: s @pytest.mark.parametrize( "header", [ - "columns (ts DATETIME2(6))", "audits (my_audit(t := CAST('2024-01-01' AS DATETIME2)))", + "audits (my_audit(flag := true))", "kind SCD_TYPE_2_BY_COLUMN(unique_key id, columns (a, b), time_data_type DATETIME2(6))", "physical_properties (labels = (('env', 'prod')))", "allow_partials true, description 'my description'", @@ -447,6 +447,28 @@ def test_format_model_expressions_is_idempotent(header: str): assert once == twice +@pytest.mark.parametrize( + "dialect,column_type", + [("bigquery", "DATETIME"), ("tsql", "DATETIME2(6)")], +) +def test_format_model_expressions_preserves_column_types(dialect: str, column_type: str): + """Repeated `sqlmesh format` runs must not change a model's declared column types. + + Rendering `columns` with the generic generator rewrote them: BigQuery `DATETIME` + became `TIMESTAMP` and then `TIMESTAMPTZ`, tsql `DATETIME2` became `TIMESTAMP` and + then `VARBINARY`. + """ + expected = exp.DataType.build(column_type, dialect=dialect) + formatted = f"MODEL (name a.b, dialect {dialect}, columns (ts {column_type}));\nSELECT 1 AS ts" + + for _ in range(2): + formatted = format_model_expressions( + parse(formatted, default_dialect=dialect), dialect=dialect + ) + model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect) + assert model.columns_to_types == {"ts": expected} + + def test_format_audit_expressions_meta_render_policy(): """AUDIT headers have their own meta model, and get the same split: `blocking` is SQLMesh's own boolean and must not become tsql's `(1 = 0)`, while `defaults` holds @@ -473,60 +495,21 @@ def test_format_audit_expressions_meta_render_policy(): assert "cutoff := '2024-01-01'::DATETIME2" in formatted -def test_format_model_expressions_time_column_dialect(): - """`time_column` is a nested Pydantic model (`TimeColumn`) wrapping an expression, not +def test_format_model_expressions_kind_time_column_dialect(): + """Expression-bearing properties nested inside `kind` render with the model dialect, + while their scalar siblings stay dialect-agnostic. + + `time_column` is a nested Pydantic model (`TimeColumn`) wrapping an expression, not an `exp.Expr` annotation itself, so the render-policy reflection must recurse into nested Pydantic models to classify it as warehouse SQL. Otherwise it falls back to generic rendering and loses dialect-specific identifier quoting: tsql's `[end]` becomes ANSI `"end"`, even though the same identifier in the query body is correctly kept as `[end]`. - """ - formatted = format_model_expressions( - parse( - """ - MODEL ( - name a.b, - dialect tsql, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column [end] - ) - ); - - SELECT 1 AS x, [end] FROM t - """, - default_dialect="tsql", - ), - dialect="tsql", - ) - - assert ( - formatted - == """MODEL ( - name a.b, - dialect tsql, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column [end] - ) -); - -SELECT - 1 AS x, - [end] -FROM t""" - ) - - -def test_format_model_expressions_kind_scalar_sibling_dialect(): - """A scalar sibling property of an expression-bearing property inside `kind` (e.g. - `forward_only` next to `time_column`) must stay dialect-agnostic even though the - render policy correctly marks `kind` as containing an expression-holding field - somewhere in the `ModelKind` union. - Regression: recursing into nested Pydantic models to fix `time_column` (see - `test_format_model_expressions_time_column_dialect`) made `_holds_expression` also - match on `kind` itself, since *some* member of the `ModelKind` union - (`IncrementalByTimeRangeKind.time_column`) holds an expression. That routed the - entire `kind (...)` subtree through a dialect-specific generator, so tsql's + Regression: recursing into nested Pydantic models to fix `time_column` made + `_holds_expression` also match on `kind` itself, since *some* member of the + `ModelKind` union (`IncrementalByTimeRangeKind.time_column`) holds an expression. + That routed the entire `kind (...)` subtree through a dialect-specific generator, so tsql's boolean-literal preprocessing rewrote `forward_only TRUE` into `forward_only (1 = 1)`. That reparses without error, but `str_to_bool` on `Paren(EQ(1, 1)).name` (`""`) evaluates to `False`, so the value silently flips on reload. From 1e8dc950cc3b55f3a594cd89ec7520d642e9fb25 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:34:00 -0600 Subject: [PATCH 66/67] fix(mssql): skip dbo on drop schema (#6026) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- sqlmesh/core/engine_adapter/mssql.py | 14 ++++++++++- tests/core/engine_adapter/test_mssql.py | 32 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index fca6b4cc9f..70031de55a 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -180,7 +180,10 @@ def drop_schema( **drop_args: t.Dict[str, exp.Expr], ) -> None: """ - MsSql doesn't support CASCADE clause and drops schemas unconditionally. + MsSql doesn't support CASCADE clause so objects are dropped individually. + + SQL Server also forbids dropping the built-in ``dbo`` schema (error 15150). + Objects inside it are still dropped when cascade=True, but the schema itself is left in place. """ if cascade: objects = self._get_data_objects(schema_name) @@ -199,6 +202,15 @@ def drop_schema( object_table, exists=ignore_if_not_exists, ) + + schema = ( + (schema_name.db or schema_name.name) + if isinstance(schema_name, exp.Table) + else schema_name + ) + if schema.lower() == "dbo": + return + super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False) def merge( diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index 007a59b365..e54a42ff29 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -714,6 +714,38 @@ def test_drop_schema(make_mocked_engine_adapter: t.Callable): ] +def test_drop_schema_skips_dbo(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) + + adapter._get_data_objects = mock.Mock() + adapter._get_data_objects.return_value = [ + DataObject( + catalog="test_catalog", + schema="dbo", + name="test_view", + type=DataObjectType.from_str("VIEW"), + ) + ] + + # String "dbo" path (original coverage) + adapter.drop_schema("dbo", cascade=True) + + sql_calls = to_sql_calls(adapter) + assert """DROP VIEW IF EXISTS [dbo].[test_view];""" in sql_calls + assert """DROP SCHEMA IF EXISTS [dbo];""" not in sql_calls + + # exp.to_table("dbo") path — schema ends up in .name, not .db + adapter2 = make_mocked_engine_adapter(MSSQLEngineAdapter) + adapter2._get_data_objects = mock.Mock() + adapter2._get_data_objects.return_value = [] + + schema_name = exp.to_table("dbo", dialect="tsql") + adapter2.drop_schema(schema_name, cascade=False) + + sql_calls2 = to_sql_calls(adapter2) + assert """DROP SCHEMA IF EXISTS [dbo];""" not in sql_calls2 + + def test_drop_schema_with_special_identifiers(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) From 5f1911d401c65ae83deda9b0ff8a9197e24e40b2 Mon Sep 17 00:00:00 2001 From: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:29:51 -0600 Subject: [PATCH 67/67] fix(ci): fixing 1.5 dbt tests (#6073) Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com> --- .github/workflows/pr.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0c26d39a2f..05b737a4b7 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -558,7 +558,8 @@ jobs: run: | source ../../.venv/bin/activate sed -i 's/target: in_memory/target: postgres/g' profiles.yml - if [[ $(echo -e "${{ matrix.dbt-version }}\n1.5.0" | sort -V | head -n1) == "${{ matrix.dbt-version }}" ]] && [[ "${{ matrix.dbt-version }}" != "1.5.0" ]]; then + if [[ "${{ matrix.dbt-version }}" == "1.3" ]] || \ + [[ "${{ matrix.dbt-version }}" == "1.4" ]]; then echo "DBT version is ${{ matrix.dbt-version }} (< 1.5.0), removing version parameters..." sed -i -e 's/, version=1) }}/) }}/g' -e 's/, v=1) }}/) }}/g' models/top_waiters.sql else